diff --git a/AssetStudio/AssetsManager.cs b/AssetStudio/AssetsManager.cs index 40e75272..954d1390 100644 --- a/AssetStudio/AssetsManager.cs +++ b/AssetStudio/AssetsManager.cs @@ -37,7 +37,7 @@ public void SetAssetFilter(params ClassIDType[] classIDTypes) { filteredAssetTypesList.Add(ClassIDType.MonoScript); } - if (classIDTypes.Contains(ClassIDType.Sprite)) + if (classIDTypes.Contains(ClassIDType.Sprite) || classIDTypes.Contains(ClassIDType.AkPortraitSprite)) { filteredAssetTypesList.UnionWith(new HashSet { diff --git a/AssetStudioCLI/Components/Arknights/AkSpriteHelper.cs b/AssetStudioCLI/Components/Arknights/AkSpriteHelper.cs index fec1f3ea..b86ff285 100644 --- a/AssetStudioCLI/Components/Arknights/AkSpriteHelper.cs +++ b/AssetStudioCLI/Components/Arknights/AkSpriteHelper.cs @@ -1,10 +1,13 @@ -using AssetStudio; +using Arknights.PortraitSpriteMono; +using AssetStudio; using AssetStudioCLI; using AssetStudioCLI.Options; +using Newtonsoft.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System; +using System.Collections.Generic; using System.IO; namespace Arknights @@ -83,6 +86,61 @@ public static Image AkGetImage(this Sprite m_Sprite, AvgSprite avgSprite return null; } + public static Image AkGetImage(this PortraitSprite portraitSprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On) + { + if (portraitSprite.Texture != null && portraitSprite.AlphaTexture != null) + { + var tex = CutImage(portraitSprite.Texture.ConvertToImage(false), portraitSprite.TextureRect, portraitSprite.DownscaleMultiplier, portraitSprite.Rotate); + + if (spriteMaskMode == SpriteMaskMode.Off) + { + return tex; + } + else + { + var alphaTex = CutImage(portraitSprite.AlphaTexture.ConvertToImage(false), portraitSprite.TextureRect, portraitSprite.DownscaleMultiplier, portraitSprite.Rotate); + tex.ApplyRGBMask(alphaTex); + return tex; + } + } + + return null; + } + + public static List GeneratePortraits(AssetItem asset) + { + var portraits = new List(); + + var portraitsDict = ((MonoBehaviour)asset.Asset).ToType(); + if (portraitsDict == null) + { + Logger.Warning("Portraits MonoBehaviour is not readable."); + return portraits; + } + var portraitsJson = JsonConvert.SerializeObject(portraitsDict); + var portraitsData = JsonConvert.DeserializeObject(portraitsJson); + + var atlasTex = (Texture2D)Studio.loadedAssetsList.Find(x => x.m_PathID == portraitsData._atlas.Texture.m_PathID).Asset; + var atlasAlpha = (Texture2D)Studio.loadedAssetsList.Find(x => x.m_PathID == portraitsData._atlas.Alpha.m_PathID).Asset; + + foreach (var portraitData in portraitsData._sprites) + { + var portraitSprite = new PortraitSprite() + { + Name = portraitData.Name, + AssetsFile = atlasTex.assetsFile, + Container = asset.Container, + Texture = atlasTex, + AlphaTexture = atlasAlpha, + TextureRect = new Rectf(portraitData.Rect.X, portraitData.Rect.Y, portraitData.Rect.W, portraitData.Rect.H), + Rotate = portraitData.Rotate, + }; + portraits.Add(portraitSprite); + } + + return portraits; + } + private static void ApplyRGBMask(this Image tex, Image texMask) { using (texMask) @@ -120,29 +178,31 @@ private static void ApplyRGBMask(this Image tex, Image texMask) } } - private static Image CutImage(Image originalImage, Rectf textureRect, float downscaleMultiplier) + private static Image CutImage(Image originalImage, Rectf textureRect, float downscaleMultiplier, bool rotate = false) { if (originalImage != null) { - using (originalImage) + if (downscaleMultiplier > 0f && downscaleMultiplier != 1f) { - if (downscaleMultiplier > 0f && downscaleMultiplier != 1f) - { - var newSize = (Size)(originalImage.Size() / downscaleMultiplier); - originalImage.Mutate(x => x.Resize(newSize, KnownResamplers.Lanczos3, compand: true)); - } - var rectX = (int)Math.Floor(textureRect.x); - var rectY = (int)Math.Floor(textureRect.y); - var rectRight = (int)Math.Ceiling(textureRect.x + textureRect.width); - var rectBottom = (int)Math.Ceiling(textureRect.y + textureRect.height); - rectRight = Math.Min(rectRight, originalImage.Width); - rectBottom = Math.Min(rectBottom, originalImage.Height); - var rect = new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY); - var spriteImage = originalImage.Clone(x => x.Crop(rect)); - spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); - - return spriteImage; + var newSize = (Size)(originalImage.Size() / downscaleMultiplier); + originalImage.Mutate(x => x.Resize(newSize, KnownResamplers.Lanczos3, compand: true)); } + var rectX = (int)Math.Floor(textureRect.x); + var rectY = (int)Math.Floor(textureRect.y); + var rectRight = (int)Math.Ceiling(textureRect.x + textureRect.width); + var rectBottom = (int)Math.Ceiling(textureRect.y + textureRect.height); + rectRight = Math.Min(rectRight, originalImage.Width); + rectBottom = Math.Min(rectBottom, originalImage.Height); + var rect = new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY); + var spriteImage = originalImage.Clone(x => x.Crop(rect)); + originalImage.Dispose(); + if (rotate) + { + spriteImage.Mutate(x => x.Rotate(RotateMode.Rotate270)); + } + spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); + + return spriteImage; } return null; diff --git a/AssetStudioCLI/Components/Arknights/AvgSprite.cs b/AssetStudioCLI/Components/Arknights/AvgSprite.cs index 906afbcc..9f69cadf 100644 --- a/AssetStudioCLI/Components/Arknights/AvgSprite.cs +++ b/AssetStudioCLI/Components/Arknights/AvgSprite.cs @@ -1,4 +1,4 @@ -using Arknights.AvgCharHub; +using Arknights.AvgCharHubMono; using AssetStudio; using AssetStudioCLI; using SixLabors.ImageSharp; diff --git a/AssetStudioCLI/Components/Arknights/AvgSpriteConfig.cs b/AssetStudioCLI/Components/Arknights/AvgSpriteConfig.cs index d834743a..7db3f6e1 100644 --- a/AssetStudioCLI/Components/Arknights/AvgSpriteConfig.cs +++ b/AssetStudioCLI/Components/Arknights/AvgSpriteConfig.cs @@ -1,6 +1,6 @@ using AssetStudio; -namespace Arknights.AvgCharHub +namespace Arknights.AvgCharHubMono { internal class AvgAssetIDs { diff --git a/AssetStudioCLI/Components/Arknights/PortraitSprite.cs b/AssetStudioCLI/Components/Arknights/PortraitSprite.cs new file mode 100644 index 00000000..84973b89 --- /dev/null +++ b/AssetStudioCLI/Components/Arknights/PortraitSprite.cs @@ -0,0 +1,24 @@ +using AssetStudio; + + +namespace Arknights +{ + internal class PortraitSprite + { + public string Name { get; set; } + public ClassIDType Type { get; } + public SerializedFile AssetsFile { get; set; } + public string Container { get; set; } + public Texture2D Texture { get; set; } + public Texture2D AlphaTexture { get; set; } + public Rectf TextureRect { get; set; } + public bool Rotate { get; set; } + public float DownscaleMultiplier { get; } + + public PortraitSprite() + { + Type = ClassIDType.AkPortraitSprite; + DownscaleMultiplier = 1f; + } + } +} diff --git a/AssetStudioCLI/Components/Arknights/PortraitSpriteConfig.cs b/AssetStudioCLI/Components/Arknights/PortraitSpriteConfig.cs new file mode 100644 index 00000000..f59e1188 --- /dev/null +++ b/AssetStudioCLI/Components/Arknights/PortraitSpriteConfig.cs @@ -0,0 +1,41 @@ +namespace Arknights.PortraitSpriteMono +{ + internal class PortraitRect + { + public float X { get; set; } + public float Y { get; set; } + public float W { get; set; } + public float H { get; set; } + } + + internal class AtlasSprite + { + public string Name { get; set; } + public string Guid { get; set; } + public int Atlas { get; set; } + public PortraitRect Rect { get; set; } + public bool Rotate { get; set; } + } + + internal class TextureIDs + { + public int m_FileID { get; set; } + public long m_PathID { get; set; } + } + + internal class AtlasInfo + { + public int Index { get; set; } + public TextureIDs Texture { get; set; } + public TextureIDs Alpha { get; set; } + public int Size { get; set; } + } + + internal class PortraitSpriteConfig + { + public string m_Name { get; set; } + public AtlasSprite[] _sprites { get; set; } + public AtlasInfo _atlas { get; set; } + public int _index { get; set; } + } +} diff --git a/AssetStudioCLI/Components/AssetItem.cs b/AssetStudioCLI/Components/AssetItem.cs index 00f929a4..acb25ac7 100644 --- a/AssetStudioCLI/Components/AssetItem.cs +++ b/AssetStudioCLI/Components/AssetItem.cs @@ -1,4 +1,5 @@ -using AssetStudio; +using Arknights; +using AssetStudio; namespace AssetStudioCLI { @@ -13,6 +14,7 @@ internal class AssetItem public ClassIDType Type; public string Text; public string UniqueID; + public PortraitSprite AkPortraitSprite; public AssetItem(Object asset) { @@ -23,5 +25,17 @@ public AssetItem(Object asset) m_PathID = asset.m_PathID; FullSize = asset.byteSize; } + + public AssetItem(PortraitSprite akPortraitSprite) + { + Asset = null; + SourceFile = akPortraitSprite.AssetsFile; + Container = akPortraitSprite.Container; + Type = akPortraitSprite.Type; + TypeString = Type.ToString(); + Text = akPortraitSprite.Name; + m_PathID = -1; + AkPortraitSprite = akPortraitSprite; + } } } diff --git a/AssetStudioCLI/Exporter.cs b/AssetStudioCLI/Exporter.cs index b48d6f9d..4c6b0cbc 100644 --- a/AssetStudioCLI/Exporter.cs +++ b/AssetStudioCLI/Exporter.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace AssetStudioCLI { @@ -224,15 +225,16 @@ public static bool ExportSprite(AssetItem item, string exportPath) { alias = $"_{avgSprite.Alias}"; } - } - if (!CLIOptions.f_akOriginalAvgNames.Value) - { - if ((m_Sprite.m_Name.Length < 3 && m_Sprite.m_Name.All(char.IsDigit)) //not grouped ("spriteIndex") - || (m_Sprite.m_Name.Length < 5 && m_Sprite.m_Name.Contains('$') && m_Sprite.m_Name.Split('$')[0].All(char.IsDigit))) //grouped ("spriteIndex$groupIndex") + if (!CLIOptions.f_akOriginalAvgNames.Value) { - var fullName = Path.GetFileNameWithoutExtension(item.Container); - item.Text = $"{fullName}#{m_Sprite.m_Name}"; + var groupedPattern = new Regex(@"^\d{1,2}\$\d{1,2}$"); // "spriteIndex$groupIndex" + var notGroupedPattern = new Regex(@"^\d{1,2}$"); // "spriteIndex" + if (groupedPattern.IsMatch(m_Sprite.m_Name) || notGroupedPattern.IsMatch(m_Sprite.m_Name)) + { + var fullName = Path.GetFileNameWithoutExtension(item.Container); + item.Text = $"{fullName}#{m_Sprite.m_Name}"; + } } } @@ -272,8 +274,36 @@ public static bool ExportSprite(AssetItem item, string exportPath) return false; } + public static bool ExportPortraitSprite(AssetItem item, string exportPath) + { + var type = CLIOptions.o_imageFormat.Value; + var spriteMaskMode = CLIOptions.o_akSpriteMaskMode.Value != AkSpriteMaskMode.None ? SpriteMaskMode.Export : SpriteMaskMode.Off; + if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath)) + return false; + + var image = item.AkPortraitSprite.AkGetImage(spriteMaskMode: spriteMaskMode); + if (image != null) + { + using (image) + { + using (var file = File.OpenWrite(exportFullPath)) + { + image.WriteToStream(file, type); + } + Logger.Debug($"{item.TypeString}: \"{item.Text}\" exported to \"{exportFullPath}\""); + return true; + } + } + return false; + } + public static bool ExportRawFile(AssetItem item, string exportPath) { + if (item.Asset == null) + { + Logger.Warning($"Raw export is not supported for \"{item.Text}\" ({item.TypeString}) file"); + return false; + } if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath)) return false; File.WriteAllBytes(exportFullPath, item.Asset.GetRawData()); @@ -284,6 +314,11 @@ public static bool ExportRawFile(AssetItem item, string exportPath) public static bool ExportDumpFile(AssetItem item, string exportPath) { + if (item.Asset == null) + { + Logger.Warning($"Dump is not supported for \"{item.Text}\" ({item.TypeString}) file"); + return false; + } if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath)) return false; var str = item.Asset.Dump(); @@ -439,6 +474,8 @@ public static bool ExportConvertFile(AssetItem item, string exportPath) return ExportFont(item, exportPath); case ClassIDType.Sprite: return ExportSprite(item, exportPath); + case ClassIDType.AkPortraitSprite: + return ExportPortraitSprite(item, exportPath); case ClassIDType.Mesh: return ExportMesh(item, exportPath); default: diff --git a/AssetStudioCLI/Options/CLIOptions.cs b/AssetStudioCLI/Options/CLIOptions.cs index 13ad51ad..aad1ebd3 100644 --- a/AssetStudioCLI/Options/CLIOptions.cs +++ b/AssetStudioCLI/Options/CLIOptions.cs @@ -154,6 +154,7 @@ private static void InitOptions() { ClassIDType.Texture2D, ClassIDType.Sprite, + ClassIDType.AkPortraitSprite, ClassIDType.TextAsset, ClassIDType.MonoBehaviour, ClassIDType.Font, @@ -184,8 +185,8 @@ private static void InitOptions() optionDefaultValue: supportedAssetTypes, optionName: "-t, --asset-type ", optionDescription: "Specify asset type(s) to export\n" + - "\n" + + "\n" + "All - export all asset types, which are listed in the values\n" + "*To specify multiple asset types, write them separated by ',' or ';' without spaces\n" + "Examples: \"-t sprite\" or \"-t tex2d,sprite,audio\" or \"-t tex2d;sprite;font\"\n", @@ -532,6 +533,9 @@ public static void ParseArgs(string[] args) case "sprite": o_exportAssetTypes.Value.Add(ClassIDType.Sprite); break; + case "akportrait": + o_exportAssetTypes.Value.Add(ClassIDType.AkPortraitSprite); + break; case "textasset": o_exportAssetTypes.Value.Add(ClassIDType.TextAsset); break; @@ -959,10 +963,10 @@ public static void ShowCurrentOptions() sb.AppendLine($"# Asset Group Option: {o_groupAssetsBy}"); sb.AppendLine($"# Export Image Format: {o_imageFormat}"); sb.AppendLine($"# Export Audio Format: {o_audioFormat}"); - sb.AppendLine($"# [Arkingths] Sprite Mode: {o_akSpriteMaskMode}"); + sb.AppendLine($"# [Arkingths] Sprite Mask Mode: {o_akSpriteMaskMode}"); sb.AppendLine($"# [Arknights] Mask Resampler: {resamplerName}"); sb.AppendLine($"# [Arknights] Mask Gamma Correction: {o_akAlphaMaskGamma.Value * 10:+#;-#;0}%"); - sb.AppendLine($"# [Arknights] Original Avg Names: {f_akOriginalAvgNames}"); + sb.AppendLine($"# [Arknights] Don't Fix Avg Names: {f_akOriginalAvgNames}"); sb.AppendLine($"# [Arknights] Add Aliases: {f_akAddAliases}"); sb.AppendLine($"# Log Level: {o_logLevel}"); sb.AppendLine($"# Log Output: {o_logOutput}"); diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index d7efccd9..c12bb559 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -145,6 +145,15 @@ public static void ParseAssets() if (containers.ContainsKey(asset.Asset)) { asset.Container = containers[asset.Asset]; + + if (asset.Type == ClassIDType.MonoBehaviour && asset.Container.Contains("/arts/charportraits/portraits")) + { + var portraitsList = Arknights.AkSpriteHelper.GeneratePortraits(asset); + foreach (var portrait in portraitsList) + { + exportableAssetsList.Add(new AssetItem(portrait)); + } + } } } if (CLIOptions.o_workMode.Value != WorkMode.ExportLive2D)