- [Core] Project restructure
This commit is contained in:
29
AssetStudio.CLI/App.config
Normal file
29
AssetStudio.CLI/App.config
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<add key="exportAll" value="False" />
|
||||
<add key="openAfterExport" value="True" />
|
||||
<add key="convertTexture" value="True" />
|
||||
<add key="convertAudio" value="True" />
|
||||
<add key="convertType" value="Png" />
|
||||
<add key="eulerFilter" value="True" />
|
||||
<add key="filterPrecision" value="0.25" />
|
||||
<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" />
|
||||
<add key="scaleFactor" value="1" />
|
||||
<add key="exportBlendShape" value="True" />
|
||||
<add key="castToBone" value="False" />
|
||||
<add key="restoreExtensionName" value="True" />
|
||||
<add key="exportAllUvsAsDiffuseMaps" value="False" />
|
||||
<add key="disableShader" value="False" />
|
||||
<add key="disableRenderer" value="False" />
|
||||
<add key="disableAnimationClip" value="False" />
|
||||
<add key="enableFileLogging" value="False" />
|
||||
<add key="minimalAssetMap" value="True" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
41
AssetStudio.CLI/AssetStudio.CLI.csproj
Normal file
41
AssetStudio.CLI/AssetStudio.CLI.csproj
Normal file
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net6.0-windows;net7.0-windows</TargetFrameworks>
|
||||
<ApplicationIcon>Resources\as.ico</ApplicationIcon>
|
||||
<Version>1.00.00</Version>
|
||||
<AssemblyVersion>1.00.00</AssemblyVersion>
|
||||
<FileVersion>1.00.00</FileVersion>
|
||||
<Copyright>Copyright © Razmoth 2022; Copyright © Perfare 2018-2022</Copyright>
|
||||
<BaseOutputPath>..\AssetStudio.GUI\bin</BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0-preview.5.23280.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AssetStudio.Utility\AssetStudio.Utility.csproj" />
|
||||
<ProjectReference Include="..\AssetStudio\AssetStudio.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Settings.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyExtraFiles" AfterTargets="AfterBuild">
|
||||
<Copy SourceFiles="$(SolutionDir)AssetStudio.FBXNative\bin\Win32\$(Configuration)\AssetStudio.FBXNative.dll" DestinationFolder="$(TargetDir)x86" ContinueOnError="true" />
|
||||
<Copy SourceFiles="$(SolutionDir)AssetStudio.FBXNative\bin\x64\$(Configuration)\AssetStudio.FBXNative.dll" DestinationFolder="$(TargetDir)x64" ContinueOnError="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishExtraFiles" AfterTargets="Publish">
|
||||
<Copy SourceFiles="$(TargetDir)x86\AssetStudio.FBXNative.dll" DestinationFolder="$(PublishDir)x86" ContinueOnError="true" />
|
||||
<Copy SourceFiles="$(TargetDir)x64\AssetStudio.FBXNative.dll" DestinationFolder="$(PublishDir)x64" ContinueOnError="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
27
AssetStudio.CLI/Components/AssetItem.cs
Normal file
27
AssetStudio.CLI/Components/AssetItem.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace AssetStudio.CLI
|
||||
{
|
||||
public class AssetItem
|
||||
{
|
||||
public string Text;
|
||||
public Object Asset;
|
||||
public SerializedFile SourceFile;
|
||||
public string Container = string.Empty;
|
||||
public string TypeString;
|
||||
public long m_PathID;
|
||||
public long FullSize;
|
||||
public ClassIDType Type;
|
||||
public string InfoText;
|
||||
public string UniqueID;
|
||||
|
||||
public AssetItem(Object asset)
|
||||
{
|
||||
Asset = asset;
|
||||
Text = asset.Name;
|
||||
SourceFile = asset.assetsFile;
|
||||
Type = asset.type;
|
||||
TypeString = Type.ToString();
|
||||
m_PathID = asset.m_PathID;
|
||||
FullSize = asset.byteSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
204
AssetStudio.CLI/Components/CommandLine.cs
Normal file
204
AssetStudio.CLI/Components/CommandLine.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Binding;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AssetStudio.CLI
|
||||
{
|
||||
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.Verbose,
|
||||
optionsBinder.TypeFilter,
|
||||
optionsBinder.NameFilter,
|
||||
optionsBinder.ContainerFilter,
|
||||
optionsBinder.GameName,
|
||||
optionsBinder.KeyIndex,
|
||||
optionsBinder.MapOp,
|
||||
optionsBinder.MapType,
|
||||
optionsBinder.MapName,
|
||||
optionsBinder.UnityVersion,
|
||||
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 bool Verbose { get; set; }
|
||||
public ClassIDType[] TypeFilter { get; set; }
|
||||
public Regex[] NameFilter { get; set; }
|
||||
public Regex[] ContainerFilter { get; set; }
|
||||
public string GameName { get; set; }
|
||||
public int KeyIndex { get; set; }
|
||||
public MapOpType MapOp { get; set; }
|
||||
public ExportListType MapType { get; set; }
|
||||
public string MapName { get; set; }
|
||||
public string UnityVersion { 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<bool> Verbose;
|
||||
public readonly Option<ClassIDType[]> TypeFilter;
|
||||
public readonly Option<Regex[]> NameFilter;
|
||||
public readonly Option<Regex[]> ContainerFilter;
|
||||
public readonly Option<string> GameName;
|
||||
public readonly Option<int> KeyIndex;
|
||||
public readonly Option<MapOpType> MapOp;
|
||||
public readonly Option<ExportListType> MapType;
|
||||
public readonly Option<string> MapName;
|
||||
public readonly Option<string> UnityVersion;
|
||||
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.");
|
||||
Verbose = new Option<bool>("--verbose", "Enable verbose logging.");
|
||||
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 };
|
||||
KeyIndex = new Option<int>("--key_index", "Specify key index.") { ArgumentHelpName = UnityCNManager.ToString() };
|
||||
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.");
|
||||
UnityVersion = new Option<string>("--unity_version", "Specify Unity version.");
|
||||
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.None);
|
||||
MapType.SetDefaultValue(ExportListType.XML);
|
||||
KeyIndex.SetDefaultValue(0);
|
||||
}
|
||||
|
||||
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),
|
||||
Verbose = bindingContext.ParseResult.GetValueForOption(Verbose),
|
||||
TypeFilter = bindingContext.ParseResult.GetValueForOption(TypeFilter),
|
||||
NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter),
|
||||
ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter),
|
||||
GameName = bindingContext.ParseResult.GetValueForOption(GameName),
|
||||
KeyIndex = bindingContext.ParseResult.GetValueForOption(KeyIndex),
|
||||
MapOp = bindingContext.ParseResult.GetValueForOption(MapOp),
|
||||
MapType = bindingContext.ParseResult.GetValueForOption(MapType),
|
||||
MapName = bindingContext.ParseResult.GetValueForOption(MapName),
|
||||
UnityVersion = bindingContext.ParseResult.GetValueForOption(UnityVersion),
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
457
AssetStudio.CLI/Exporter.cs
Normal file
457
AssetStudio.CLI/Exporter.cs
Normal file
@@ -0,0 +1,457 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AssetStudio.CLI
|
||||
{
|
||||
internal static class Exporter
|
||||
{
|
||||
public static bool ExportTexture2D(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_Texture2D = (Texture2D)item.Asset;
|
||||
if (Properties.Settings.Default.convertTexture)
|
||||
{
|
||||
var type = Properties.Settings.Default.convertType;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
|
||||
return false;
|
||||
var image = m_Texture2D.ConvertToImage(true);
|
||||
if (image == null)
|
||||
return false;
|
||||
using (image)
|
||||
{
|
||||
using (var file = File.OpenWrite(exportFullPath))
|
||||
{
|
||||
image.WriteToStream(file, type);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".tex", out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, m_Texture2D.image_data.GetData());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ExportAudioClip(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_AudioClip = (AudioClip)item.Asset;
|
||||
var m_AudioData = m_AudioClip.m_AudioData.GetData();
|
||||
if (m_AudioData == null || m_AudioData.Length == 0)
|
||||
return false;
|
||||
var converter = new AudioClipConverter(m_AudioClip);
|
||||
if (Properties.Settings.Default.convertAudio && converter.IsSupport)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".wav", out var exportFullPath))
|
||||
return false;
|
||||
var buffer = converter.ConvertToWav();
|
||||
if (buffer == null)
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, converter.GetExtensionName(), out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, m_AudioData);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportShader(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".shader", out var exportFullPath))
|
||||
return false;
|
||||
var m_Shader = (Shader)item.Asset;
|
||||
var str = m_Shader.Convert();
|
||||
File.WriteAllText(exportFullPath, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportTextAsset(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_TextAsset = (TextAsset)(item.Asset);
|
||||
var extension = ".txt";
|
||||
if (Properties.Settings.Default.restoreExtensionName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Container))
|
||||
{
|
||||
extension = Path.GetExtension(item.Container);
|
||||
}
|
||||
}
|
||||
if (!TryExportFile(exportPath, item, extension, out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, m_TextAsset.m_Script);
|
||||
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;
|
||||
if (item.Asset is MiHoYoBinData m_MiHoYoBinData)
|
||||
{
|
||||
switch (m_MiHoYoBinData.Type)
|
||||
{
|
||||
case MiHoYoBinDataType.JSON:
|
||||
|
||||
if (!TryExportFile(exportPath, item, ".json", out exportFullPath))
|
||||
return false;
|
||||
var json = m_MiHoYoBinData.Dump() as string;
|
||||
if (json.Length != 0)
|
||||
{
|
||||
File.WriteAllText(exportFullPath, json);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MiHoYoBinDataType.Bytes:
|
||||
var extension = ".bin";
|
||||
if (Properties.Settings.Default.restoreExtensionName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Container))
|
||||
{
|
||||
extension = Path.GetExtension(item.Container);
|
||||
}
|
||||
}
|
||||
if (!TryExportFile(exportPath, item, extension, out exportFullPath))
|
||||
return false;
|
||||
var bytes = m_MiHoYoBinData.Dump() as byte[];
|
||||
if (!bytes.IsNullOrEmpty())
|
||||
{
|
||||
File.WriteAllBytes(exportFullPath, bytes);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportFont(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_Font = (Font)item.Asset;
|
||||
if (m_Font.m_FontData != null)
|
||||
{
|
||||
var extension = ".ttf";
|
||||
if (m_Font.m_FontData[0] == 79 && m_Font.m_FontData[1] == 84 && m_Font.m_FontData[2] == 84 && m_Font.m_FontData[3] == 79)
|
||||
{
|
||||
extension = ".otf";
|
||||
}
|
||||
if (!TryExportFile(exportPath, item, extension, out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, m_Font.m_FontData);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportMesh(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_Mesh = (Mesh)item.Asset;
|
||||
if (m_Mesh.m_VertexCount <= 0)
|
||||
return false;
|
||||
if (!TryExportFile(exportPath, item, ".obj", out var exportFullPath))
|
||||
return false;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("g " + m_Mesh.m_Name);
|
||||
#region Vertices
|
||||
if (m_Mesh.m_Vertices == null || m_Mesh.m_Vertices.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int c = 3;
|
||||
if (m_Mesh.m_Vertices.Length == m_Mesh.m_VertexCount * 4)
|
||||
{
|
||||
c = 4;
|
||||
}
|
||||
for (int v = 0; v < m_Mesh.m_VertexCount; v++)
|
||||
{
|
||||
sb.AppendFormat("v {0} {1} {2}\r\n", -m_Mesh.m_Vertices[v * c], m_Mesh.m_Vertices[v * c + 1], m_Mesh.m_Vertices[v * c + 2]);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UV
|
||||
if (m_Mesh.m_UV0?.Length > 0)
|
||||
{
|
||||
c = 4;
|
||||
if (m_Mesh.m_UV0.Length == m_Mesh.m_VertexCount * 2)
|
||||
{
|
||||
c = 2;
|
||||
}
|
||||
else if (m_Mesh.m_UV0.Length == m_Mesh.m_VertexCount * 3)
|
||||
{
|
||||
c = 3;
|
||||
}
|
||||
for (int v = 0; v < m_Mesh.m_VertexCount; v++)
|
||||
{
|
||||
sb.AppendFormat("vt {0} {1}\r\n", m_Mesh.m_UV0[v * c], m_Mesh.m_UV0[v * c + 1]);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Normals
|
||||
if (m_Mesh.m_Normals?.Length > 0)
|
||||
{
|
||||
if (m_Mesh.m_Normals.Length == m_Mesh.m_VertexCount * 3)
|
||||
{
|
||||
c = 3;
|
||||
}
|
||||
else if (m_Mesh.m_Normals.Length == m_Mesh.m_VertexCount * 4)
|
||||
{
|
||||
c = 4;
|
||||
}
|
||||
for (int v = 0; v < m_Mesh.m_VertexCount; v++)
|
||||
{
|
||||
sb.AppendFormat("vn {0} {1} {2}\r\n", -m_Mesh.m_Normals[v * c], m_Mesh.m_Normals[v * c + 1], m_Mesh.m_Normals[v * c + 2]);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Face
|
||||
int sum = 0;
|
||||
for (var i = 0; i < m_Mesh.m_SubMeshes.Length; i++)
|
||||
{
|
||||
sb.AppendLine($"g {m_Mesh.m_Name}_{i}");
|
||||
int indexCount = (int)m_Mesh.m_SubMeshes[i].indexCount;
|
||||
var end = sum + indexCount / 3;
|
||||
for (int f = sum; f < end; f++)
|
||||
{
|
||||
sb.AppendFormat("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\r\n", m_Mesh.m_Indices[f * 3 + 2] + 1, m_Mesh.m_Indices[f * 3 + 1] + 1, m_Mesh.m_Indices[f * 3] + 1);
|
||||
}
|
||||
sum = end;
|
||||
}
|
||||
#endregion
|
||||
|
||||
sb.Replace("NaN", "0");
|
||||
File.WriteAllText(exportFullPath, sb.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportVideoClip(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_VideoClip = (VideoClip)item.Asset;
|
||||
if (m_VideoClip.m_ExternalResources.m_Size > 0)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, Path.GetExtension(m_VideoClip.m_OriginalPath), out var exportFullPath))
|
||||
return false;
|
||||
m_VideoClip.m_VideoData.WriteData(exportFullPath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportMovieTexture(AssetItem item, string exportPath)
|
||||
{
|
||||
var m_MovieTexture = (MovieTexture)item.Asset;
|
||||
if (!TryExportFile(exportPath, item, ".ogv", out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, m_MovieTexture.m_MovieData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportSprite(AssetItem item, string exportPath)
|
||||
{
|
||||
var type = Properties.Settings.Default.convertType;
|
||||
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
|
||||
return false;
|
||||
var image = ((Sprite)item.Asset).GetImage();
|
||||
if (image != null)
|
||||
{
|
||||
using (image)
|
||||
{
|
||||
using (var file = File.OpenWrite(exportFullPath))
|
||||
{
|
||||
image.WriteToStream(file, type);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportRawFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath))
|
||||
return false;
|
||||
File.WriteAllBytes(exportFullPath, item.Asset.GetRawData());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryExportFile(string dir, AssetItem item, string extension, out string fullPath)
|
||||
{
|
||||
var fileName = FixFileName(item.Text);
|
||||
fullPath = Path.Combine(dir, fileName + extension);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
return true;
|
||||
}
|
||||
fullPath = Path.Combine(dir, fileName + item.UniqueID + extension);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportAnimationClip(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".anim", out var exportFullPath))
|
||||
return false;
|
||||
var m_AnimationClip = (AnimationClip)item.Asset;
|
||||
var str = m_AnimationClip.Convert();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return false;
|
||||
File.WriteAllText(exportFullPath, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportAnimator(AssetItem item, string exportPath, List<AssetItem> animationList = null)
|
||||
{
|
||||
var exportFullPath = Path.Combine(exportPath, item.Text, item.Text + ".fbx");
|
||||
if (File.Exists(exportFullPath))
|
||||
{
|
||||
exportFullPath = Path.Combine(exportPath, item.Text + item.UniqueID, item.Text + ".fbx");
|
||||
}
|
||||
var m_Animator = (Animator)item.Asset;
|
||||
var convert = animationList != null
|
||||
? new ModelConverter(m_Animator, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations, animationList.Select(x => (AnimationClip)x.Asset).ToArray())
|
||||
: new ModelConverter(m_Animator, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations);
|
||||
ExportFbx(convert, exportFullPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ExportGameObject(AssetItem item, string exportPath, List <AssetItem> animationList = null)
|
||||
{
|
||||
var m_GameObject = (GameObject)item.Asset;
|
||||
exportPath = Path.Combine(exportPath, m_GameObject.m_Name) + Path.DirectorySeparatorChar;
|
||||
return ExportGameObject(m_GameObject, exportPath, animationList);
|
||||
}
|
||||
|
||||
public static bool ExportGameObject(GameObject gameObject, string exportPath, List<AssetItem> animationList = null)
|
||||
{
|
||||
var convert = animationList != null
|
||||
? new ModelConverter(gameObject, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations, animationList.Select(x => (AnimationClip)x.Asset).ToArray())
|
||||
: new ModelConverter(gameObject, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations);
|
||||
|
||||
if (convert.MeshList.Count == 0)
|
||||
{
|
||||
Logger.Info($"GameObject {gameObject.m_Name} has no mesh, skipping...");
|
||||
return false;
|
||||
}
|
||||
exportPath = exportPath + FixFileName(gameObject.m_Name) + ".fbx";
|
||||
ExportFbx(convert, exportPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ExportFbx(IImported convert, string exportPath)
|
||||
{
|
||||
var eulerFilter = Properties.Settings.Default.eulerFilter;
|
||||
var filterPrecision = (float)Properties.Settings.Default.filterPrecision;
|
||||
var exportAllNodes = Properties.Settings.Default.exportAllNodes;
|
||||
var exportSkins = Properties.Settings.Default.exportSkins;
|
||||
var exportAnimations = Properties.Settings.Default.exportAnimations;
|
||||
var exportBlendShape = Properties.Settings.Default.exportBlendShape;
|
||||
var castToBone = Properties.Settings.Default.castToBone;
|
||||
var boneSize = (int)Properties.Settings.Default.boneSize;
|
||||
var exportAllUvsAsDiffuseMaps = Properties.Settings.Default.exportAllUvsAsDiffuseMaps;
|
||||
var exportUV0UV1 = Properties.Settings.Default.exportUV0UV1;
|
||||
var scaleFactor = (float)Properties.Settings.Default.scaleFactor;
|
||||
var fbxVersion = Properties.Settings.Default.fbxVersion;
|
||||
var fbxFormat = Properties.Settings.Default.fbxFormat;
|
||||
ModelExporter.ExportFbx(exportPath, convert, eulerFilter, filterPrecision,
|
||||
exportAllNodes, exportSkins, exportAnimations, exportBlendShape, castToBone, boneSize, exportAllUvsAsDiffuseMaps, exportUV0UV1, scaleFactor, fbxVersion, fbxFormat == 1);
|
||||
}
|
||||
|
||||
public static bool ExportDumpFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath))
|
||||
return false;
|
||||
var str = item.Asset.Dump();
|
||||
if (str != null)
|
||||
{
|
||||
File.WriteAllText(exportFullPath, str);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ExportConvertFile(AssetItem item, string exportPath)
|
||||
{
|
||||
switch (item.Type)
|
||||
{
|
||||
case ClassIDType.GameObject:
|
||||
return ExportGameObject(item, exportPath);
|
||||
case ClassIDType.Texture2D:
|
||||
return ExportTexture2D(item, exportPath);
|
||||
case ClassIDType.AudioClip:
|
||||
return ExportAudioClip(item, exportPath);
|
||||
case ClassIDType.Shader:
|
||||
return ExportShader(item, exportPath);
|
||||
case ClassIDType.TextAsset:
|
||||
return ExportTextAsset(item, exportPath);
|
||||
case ClassIDType.MonoBehaviour:
|
||||
return ExportMonoBehaviour(item, exportPath);
|
||||
case ClassIDType.Font:
|
||||
return ExportFont(item, exportPath);
|
||||
case ClassIDType.Mesh:
|
||||
return ExportMesh(item, exportPath);
|
||||
case ClassIDType.VideoClip:
|
||||
return ExportVideoClip(item, exportPath);
|
||||
case ClassIDType.MovieTexture:
|
||||
return ExportMovieTexture(item, exportPath);
|
||||
case ClassIDType.Sprite:
|
||||
return ExportSprite(item, exportPath);
|
||||
case ClassIDType.Animator:
|
||||
return ExportAnimator(item, exportPath);
|
||||
case ClassIDType.AnimationClip:
|
||||
return ExportAnimationClip(item, exportPath);
|
||||
case ClassIDType.MiHoYoBinData:
|
||||
return ExportMiHoYoBinData(item, exportPath);
|
||||
case ClassIDType.Material:
|
||||
return ExportJSONFile(item, exportPath);
|
||||
default:
|
||||
return ExportRawFile(item, exportPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ExportJSONFile(AssetItem item, string exportPath)
|
||||
{
|
||||
if (!TryExportFile(exportPath, item, ".json", out var exportFullPath))
|
||||
return false;
|
||||
|
||||
var settings = new JsonSerializerSettings();
|
||||
settings.Converters.Add(new StringEnumConverter());
|
||||
var str = JsonConvert.SerializeObject(item.Asset, Formatting.Indented, settings);
|
||||
File.WriteAllText(exportFullPath, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string FixFileName(string str)
|
||||
{
|
||||
if (str.Length >= 260) return Path.GetRandomFileName();
|
||||
return Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_'));
|
||||
}
|
||||
}
|
||||
}
|
||||
130
AssetStudio.CLI/Program.cs
Normal file
130
AssetStudio.CLI/Program.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using AssetStudio.CLI.Properties;
|
||||
using static AssetStudio.CLI.Studio;
|
||||
|
||||
namespace AssetStudio.CLI
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args) => CommandLine.Init(args);
|
||||
|
||||
public static void Run(Options o)
|
||||
{
|
||||
try
|
||||
{
|
||||
var game = GameManager.GetGame(o.GameName);
|
||||
|
||||
if (game == null)
|
||||
{
|
||||
Console.WriteLine("Invalid Game !!");
|
||||
Console.WriteLine(GameManager.SupportedGames());
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.Type.IsUnityCN())
|
||||
{
|
||||
if (!UnityCNManager.TryGetEntry(o.KeyIndex, out var unityCN))
|
||||
{
|
||||
Console.WriteLine("Invalid key index !!");
|
||||
Console.WriteLine($"Available Options: \n{UnityCNManager.ToString()}");
|
||||
return;
|
||||
}
|
||||
|
||||
UnityCN.SetKey(unityCN);
|
||||
Logger.Info($"[UnityCN] Selected Key is {unityCN}");
|
||||
}
|
||||
|
||||
Studio.Game = game;
|
||||
Logger.LogVerbose = o.Verbose;
|
||||
Logger.Default = new ConsoleLogger();
|
||||
Logger.FileLogging = Settings.Default.enableFileLogging;
|
||||
AssetsHelper.Minimal = Settings.Default.minimalAssetMap;
|
||||
Shader.Parsable = !Settings.Default.disableShader;
|
||||
Renderer.Parsable = !Settings.Default.disableRenderer;
|
||||
AnimationClip.Parsable = !Settings.Default.disableAnimationClip;
|
||||
AssetsHelper.SetUnityVersion(o.UnityVersion);
|
||||
|
||||
assetsManager.Silent = o.Silent;
|
||||
assetsManager.Game = game;
|
||||
assetsManager.SpecifyUnityVersion = o.UnityVersion;
|
||||
ModelOnly = o.Model;
|
||||
o.Output.Create();
|
||||
|
||||
if (o.Key != default)
|
||||
{
|
||||
MiHoYoBinData.Encrypted = true;
|
||||
MiHoYoBinData.Key = o.Key;
|
||||
}
|
||||
|
||||
if (o.AIFile != null && game.Type.IsGISubGroup())
|
||||
{
|
||||
ResourceIndex.FromFile(o.AIFile.FullName);
|
||||
}
|
||||
|
||||
if (o.DummyDllFolder != null)
|
||||
{
|
||||
assemblyLoader.Load(o.DummyDllFolder.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($"Found {files.Length} files");
|
||||
|
||||
if (o.MapOp.HasFlag(MapOpType.CABMap))
|
||||
{
|
||||
AssetsHelper.BuildCABMap(files, o.MapName, o.Input.FullName, game);
|
||||
}
|
||||
if (o.MapOp.HasFlag(MapOpType.Load))
|
||||
{
|
||||
AssetsHelper.LoadCABMap(o.MapName);
|
||||
assetsManager.ResolveDependencies = true;
|
||||
}
|
||||
if (o.MapOp.HasFlag(MapOpType.AssetMap))
|
||||
{
|
||||
if (files.Length == 1)
|
||||
{
|
||||
throw new Exception("Unable to build AssetMap with input_path as a file !!");
|
||||
}
|
||||
var resetEvent = new ManualResetEvent(false);
|
||||
AssetsHelper.BuildAssetMap(files, o.MapName, game, o.Output.FullName, o.MapType, resetEvent, o.TypeFilter, o.NameFilter, o.ContainerFilter);
|
||||
resetEvent.WaitOne();
|
||||
}
|
||||
if (o.MapOp.HasFlag(MapOpType.Both))
|
||||
{
|
||||
var resetEvent = new ManualResetEvent(false);
|
||||
AssetsHelper.BuildBoth(files, o.MapName, o.Input.FullName, game, o.Output.FullName, o.MapType, resetEvent, o.TypeFilter, o.NameFilter, o.ContainerFilter);
|
||||
resetEvent.WaitOne();
|
||||
}
|
||||
if (o.MapOp.Equals(MapOpType.None) || o.MapOp.HasFlag(MapOpType.Load))
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
var path = Path.GetDirectoryName(Path.GetFullPath(files[0]));
|
||||
ImportHelper.MergeSplitAssets(path);
|
||||
var toReadFile = ImportHelper.ProcessingSplitFiles(files.ToList());
|
||||
|
||||
var fileList = new List<string>(toReadFile);
|
||||
foreach (var file in fileList)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
AssetStudio.CLI/Resources/as.ico
Normal file
BIN
AssetStudio.CLI/Resources/as.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
76
AssetStudio.CLI/Settings.cs
Normal file
76
AssetStudio.CLI/Settings.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Configuration;
|
||||
|
||||
namespace AssetStudio.CLI.Properties {
|
||||
public static class AppSettings
|
||||
{
|
||||
public static string Get(string key)
|
||||
{
|
||||
return ConfigurationManager.AppSettings[key];
|
||||
}
|
||||
|
||||
public static TValue Get<TValue>(string key, TValue defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = Get(key);
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return defaultValue;
|
||||
|
||||
return (TValue)TypeDescriptor.GetConverter(typeof(TValue)).ConvertFromInvariantString(value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Invalid value at \"{key}\", switching to default value [{defaultValue}] !!");
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Settings
|
||||
{
|
||||
private static Settings defaultInstance = new Settings();
|
||||
|
||||
public static Settings Default => defaultInstance;
|
||||
|
||||
public bool displayAll => AppSettings.Get("displayAll", false);
|
||||
public bool enablePreview => AppSettings.Get("enablePreview", true);
|
||||
public bool displayInfo => AppSettings.Get("displayInfo", true);
|
||||
public bool openAfterExport => AppSettings.Get("openAfterExport", true);
|
||||
public int assetGroupOption => AppSettings.Get("assetGroupOption", 0);
|
||||
public bool convertTexture => AppSettings.Get("convertTexture", true);
|
||||
public bool convertAudio => AppSettings.Get("convertAudio", true);
|
||||
public ImageFormat convertType => AppSettings.Get("convertType", ImageFormat.Png);
|
||||
public bool eulerFilter => AppSettings.Get("eulerFilter", true);
|
||||
public decimal filterPrecision => AppSettings.Get("filterPrecision", (decimal)0.25);
|
||||
public bool exportAllNodes => AppSettings.Get("exportAllNodes", true);
|
||||
public bool exportSkins => AppSettings.Get("exportSkins", true);
|
||||
public bool collectAnimations => AppSettings.Get("collectAnimations", true);
|
||||
public bool exportAnimations => AppSettings.Get("exportAnimations", true);
|
||||
public decimal boneSize => AppSettings.Get("boneSize", (decimal)10);
|
||||
public int fbxVersion => AppSettings.Get("fbxVersion", 3);
|
||||
public int fbxFormat => AppSettings.Get("fbxFormat", 0);
|
||||
public decimal scaleFactor => AppSettings.Get("scaleFactor", (decimal)1);
|
||||
public bool exportBlendShape => AppSettings.Get("exportBlendShape", true);
|
||||
public bool castToBone => AppSettings.Get("castToBone", false);
|
||||
public bool restoreExtensionName => AppSettings.Get("restoreExtensionName", true);
|
||||
public bool exportAllUvsAsDiffuseMaps => AppSettings.Get("exportAllUvsAsDiffuseMaps", false);
|
||||
public bool exportUV0UV1 => AppSettings.Get("exportUV0UV1", false);
|
||||
public bool encrypted => AppSettings.Get("encrypted", true);
|
||||
public byte key => AppSettings.Get("key", (byte)0x93);
|
||||
public int selectedGame => AppSettings.Get("selectedGame", 0);
|
||||
public bool enableResolveDependencies => AppSettings.Get("enableResolveDependencies", true);
|
||||
public int selectedCNUnityKey => AppSettings.Get("selectedCNUnityKey", 0);
|
||||
public int selectedAssetMapType => AppSettings.Get("selectedAssetMapType", 0);
|
||||
public bool exportMiHoYoBinData => AppSettings.Get("exportMiHoYoBinData", true);
|
||||
public bool disableShader => AppSettings.Get("disableShader", false);
|
||||
public bool disableRenderer => AppSettings.Get("disableRenderer", false);
|
||||
public bool disableAnimationClip => AppSettings.Get("disableAnimationClip", false);
|
||||
public bool enableFileLogging => AppSettings.Get("enableFileLogging", false);
|
||||
public bool minimalAssetMap => AppSettings.Get("minimalAssetMap", true);
|
||||
|
||||
}
|
||||
}
|
||||
482
AssetStudio.CLI/Studio.cs
Normal file
482
AssetStudio.CLI/Studio.cs
Normal file
@@ -0,0 +1,482 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using static AssetStudio.CLI.Exporter;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
|
||||
namespace AssetStudio.CLI
|
||||
{
|
||||
[Flags]
|
||||
public enum MapOpType
|
||||
{
|
||||
None,
|
||||
Load,
|
||||
CABMap,
|
||||
AssetMap = 4,
|
||||
Both = 8,
|
||||
All = Both | Load,
|
||||
}
|
||||
|
||||
public enum AssetGroupOption
|
||||
{
|
||||
ByType,
|
||||
ByContainer,
|
||||
BySource,
|
||||
None,
|
||||
}
|
||||
|
||||
internal static class Studio
|
||||
{
|
||||
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)
|
||||
{
|
||||
int extractedCount = 0;
|
||||
var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
var file = files[i];
|
||||
var fileOriPath = Path.GetDirectoryName(file);
|
||||
var fileSavePath = fileOriPath.Replace(path, savePath);
|
||||
extractedCount += ExtractFile(file, fileSavePath);
|
||||
}
|
||||
return extractedCount;
|
||||
}
|
||||
|
||||
public static int ExtractFile(string[] fileNames, string savePath)
|
||||
{
|
||||
int extractedCount = 0;
|
||||
for (var i = 0; i < fileNames.Length; i++)
|
||||
{
|
||||
var fileName = fileNames[i];
|
||||
extractedCount += ExtractFile(fileName, savePath);
|
||||
}
|
||||
return extractedCount;
|
||||
}
|
||||
|
||||
public static int ExtractFile(string fileName, string savePath)
|
||||
{
|
||||
int extractedCount = 0;
|
||||
var reader = new FileReader(fileName);
|
||||
reader = reader.PreProcessing(Game);
|
||||
if (reader.FileType == FileType.BundleFile)
|
||||
extractedCount += ExtractBundleFile(reader, savePath);
|
||||
else if (reader.FileType == FileType.WebFile)
|
||||
extractedCount += ExtractWebDataFile(reader, savePath);
|
||||
else if (reader.FileType == FileType.BlkFile)
|
||||
extractedCount += ExtractBlkFile(reader, savePath);
|
||||
else if (reader.FileType == FileType.BlockFile)
|
||||
extractedCount += ExtractBlockFile(reader, savePath);
|
||||
else
|
||||
reader.Dispose();
|
||||
return extractedCount;
|
||||
}
|
||||
|
||||
private static int ExtractBundleFile(FileReader reader, string savePath)
|
||||
{
|
||||
Logger.Info($"Decompressing {reader.FileName} ...");
|
||||
try
|
||||
{
|
||||
var bundleFile = new BundleFile(reader, Game);
|
||||
reader.Dispose();
|
||||
if (bundleFile.fileList.Length > 0)
|
||||
{
|
||||
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
|
||||
return ExtractStreamFile(extractPath, bundleFile.fileList);
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
Logger.Error($"Game type mismatch, Expected {nameof(Mr0k)} but got {Game.Name} ({Game.GetType().Name}) !!");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int ExtractWebDataFile(FileReader reader, string savePath)
|
||||
{
|
||||
Logger.Info($"Decompressing {reader.FileName} ...");
|
||||
var webFile = new WebFile(reader);
|
||||
reader.Dispose();
|
||||
if (webFile.fileList.Length > 0)
|
||||
{
|
||||
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
|
||||
return ExtractStreamFile(extractPath, webFile.fileList);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int ExtractBlkFile(FileReader reader, string savePath)
|
||||
{
|
||||
int total = 0;
|
||||
Logger.Info($"Decompressing {reader.FileName} ...");
|
||||
try
|
||||
{
|
||||
using var stream = BlkUtils.Decrypt(reader, (Blk)Game);
|
||||
do
|
||||
{
|
||||
stream.Offset = stream.AbsolutePosition;
|
||||
var dummyPath = Path.Combine(reader.FullPath, stream.AbsolutePosition.ToString("X8"));
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
var subSavePath = Path.Combine(savePath, reader.FileName + "_unpacked");
|
||||
switch (subReader.FileType)
|
||||
{
|
||||
case FileType.BundleFile:
|
||||
total += ExtractBundleFile(subReader, subSavePath);
|
||||
break;
|
||||
case FileType.Mhy0File:
|
||||
total += ExtractMhy0File(subReader, subSavePath);
|
||||
break;
|
||||
}
|
||||
} while (stream.Remaining > 0);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
Logger.Error($"Game type mismatch, Expected {nameof(Blk)} but got {Game.Name} ({Game.GetType().Name}) !!");
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static int ExtractBlockFile(FileReader reader, string savePath)
|
||||
{
|
||||
int total = 0;
|
||||
Logger.Info($"Decompressing {reader.FileName} ...");
|
||||
using var stream = new OffsetStream(reader.BaseStream, 0);
|
||||
do
|
||||
{
|
||||
stream.Offset = stream.AbsolutePosition;
|
||||
var subSavePath = Path.Combine(savePath, reader.FileName + "_unpacked");
|
||||
var dummyPath = Path.Combine(reader.FullPath, stream.AbsolutePosition.ToString("X8"));
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
total += ExtractBundleFile(subReader, subSavePath);
|
||||
} while (stream.Remaining > 0);
|
||||
return total;
|
||||
}
|
||||
|
||||
private static int ExtractMhy0File(FileReader reader, string savePath)
|
||||
{
|
||||
Logger.Info($"Decompressing {reader.FileName} ...");
|
||||
try
|
||||
{
|
||||
var mhy0File = new Mhy0File(reader, reader.FullPath, (Mhy0)Game);
|
||||
reader.Dispose();
|
||||
if (mhy0File.fileList.Length > 0)
|
||||
{
|
||||
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
|
||||
return ExtractStreamFile(extractPath, mhy0File.fileList);
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
Logger.Error($"Game type mismatch, Expected {nameof(Mhy0)} but got {Game.Name} ({Game.GetType().Name}) !!");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int ExtractStreamFile(string extractPath, StreamFile[] fileList)
|
||||
{
|
||||
int extractedCount = 0;
|
||||
foreach (var file in fileList)
|
||||
{
|
||||
var filePath = Path.Combine(extractPath, file.path);
|
||||
var fileDirectory = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(fileDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(fileDirectory);
|
||||
}
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
using (var fileStream = File.Create(filePath))
|
||||
{
|
||||
file.stream.CopyTo(fileStream);
|
||||
}
|
||||
extractedCount += 1;
|
||||
}
|
||||
file.stream.Dispose();
|
||||
}
|
||||
return extractedCount;
|
||||
}
|
||||
|
||||
public static void UpdateContainers()
|
||||
{
|
||||
if (exportableAssets.Count > 0)
|
||||
{
|
||||
Logger.Info("Updating Containers...");
|
||||
foreach (var asset in exportableAssets)
|
||||
{
|
||||
if (int.TryParse(asset.Container, out var value))
|
||||
{
|
||||
var last = unchecked((uint)value);
|
||||
var name = Path.GetFileNameWithoutExtension(asset.SourceFile.originalPath);
|
||||
if (uint.TryParse(name, out var id))
|
||||
{
|
||||
var path = ResourceIndex.GetContainer(id, last);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
asset.Container = path;
|
||||
if (asset.Type == ClassIDType.MiHoYoBinData)
|
||||
{
|
||||
asset.Text = Path.GetFileNameWithoutExtension(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.Info("Updated !!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters, Regex[] containerFilters, ref int i)
|
||||
{
|
||||
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)
|
||||
{
|
||||
ProcessAssetData(asset, typeFilters, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i);
|
||||
}
|
||||
}
|
||||
foreach ((var pptr, var name) in mihoyoBinDataNames)
|
||||
{
|
||||
if (pptr.TryGet<MiHoYoBinData>(out var obj))
|
||||
{
|
||||
var assetItem = objectAssetItemDic[obj];
|
||||
if (int.TryParse(name, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hash))
|
||||
{
|
||||
assetItem.Text = name;
|
||||
assetItem.Container = hash.ToString();
|
||||
}
|
||||
else assetItem.Text = $"BinFile #{assetItem.m_PathID}";
|
||||
}
|
||||
}
|
||||
if (!SkipContainer)
|
||||
{
|
||||
foreach ((var pptr, var container) in containers)
|
||||
{
|
||||
if (pptr.TryGet(out var obj))
|
||||
{
|
||||
var 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, ClassIDType[] typeFilters, 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:
|
||||
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;
|
||||
exportable = !ModelOnly;
|
||||
break;
|
||||
case AudioClip m_AudioClip:
|
||||
if (!string.IsNullOrEmpty(m_AudioClip.m_Source))
|
||||
assetItem.FullSize = asset.byteSize + m_AudioClip.m_Size;
|
||||
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;
|
||||
exportable = !ModelOnly;
|
||||
break;
|
||||
case MonoBehaviour m_MonoBehaviour:
|
||||
exportable = !ModelOnly && assemblyLoader.Loaded;
|
||||
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));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IndexObject m_IndexObject:
|
||||
foreach (var index in m_IndexObject.AssetMap)
|
||||
{
|
||||
mihoyoBinDataNames.Add((index.Value.Object, index.Key));
|
||||
}
|
||||
break;
|
||||
case ResourceManager m_ResourceManager:
|
||||
foreach (var m_Container in m_ResourceManager.m_Container)
|
||||
{
|
||||
containers.Add((m_Container.Value, m_Container.Key));
|
||||
}
|
||||
break;
|
||||
case Mesh _:
|
||||
case TextAsset _:
|
||||
case AnimationClip _ when AnimationClip.Parsable:
|
||||
case Font _:
|
||||
case MovieTexture _:
|
||||
case Sprite _:
|
||||
case Material _:
|
||||
case MiHoYoBinData _:
|
||||
case Shader _ when Shader.Parsable:
|
||||
case Animator _:
|
||||
exportable = !ModelOnly;
|
||||
break;
|
||||
}
|
||||
if (assetItem.Text == "")
|
||||
{
|
||||
assetItem.Text = assetItem.TypeString + assetItem.UniqueID;
|
||||
}
|
||||
|
||||
var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(assetItem.Text));
|
||||
var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(assetItem.Type);
|
||||
if (isMatchRegex && isFilteredType && exportable)
|
||||
{
|
||||
exportableAssets.Add(assetItem);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ExportAssets(string savePath, List<AssetItem> toExportAssets, AssetGroupOption assetGroupOption)
|
||||
{
|
||||
int toExportCount = toExportAssets.Count;
|
||||
int exportedCount = 0;
|
||||
foreach (var asset in toExportAssets)
|
||||
{
|
||||
string exportPath;
|
||||
switch (assetGroupOption)
|
||||
{
|
||||
case AssetGroupOption.ByType: //type name
|
||||
exportPath = Path.Combine(savePath, asset.TypeString);
|
||||
break;
|
||||
case AssetGroupOption.ByContainer: //container path
|
||||
if (!string.IsNullOrEmpty(asset.Container))
|
||||
{
|
||||
exportPath = Path.HasExtension(asset.Container) ? Path.Combine(savePath, Path.GetDirectoryName(asset.Container)) : Path.Combine(savePath, asset.Container);
|
||||
}
|
||||
else
|
||||
{
|
||||
exportPath = Path.Combine(savePath, asset.TypeString);
|
||||
}
|
||||
break;
|
||||
case AssetGroupOption.BySource: //source file
|
||||
if (string.IsNullOrEmpty(asset.SourceFile.originalPath))
|
||||
{
|
||||
exportPath = Path.Combine(savePath, asset.SourceFile.fileName + "_export");
|
||||
}
|
||||
else
|
||||
{
|
||||
exportPath = Path.Combine(savePath, Path.GetFileName(asset.SourceFile.originalPath) + "_export", asset.SourceFile.fileName);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
exportPath = savePath;
|
||||
break;
|
||||
}
|
||||
exportPath += Path.DirectorySeparatorChar;
|
||||
Logger.Info($"[{exportedCount}/{toExportCount}] Exporting {asset.TypeString}: {asset.Text}");
|
||||
try
|
||||
{
|
||||
if (ExportConvertFile(asset, exportPath))
|
||||
{
|
||||
exportedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Export {asset.Type}:{asset.Text} error\r\n{ex.Message}\r\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished exporting {exportedCount} assets.";
|
||||
|
||||
if (toExportCount > exportedCount)
|
||||
{
|
||||
statusText += $" {toExportCount - exportedCount} assets skipped (not extractable or files already exist)";
|
||||
}
|
||||
|
||||
Logger.Info(statusText);
|
||||
}
|
||||
|
||||
public static void ExportAssetsMap(string savePath, List<AssetEntry> toExportAssets, string exportListName, ExportListType exportListType)
|
||||
{
|
||||
string filename;
|
||||
switch (exportListType)
|
||||
{
|
||||
case ExportListType.XML:
|
||||
filename = Path.Combine(savePath, $"{exportListName}.xml");
|
||||
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 = Newtonsoft.Json.Formatting.Indented };
|
||||
serializer.Converters.Add(new StringEnumConverter());
|
||||
serializer.Serialize(file, toExportAssets);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var statusText = $"Finished exporting asset list with {toExportAssets.Count()} items.";
|
||||
|
||||
Logger.Info(statusText);
|
||||
|
||||
Logger.Info($"AssetMap build successfully !!");
|
||||
}
|
||||
|
||||
public static TypeTree MonoBehaviourToTypeTree(MonoBehaviour m_MonoBehaviour)
|
||||
{
|
||||
return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user