This commit is contained in:
Razmoth
2023-04-01 17:51:33 +04:00
parent 29e99495de
commit c7d60450f8
25 changed files with 1077 additions and 735 deletions

View File

@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="displayAll" value="False" />
<add key="enablePreview" value="True" />
<add key="displayInfo" value="True" />
<add key="exportAll" value="False" />
<add key="openAfterExport" value="True" />
<add key="assetGroupOption" value="0" />
<add key="convertTexture" value="True" />
<add key="convertAudio" value="True" />
<add key="convertType" value="Png" />
@@ -14,6 +11,7 @@
<add key="exportAllNodes" value="True" />
<add key="exportSkins" value="True" />
<add key="exportAnimations" value="True" />
<add key="exportUV0UV1" value="False" />
<add key="boneSize" value="10" />
<add key="fbxVersion" value="3" />
<add key="fbxFormat" value="0" />
@@ -22,14 +20,5 @@
<add key="castToBone" value="False" />
<add key="restoreExtensionName" value="True" />
<add key="exportAllUvsAsDiffuseMaps" value="False" />
<add key="key" value="147" />
<add key="encrypted" value="True" />
<add key="skipRenderer" value="False" />
<add key="selectedGame" value="0" />
<add key="collectAnimations" value="False" />
<add key="enableResolveDependencies" value="True" />
<add key="selectedCNUnityKey" value="0" />
<add key="selectedAssetMapType" value="0" />
<add key="exportMiHoYoBinData" value="True" />
</appSettings>
</configuration>

View File

@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Options.cs">
<Compile Update="Settings.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
</Compile>

View File

@@ -0,0 +1,189 @@
using System;
using System.IO;
using System.Linq;
using System.CommandLine;
using System.CommandLine.Binding;
using System.CommandLine.Parsing;
using System.Text.RegularExpressions;
using AssetStudio;
namespace AssetStudioCLI
{
public static class CommandLine
{
public static void Init(string[] args)
{
var rootCommand = RegisterOptions();
rootCommand.Invoke(args);
}
public static RootCommand RegisterOptions()
{
var optionsBinder = new OptionsBinder();
var rootCommand = new RootCommand()
{
optionsBinder.Silent,
optionsBinder.TypeFilter,
optionsBinder.NameFilter,
optionsBinder.ContainerFilter,
optionsBinder.GameName,
optionsBinder.MapOp,
optionsBinder.MapType,
optionsBinder.MapName,
optionsBinder.GroupAssetsType,
optionsBinder.Model,
optionsBinder.Key,
optionsBinder.AIFile,
optionsBinder.DummyDllFolder,
optionsBinder.Input,
optionsBinder.Output
};
rootCommand.SetHandler(Program.Run, optionsBinder);
return rootCommand;
}
}
public class Options
{
public bool Silent { get; set; }
public ClassIDType[] TypeFilter { get; set; }
public Regex[] NameFilter { get; set; }
public Regex[] ContainerFilter { get; set; }
public string GameName { get; set; }
public MapOpType MapOp { get; set; }
public ExportListType MapType { get; set; }
public string MapName { get; set; }
public AssetGroupOption GroupAssetsType { get; set; }
public bool Model { get; set; }
public byte Key { get; set; }
public FileInfo AIFile { get; set; }
public DirectoryInfo DummyDllFolder { get; set; }
public FileInfo Input { get; set; }
public DirectoryInfo Output { get; set; }
}
public class OptionsBinder : BinderBase<Options>
{
public readonly Option<bool> Silent;
public readonly Option<ClassIDType[]> TypeFilter;
public readonly Option<Regex[]> NameFilter;
public readonly Option<Regex[]> ContainerFilter;
public readonly Option<string> GameName;
public readonly Option<MapOpType> MapOp;
public readonly Option<ExportListType> MapType;
public readonly Option<string> MapName;
public readonly Option<AssetGroupOption> GroupAssetsType;
public readonly Option<bool> Model;
public readonly Option<byte> Key;
public readonly Option<FileInfo> AIFile;
public readonly Option<DirectoryInfo> DummyDllFolder;
public readonly Argument<FileInfo> Input;
public readonly Argument<DirectoryInfo> Output;
public OptionsBinder()
{
Silent = new Option<bool>("--silent", "Hide log messages.");
TypeFilter = new Option<ClassIDType[]>("--types", "Specify unity class type(s)") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Texture2D|Sprite|etc.." };
NameFilter = new Option<Regex[]>("--names", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true };
ContainerFilter = new Option<Regex[]>("--containers", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true };
GameName = new Option<string>("--game", $"Specify Game.") { IsRequired = true };
MapOp = new Option<MapOpType>("--map_op", "Specify which map to build.");
MapType = new Option<ExportListType>("--map_type", "AssetMap output type.");
MapName = new Option<string>("--map_name", () => "assets_map", "Specify AssetMap file name.");
GroupAssetsType = new Option<AssetGroupOption>("--group_assets", "Specify how exported assets should be grouped.");
Model = new Option<bool>("--models", "Enable to export models only");
AIFile = new Option<FileInfo>("--ai_file", "Specify asset_index json file path (to recover GI containers).").LegalFilePathsOnly();
DummyDllFolder = new Option<DirectoryInfo>("--dummy_dlls", "Specify DummyDll path.").LegalFilePathsOnly();
Input = new Argument<FileInfo>("input_path", "Input file/folder.").LegalFilePathsOnly();
Output = new Argument<DirectoryInfo>("output_path", "Output folder.").LegalFilePathsOnly();
Key = new Option<byte>("--key", result =>
{
var value = result.Tokens.Single().Value;
if (value.StartsWith("0x"))
{
value = value[2..];
return Convert.ToByte(value, 0x10);
}
else
{
return byte.Parse(value);
}
}, false, "XOR key to decrypt MiHoYoBinData.");
TypeFilter.AddValidator(FilterValidator);
NameFilter.AddValidator(FilterValidator);
ContainerFilter.AddValidator(FilterValidator);
Key.AddValidator(result =>
{
var value = result.Tokens.Single().Value;
try
{
if (value.StartsWith("0x"))
{
value = value.Substring(2);
Convert.ToByte(value, 0x10);
}
else
{
byte.Parse(value);
}
}
catch (Exception e)
{
result.ErrorMessage = "Invalid byte value.\n" + e.Message;
}
});
GameName.FromAmong(GameManager.GetGameNames());
GroupAssetsType.SetDefaultValue(AssetGroupOption.ByType);
MapOp.SetDefaultValue(MapOpType.Load);
MapType.SetDefaultValue(ExportListType.XML);
}
public void FilterValidator(OptionResult result)
{
var values = result.Tokens.Select(x => x.Value).ToArray();
foreach (var val in values)
{
if (string.IsNullOrWhiteSpace(val))
{
result.ErrorMessage = "Empty string.";
return;
}
try
{
Regex.Match("", val, RegexOptions.IgnoreCase);
}
catch (ArgumentException e)
{
result.ErrorMessage = "Invalid Regex.\n" + e.Message;
return;
}
}
}
protected override Options GetBoundValue(BindingContext bindingContext) =>
new()
{
Silent = bindingContext.ParseResult.GetValueForOption(Silent),
TypeFilter = bindingContext.ParseResult.GetValueForOption(TypeFilter),
NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter),
ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter),
GameName = bindingContext.ParseResult.GetValueForOption(GameName),
MapOp = bindingContext.ParseResult.GetValueForOption(MapOp),
MapType = bindingContext.ParseResult.GetValueForOption(MapType),
MapName = bindingContext.ParseResult.GetValueForOption(MapName),
GroupAssetsType = bindingContext.ParseResult.GetValueForOption(GroupAssetsType),
Model = bindingContext.ParseResult.GetValueForOption(Model),
Key = bindingContext.ParseResult.GetValueForOption(Key),
AIFile = bindingContext.ParseResult.GetValueForOption(AIFile),
DummyDllFolder = bindingContext.ParseResult.GetValueForOption(DummyDllFolder),
Input = bindingContext.ParseResult.GetValueForArgument(Input),
Output = bindingContext.ParseResult.GetValueForArgument(Output)
};
}
}

View File

@@ -91,6 +91,22 @@ namespace AssetStudioCLI
return true;
}
public static bool ExportMonoBehaviour(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".json", out var exportFullPath))
return false;
var m_MonoBehaviour = (MonoBehaviour)item.Asset;
var type = m_MonoBehaviour.ToType();
if (type == null)
{
var m_Type = Studio.MonoBehaviourToTypeTree(m_MonoBehaviour);
type = m_MonoBehaviour.ToType(m_Type);
}
var str = JsonConvert.SerializeObject(type, Formatting.Indented);
File.WriteAllText(exportFullPath, str);
return true;
}
public static bool ExportMiHoYoBinData(AssetItem item, string exportPath)
{
string exportFullPath;
@@ -315,6 +331,13 @@ namespace AssetStudioCLI
return true;
}
public static bool ExportGameObject(AssetItem item, string exportPath, List <AssetItem> animationList = null)
{
var m_GameObject = (GameObject)item.Asset;
ExportGameObject(m_GameObject, exportPath, animationList);
return true;
}
public static void ExportGameObject(GameObject gameObject, string exportPath, List<AssetItem> animationList = null)
{
var convert = animationList != null
@@ -369,6 +392,8 @@ namespace AssetStudioCLI
{
switch (item.Type)
{
case ClassIDType.GameObject:
return ExportGameObject(item, exportPath);
case ClassIDType.Texture2D:
return ExportTexture2D(item, exportPath);
case ClassIDType.AudioClip:
@@ -378,7 +403,7 @@ namespace AssetStudioCLI
case ClassIDType.TextAsset:
return ExportTextAsset(item, exportPath);
case ClassIDType.MonoBehaviour:
return false;
return ExportMonoBehaviour(item, exportPath);
case ClassIDType.Font:
return ExportFont(item, exportPath);
case ClassIDType.Mesh:
@@ -390,7 +415,7 @@ namespace AssetStudioCLI
case ClassIDType.Sprite:
return ExportSprite(item, exportPath);
case ClassIDType.Animator:
return ExportAnimator(item, exportPath, new List<AssetItem>());
return ExportAnimator(item, exportPath);
case ClassIDType.AnimationClip:
return false;
case ClassIDType.MiHoYoBinData:

View File

@@ -1,276 +1,132 @@
using System;
using System.IO;
using System.Linq;
using System.CommandLine;
using System.CommandLine.Binding;
using System.Text.RegularExpressions;
using static AssetStudioCLI.Studio;
using System.Threading;
using AssetStudio;
using System.CommandLine.Parsing;
using static AssetStudioCLI.Studio;
namespace AssetStudioCLI
{
public class Program
{
public static void Main(string[] args)
public static void Main(string[] args) => CommandLine.Init(args);
public static void Run(Options o)
{
var rootCommand = RegisterOptions();
rootCommand.Invoke(args);
}
public static RootCommand RegisterOptions()
{
var optionsBinder = new OptionsBinder();
var rootCommand = new RootCommand()
try
{
optionsBinder.Silent,
optionsBinder.TypeFilter,
optionsBinder.NameFilter,
optionsBinder.ContainerFilter,
optionsBinder.GameName,
optionsBinder.MapOp,
optionsBinder.MapType,
optionsBinder.MapName,
optionsBinder.GroupAssetsType,
optionsBinder.IncludeAnimators,
optionsBinder.SkipMiHoYoBinData,
optionsBinder.Key,
optionsBinder.AIFile,
optionsBinder.Input,
optionsBinder.Output
};
var game = GameManager.GetGame(o.GameName);
rootCommand.SetHandler((Options o) =>
{
try
if (game == null)
{
var game = GameManager.GetGame(o.GameName);
if (game == null)
{
Console.WriteLine("Invalid Game !!");
Console.WriteLine(GameManager.SupportedGames());
return;
}
Studio.Game = game;
Logger.Default = new ConsoleLogger();
assetsManager.Silent = o.Silent;
assetsManager.Game = game;
MiHoYoBinData.Exportable = !o.SkipMiHoYoBinData;
if (o.Key != default)
{
if (o.SkipMiHoYoBinData)
{
Logger.Warning("Key is set but IndexObject/MiHoYoBinData is excluded, ignoring key...");
}
else
{
MiHoYoBinData.Encrypted = true;
MiHoYoBinData.Key = o.Key;
}
}
if (o.AIFile != null && game.Type.IsGISubGroup())
{
ResourceIndex.FromFile(o.AIFile.FullName);
}
Logger.Info("Scanning for files");
var files = o.Input.Attributes.HasFlag(FileAttributes.Directory) ? Directory.GetFiles(o.Input.FullName, "*.*", SearchOption.AllDirectories).OrderBy(x => x.Length).ToArray() : new string[] { o.Input.FullName };
Logger.Info(string.Format("Found {0} file(s)", files.Length));
if (o.MapOp.Equals(MapOpType.None))
{
var i = 0;
foreach (var file in files)
{
assetsManager.LoadFiles(file);
if (assetsManager.assetsFileList.Count > 0)
{
BuildAssetData(o.TypeFilter, o.NameFilter, o.ContainerFilter, ref i);
ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType);
}
exportableAssets.Clear();
assetsManager.Clear();
}
}
if (o.MapOp.HasFlag(MapOpType.CABMap))
{
AssetsHelper.BuildCABMap(files, "", "", game);
}
if (o.MapOp.HasFlag(MapOpType.AssetMap))
{
if (files.Length == 1)
{
throw new Exception("Unable to build AssetMap with input_path as a file !!");
}
var assets = AssetsHelper.BuildAssetMap(files, game, o.TypeFilter, o.NameFilter, o.ContainerFilter);
if (!o.Output.Exists)
{
o.Output.Create();
}
if (string.IsNullOrEmpty(o.MapName))
{
o.MapName = "assets_map";
}
AssetsHelper.ExportAssetsMap(assets, o.MapName, o.Output.FullName, o.MapType);
}
Console.WriteLine("Invalid Game !!");
Console.WriteLine(GameManager.SupportedGames());
return;
}
catch (Exception e)
Studio.Game = game;
Logger.Default = new ConsoleLogger();
assetsManager.Silent = o.Silent;
assetsManager.Game = game;
if (!o.TypeFilter.IsNullOrEmpty())
{
Console.WriteLine(e.Message);
Console.WriteLine(e.StackTrace);
}
}, optionsBinder);
return rootCommand;
}
}
public class Options
{
public bool Silent { get; set; }
public ClassIDType[] TypeFilter { get; set; }
public Regex[] NameFilter { get; set; }
public Regex[] ContainerFilter { get; set; }
public string GameName { get; set; }
public MapOpType MapOp { get; set; }
public ExportListType MapType { get; set; }
public string MapName { get; set; }
public AssetGroupOption GroupAssetsType { get; set; }
public bool SkipRenderer { get; set; }
public bool SkipMiHoYoBinData { get; set; }
public byte Key { get; set; }
public FileInfo AIFile { get; set; }
public FileInfo Input { get; set; }
public DirectoryInfo Output { get; set; }
}
public class OptionsBinder : BinderBase<Options>
{
public readonly Option<bool> Silent;
public readonly Option<ClassIDType[]> TypeFilter;
public readonly Option<Regex[]> NameFilter;
public readonly Option<Regex[]> ContainerFilter;
public readonly Option<string> GameName;
public readonly Option<MapOpType> MapOp;
public readonly Option<ExportListType> MapType;
public readonly Option<string> MapName;
public readonly Option<AssetGroupOption> GroupAssetsType;
public readonly Option<bool> IncludeAnimators;
public readonly Option<bool> SkipMiHoYoBinData;
public readonly Option<byte> Key;
public readonly Option<FileInfo> AIFile;
public readonly Argument<FileInfo> Input;
public readonly Argument<DirectoryInfo> Output;
public OptionsBinder()
{
Silent = new Option<bool>("--silent", "Hide log messages.");
TypeFilter = new Option<ClassIDType[]>("--types", "Specify unity class type(s)") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Texture2D|Sprite|etc.." };
NameFilter = new Option<Regex[]>("--names", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true };
ContainerFilter = new Option<Regex[]>("--containers", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true };
GameName = new Option<string>("--game", $"Specify Game.") { IsRequired = true };
MapOp = new Option<MapOpType>("--map_op", "Specify which map to build.");
MapType = new Option<ExportListType>("--map_type", "AssetMap output type.");
MapName = new Option<string>("--map_name", "Specify AssetMap file name.");
GroupAssetsType = new Option<AssetGroupOption>("--group_assets_type", "Specify how exported assets should be grouped.");
IncludeAnimators = new Option<bool>("--skip_re", "Include Animator with Export.");
SkipMiHoYoBinData = new Option<bool>("--skip_mihoyobindata", "Exclude IndexObject/MiHoYoBinData from AssetMap/Export.");
AIFile = new Option<FileInfo>("--ai_file", "Specify asset_index json file path (to recover GI containers).").LegalFilePathsOnly();
Input = new Argument<FileInfo>("input_path", "Input file/folder.").LegalFilePathsOnly();
Output = new Argument<DirectoryInfo>("output_path", "Output folder.").LegalFilePathsOnly();
Key = new Option<byte>("--key", result =>
{
var value = result.Tokens.Single().Value;
if (value.StartsWith("0x"))
{
value = value.Substring(2);
return Convert.ToByte(value, 0x10);
}
else
{
return byte.Parse(value);
}
}, false, "Key to decrypt MiHoYoBinData.");
TypeFilter.AddValidator(FilterValidator);
NameFilter.AddValidator(FilterValidator);
ContainerFilter.AddValidator(FilterValidator);
Key.AddValidator(result =>
{
var value = result.Tokens.Single().Value;
try
{
if (value.StartsWith("0x"))
foreach (var kv in assetsManager.ExportableTypes)
{
value = value.Substring(2);
Convert.ToByte(value, 0x10);
assetsManager.ExportableTypes[kv.Key] = o.TypeFilter.Contains(kv.Key);
}
}
if (o.Model)
{
foreach (var kv in assetsManager.ExportableTypes)
{
assetsManager.ExportableTypes[kv.Key] = false;
}
assetsManager.ExportableTypes[ClassIDType.Animator] = true;
assetsManager.ExportableTypes[ClassIDType.GameObject] = true;
assetsManager.ExportableTypes[ClassIDType.Texture2D] = true;
assetsManager.ExportableTypes[ClassIDType.Material] = true;
assetsManager.ExportableTypes[ClassIDType.Renderer] = true;
assetsManager.ExportableTypes[ClassIDType.Mesh] = true;
ModelOnly = true;
}
if (o.Key != default)
{
if (!assetsManager.ExportableTypes[ClassIDType.MiHoYoBinData])
{
Logger.Warning("Key is set but MiHoYoBinData is skipped, ignoring key...");
}
else
{
byte.Parse(value);
MiHoYoBinData.Encrypted = true;
MiHoYoBinData.Key = o.Key;
}
}
catch(Exception e)
if (o.AIFile != null && game.Type.IsGISubGroup())
{
result.ErrorMessage = "Invalid byte value.\n" + e.Message;
}
});
ResourceIndex.FromFile(o.AIFile.FullName);
}
GameName.FromAmong(GameManager.GetGameNames());
GroupAssetsType.SetDefaultValue(0);
MapOp.SetDefaultValue(MapOpType.None);
MapType.SetDefaultValue(ExportListType.XML);
}
public void FilterValidator(OptionResult result)
{
{
var values = result.Tokens.Select(x => x.Value).ToArray();
foreach (var val in values)
if (o.DummyDllFolder != null)
{
if (string.IsNullOrWhiteSpace(val))
{
result.ErrorMessage = "Empty string.";
return;
}
assemblyLoader.Load(o.DummyDllFolder.FullName);
}
try
Logger.Info("Scanning for files");
var files = o.Input.Attributes.HasFlag(FileAttributes.Directory) ? Directory.GetFiles(o.Input.FullName, "*.*", SearchOption.AllDirectories).OrderBy(x => x.Length).ToArray() : new string[] { o.Input.FullName };
Logger.Info(string.Format("Found {0} file(s)", files.Length));
if (o.MapOp.HasFlag(MapOpType.Build))
{
AssetsHelper.BuildMap(files, o.MapName, o.Input.FullName, game);
}
if (o.MapOp.HasFlag(MapOpType.Load))
{
AssetsHelper.LoadMap(o.MapName);
assetsManager.ResolveDependencies = true;
}
if (o.MapOp.HasFlag(MapOpType.List))
{
if (files.Length == 1)
{
Regex.Match("", val, RegexOptions.IgnoreCase);
throw new Exception("Unable to build AssetMap with input_path as a file !!");
}
catch (ArgumentException e)
var assets = AssetsHelper.BuildAssetMap(files, game, o.NameFilter, o.ContainerFilter);
if (!o.Output.Exists)
{
result.ErrorMessage = "Invalid Regex.\n" + e.Message;
return;
o.Output.Create();
}
var resetEvent = new ManualResetEvent(false);
AssetsHelper.ExportAssetsMap(assets, o.MapName, o.Output.FullName, o.MapType, resetEvent);
resetEvent.WaitOne();
}
if (o.MapOp.Equals(MapOpType.None) || o.MapOp.HasFlag(MapOpType.Load))
{
var i = 0;
foreach (var file in files)
{
assetsManager.LoadFiles(file);
if (assetsManager.assetsFileList.Count > 0)
{
BuildAssetData(o.NameFilter, o.ContainerFilter, ref i);
ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType);
}
exportableAssets.Clear();
assetsManager.Clear();
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
protected override Options GetBoundValue(BindingContext bindingContext) =>
new Options
{
Silent = bindingContext.ParseResult.GetValueForOption(Silent),
TypeFilter = bindingContext.ParseResult.GetValueForOption(TypeFilter),
NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter),
ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter),
GameName = bindingContext.ParseResult.GetValueForOption(GameName),
MapOp = bindingContext.ParseResult.GetValueForOption(MapOp),
MapType = bindingContext.ParseResult.GetValueForOption(MapType),
MapName = bindingContext.ParseResult.GetValueForOption(MapName),
GroupAssetsType = bindingContext.ParseResult.GetValueForOption(GroupAssetsType),
SkipRenderer = bindingContext.ParseResult.GetValueForOption(IncludeAnimators),
SkipMiHoYoBinData = bindingContext.ParseResult.GetValueForOption(SkipMiHoYoBinData),
Key = bindingContext.ParseResult.GetValueForOption(Key),
AIFile = bindingContext.ParseResult.GetValueForOption(AIFile),
Input = bindingContext.ParseResult.GetValueForArgument(Input),
Output = bindingContext.ParseResult.GetValueForArgument(Output)
};
}
}

View File

@@ -24,6 +24,7 @@ namespace AssetStudioCLI.Properties {
}
catch (Exception)
{
Console.WriteLine($"Invalid value at \"{key}\", switching to default value [{defaultValue}] !!");
return defaultValue;
}

View File

@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using static AssetStudioCLI.Exporter;
using Object = AssetStudio.Object;
using System.Globalization;
using System.Xml;
namespace AssetStudioCLI
{
@@ -17,9 +18,11 @@ namespace AssetStudioCLI
public enum MapOpType
{
None,
AssetMap,
CABMap,
Both
Load,
Build,
Both,
List,
All = Build | Load | List
}
public enum AssetGroupOption
@@ -32,9 +35,12 @@ namespace AssetStudioCLI
internal static class Studio
{
public static AssetsManager assetsManager = new AssetsManager() { ResolveDependencies = false };
public static List<AssetItem> exportableAssets = new List<AssetItem>();
public static Game Game;
public static bool ModelOnly = false;
public static bool SkipContainer = false;
public static AssetsManager assetsManager = new AssetsManager() { ResolveDependencies = false };
public static AssemblyLoader assemblyLoader = new AssemblyLoader();
public static List<AssetItem> exportableAssets = new List<AssetItem>();
public static int ExtractFolder(string path, string savePath)
{
@@ -213,13 +219,17 @@ namespace AssetStudioCLI
if (int.TryParse(asset.Container, out var value))
{
var last = unchecked((uint)value);
var path = ResourceIndex.GetAssetPath(last);
if (!string.IsNullOrEmpty(path))
var name = Path.GetFileNameWithoutExtension(asset.SourceFile.originalPath);
if (uint.TryParse(name, out var id))
{
asset.Container = path;
if (asset.Type == ClassIDType.MiHoYoBinData)
var path = ResourceIndex.GetContainer(id, last);
if (!string.IsNullOrEmpty(path))
{
asset.Text = Path.GetFileNameWithoutExtension(path);
asset.Container = path;
if (asset.Type == ClassIDType.MiHoYoBinData)
{
asset.Text = Path.GetFileNameWithoutExtension(path);
}
}
}
}
@@ -228,121 +238,16 @@ namespace AssetStudioCLI
}
}
public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters, Regex[] containerFilters, ref int i)
public static void BuildAssetData(Regex[] nameFilters, Regex[] containerFilters, ref int i)
{
string productName = null;
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
var objectAssetItemDic = new Dictionary<Object, AssetItem>(objectCount);
var objectAssetItemDic = new Dictionary<Object, AssetItem>();
var mihoyoBinDataNames = new List<(PPtr<Object>, string)>();
var containers = new List<(PPtr<Object>, string)>();
foreach (var assetsFile in assetsManager.assetsFileList)
{
foreach (var asset in assetsFile.Objects)
{
var assetItem = new AssetItem(asset);
objectAssetItemDic.Add(asset, assetItem);
assetItem.UniqueID = "#" + i++;
assetItem.Text = "";
var exportable = false;
switch (asset)
{
case GameObject m_GameObject:
assetItem.Text = m_GameObject.m_Name;
break;
case Texture2D m_Texture2D:
if (!string.IsNullOrEmpty(m_Texture2D.m_StreamData?.path))
assetItem.FullSize = asset.byteSize + m_Texture2D.m_StreamData.size;
assetItem.Text = m_Texture2D.m_Name;
exportable = true;
break;
case AudioClip m_AudioClip:
if (!string.IsNullOrEmpty(m_AudioClip.m_Source))
assetItem.FullSize = asset.byteSize + m_AudioClip.m_Size;
assetItem.Text = m_AudioClip.m_Name;
break;
case VideoClip m_VideoClip:
if (!string.IsNullOrEmpty(m_VideoClip.m_OriginalPath))
assetItem.FullSize = asset.byteSize + (long)m_VideoClip.m_ExternalResources.m_Size;
assetItem.Text = m_VideoClip.m_Name;
break;
case Shader m_Shader:
assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name;
exportable = true;
break;
case Mesh _:
case TextAsset _:
case AnimationClip _:
case Font _:
case MovieTexture _:
case Sprite _:
case Material _:
assetItem.Text = ((NamedObject)asset).m_Name;
exportable = true;
break;
case Animator m_Animator:
if (m_Animator.m_GameObject.TryGet(out var gameObject))
{
assetItem.Text = gameObject.m_Name;
}
exportable = true;
break;
case MonoBehaviour m_MonoBehaviour:
if (m_MonoBehaviour.m_Name == "" && m_MonoBehaviour.m_Script.TryGet(out var m_Script))
{
assetItem.Text = m_Script.m_ClassName;
}
else
{
assetItem.Text = m_MonoBehaviour.m_Name;
}
break;
case PlayerSettings m_PlayerSettings:
productName = m_PlayerSettings.productName;
break;
case AssetBundle m_AssetBundle:
foreach (var m_Container in m_AssetBundle.m_Container)
{
var preloadIndex = m_Container.Value.preloadIndex;
var preloadSize = m_Container.Value.preloadSize;
var preloadEnd = preloadIndex + preloadSize;
for (int k = preloadIndex; k < preloadEnd; k++)
{
containers.Add((m_AssetBundle.m_PreloadTable[k], m_Container.Key));
}
}
assetItem.Text = m_AssetBundle.m_Name;
break;
case IndexObject m_IndexObject:
foreach (var index in m_IndexObject.AssetMap)
{
mihoyoBinDataNames.Add((index.Value.Object, index.Key));
}
assetItem.Text = "IndexObject";
break;
case MiHoYoBinData m_MiHoYoBinData:
exportable = MiHoYoBinData.Exportable;
break;
case ResourceManager m_ResourceManager:
foreach (var m_Container in m_ResourceManager.m_Container)
{
containers.Add((m_Container.Value, m_Container.Key));
}
break;
case NamedObject m_NamedObject:
assetItem.Text = m_NamedObject.m_Name;
exportable = true;
break;
}
if (assetItem.Text == "")
{
assetItem.Text = assetItem.TypeString + assetItem.UniqueID;
}
var isMatchRegex = nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(assetItem.Text));
var isFilteredType = typeFilters.Length == 0 || typeFilters.Contains(assetItem.Asset.type);
if (isMatchRegex && isFilteredType && exportable)
{
exportableAssets.Add(assetItem);
}
ProcessAssetData(asset, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i);
}
}
foreach ((var pptr, var name) in mihoyoBinDataNames)
@@ -358,26 +263,138 @@ namespace AssetStudioCLI
else assetItem.Text = $"BinFile #{assetItem.m_PathID}";
}
}
foreach ((var pptr, var container) in containers)
if (!SkipContainer)
{
if (pptr.TryGet(out var obj))
foreach ((var pptr, var container) in containers)
{
var item = objectAssetItemDic[obj];
if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container)))
if (pptr.TryGet(out var obj))
{
item.Container = container;
if (!objectAssetItemDic.TryGetValue(obj, out var item))
{
ProcessAssetData(obj, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i);
item = objectAssetItemDic[obj];
}
if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container)))
{
item.Container = container;
}
else
{
exportableAssets.Remove(item);
}
}
}
containers.Clear();
if (Game.Type.IsGISubGroup())
{
UpdateContainers();
}
}
}
public static void ProcessAssetData(Object asset, Regex[] nameFilters, Dictionary<Object, AssetItem> objectAssetItemDic, List<(PPtr<Object>, string)> mihoyoBinDataNames, List<(PPtr<Object>, string)> containers, ref int i)
{
var assetItem = new AssetItem(asset);
objectAssetItemDic.Add(asset, assetItem);
assetItem.UniqueID = "#" + i++;
var exportable = false;
switch (asset)
{
case GameObject m_GameObject:
assetItem.Text = m_GameObject.m_Name;
exportable = ModelOnly && m_GameObject.HasModel();
break;
case Texture2D m_Texture2D:
if (!string.IsNullOrEmpty(m_Texture2D.m_StreamData?.path))
assetItem.FullSize = asset.byteSize + m_Texture2D.m_StreamData.size;
assetItem.Text = m_Texture2D.m_Name;
exportable = !ModelOnly;
break;
case AudioClip m_AudioClip:
if (!string.IsNullOrEmpty(m_AudioClip.m_Source))
assetItem.FullSize = asset.byteSize + m_AudioClip.m_Size;
assetItem.Text = m_AudioClip.m_Name;
exportable = !ModelOnly;
break;
case VideoClip m_VideoClip:
if (!string.IsNullOrEmpty(m_VideoClip.m_OriginalPath))
assetItem.FullSize = asset.byteSize + (long)m_VideoClip.m_ExternalResources.m_Size;
assetItem.Text = m_VideoClip.m_Name;
exportable = !ModelOnly;
break;
case Shader m_Shader:
assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name;
exportable = !ModelOnly;
break;
case Mesh _:
case TextAsset _:
case AnimationClip _:
case Font _:
case Sprite _:
case Material _:
assetItem.Text = ((NamedObject)asset).m_Name;
exportable = !ModelOnly;
break;
case Animator m_Animator:
if (m_Animator.m_GameObject.TryGet(out var gameObject))
{
assetItem.Text = gameObject.m_Name;
}
exportable = !ModelOnly;
break;
case MonoBehaviour m_MonoBehaviour:
if (m_MonoBehaviour.m_Name == "" && m_MonoBehaviour.m_Script.TryGet(out var m_Script))
{
assetItem.Text = m_Script.m_ClassName;
}
else
{
exportableAssets.Remove(item);
assetItem.Text = m_MonoBehaviour.m_Name;
}
}
exportable = !ModelOnly;
break;
case AssetBundle m_AssetBundle:
foreach (var m_Container in m_AssetBundle.m_Container)
{
var preloadIndex = m_Container.Value.preloadIndex;
var preloadSize = m_Container.Value.preloadSize;
var preloadEnd = preloadIndex + preloadSize;
for (int k = preloadIndex; k < preloadEnd; k++)
{
containers.Add((m_AssetBundle.m_PreloadTable[k], m_Container.Key));
}
}
assetItem.Text = m_AssetBundle.m_Name;
break;
case IndexObject m_IndexObject:
foreach (var index in m_IndexObject.AssetMap)
{
mihoyoBinDataNames.Add((index.Value.Object, index.Key));
}
assetItem.Text = "IndexObject";
break;
case MiHoYoBinData m_MiHoYoBinData:
exportable = !ModelOnly;
break;
case ResourceManager m_ResourceManager:
foreach (var m_Container in m_ResourceManager.m_Container)
{
containers.Add((m_Container.Value, m_Container.Key));
}
break;
case NamedObject m_NamedObject:
assetItem.Text = m_NamedObject.m_Name;
break;
}
if (Game.Type.IsGISubGroup())
if (assetItem.Text == "")
{
UpdateContainers();
assetItem.Text = assetItem.TypeString + assetItem.UniqueID;
}
var isMatchRegex = nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(assetItem.Text));
if (isMatchRegex && exportable)
{
exportableAssets.Add(assetItem);
}
containers.Clear();
}
public static void ExportAssets(string savePath, List<AssetItem> toExportAssets, AssetGroupOption assetGroupOption)
@@ -448,28 +465,35 @@ namespace AssetStudioCLI
{
case ExportListType.XML:
filename = Path.Combine(savePath, $"{exportListName}.xml");
var doc = new XDocument(
new XElement("Assets",
new XAttribute("filename", filename),
new XAttribute("createdAt", DateTime.UtcNow.ToString("s")),
toExportAssets.Select(
asset => new XElement("Asset",
new XElement("Name", asset.Name),
new XElement("Container", asset.Container),
new XElement("Type", new XAttribute("id", (int)asset.Type), asset.Type.ToString()),
new XElement("PathID", asset.PathID),
new XElement("Source", asset.Source)
)
)
)
);
doc.Save(filename);
var settings = new XmlWriterSettings() { Indent = true };
using (XmlWriter writer = XmlWriter.Create(filename, settings))
{
writer.WriteStartDocument();
writer.WriteStartElement("Assets");
writer.WriteAttributeString("filename", filename);
writer.WriteAttributeString("createdAt", DateTime.UtcNow.ToString("s"));
foreach (var asset in toExportAssets)
{
writer.WriteStartElement("Asset");
writer.WriteElementString("Name", asset.Name);
writer.WriteElementString("Container", asset.Container);
writer.WriteStartElement("Type");
writer.WriteAttributeString("id", ((int)asset.Type).ToString());
writer.WriteValue(asset.Type.ToString());
writer.WriteEndElement();
writer.WriteElementString("PathID", asset.PathID.ToString());
writer.WriteElementString("Source", asset.Source);
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndDocument();
}
break;
case ExportListType.JSON:
filename = Path.Combine(savePath, $"{exportListName}.json");
using (StreamWriter file = File.CreateText(filename))
{
JsonSerializer serializer = new JsonSerializer() { Formatting = Formatting.Indented };
JsonSerializer serializer = new JsonSerializer() { Formatting = Newtonsoft.Json.Formatting.Indented };
serializer.Converters.Add(new StringEnumConverter());
serializer.Serialize(file, toExportAssets);
}
@@ -482,5 +506,10 @@ namespace AssetStudioCLI
Logger.Info($"AssetMap build successfully !!");
}
public static TypeTree MonoBehaviourToTypeTree(MonoBehaviour m_MonoBehaviour)
{
return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader);
}
}
}