This commit is contained in:
Razmoth
2023-01-06 22:33:59 +04:00
parent a3cf868dfb
commit 2b31232b30
178 changed files with 5213 additions and 23780 deletions

35
AssetStudioCLI/App.config Normal file
View File

@@ -0,0 +1,35 @@
<?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="openAfterExport" value="True" />
<add key="assetGroupOption" value="0" />
<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="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="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

@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<ApplicationIcon>Resources\as.ico</ApplicationIcon>
<Version>0.18.60</Version>
<AssemblyVersion>0.18.60</AssemblyVersion>
<FileVersion>0.18.60</FileVersion>
<Copyright>Copyright © Razmoth 2022</Copyright>
<Version>0.80.30</Version>
<AssemblyVersion>0.80.30</AssemblyVersion>
<FileVersion>0.80.30</FileVersion>
<Copyright>Copyright © Razmoth 2022; Copyright © Perfare 2018-2022</Copyright>
<BaseOutputPath>..\AssetStudioGUI\bin</BaseOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
@@ -22,7 +22,10 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
<Compile Update="Options.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<Target Name="CopyExtraFiles" AfterTargets="AfterBuild">

View File

@@ -1,9 +1,10 @@
using System;
using AssetStudio;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AssetStudio;
using Newtonsoft.Json;
namespace AssetStudioCLI
{
@@ -12,18 +13,28 @@ namespace AssetStudioCLI
public static bool ExportTexture2D(AssetItem item, string exportPath)
{
var m_Texture2D = (Texture2D)item.Asset;
var type = ImageFormat.Png;
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)
if (Properties.Settings.Default.convertTexture)
{
using (var file = File.OpenWrite(exportFullPath))
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)
{
image.WriteToStream(file, type);
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;
}
}
@@ -35,7 +46,7 @@ namespace AssetStudioCLI
if (m_AudioData == null || m_AudioData.Length == 0)
return false;
var converter = new AudioClipConverter(m_AudioClip);
if (converter.IsSupport)
if (Properties.Settings.Default.convertAudio && converter.IsSupport)
{
if (!TryExportFile(exportPath, item, ".wav", out var exportFullPath))
return false;
@@ -67,9 +78,12 @@ namespace AssetStudioCLI
{
var m_TextAsset = (TextAsset)(item.Asset);
var extension = ".txt";
if (!string.IsNullOrEmpty(item.Container))
if (Properties.Settings.Default.restoreExtensionName)
{
extension = Path.GetExtension(item.Container);
if (!string.IsNullOrEmpty(item.Container))
{
extension = Path.GetExtension(item.Container);
}
}
if (!TryExportFile(exportPath, item, extension, out var exportFullPath))
return false;
@@ -85,6 +99,7 @@ namespace AssetStudioCLI
switch (m_MiHoYoBinData.Type)
{
case MiHoYoBinDataType.JSON:
if (!TryExportFile(exportPath, item, ".json", out exportFullPath))
return false;
var json = m_MiHoYoBinData.Dump() as string;
@@ -95,10 +110,18 @@ namespace AssetStudioCLI
}
break;
case MiHoYoBinDataType.Bytes:
if (!TryExportFile(exportPath, item, ".bin", out exportFullPath))
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.Length != 0)
if (!bytes.IsNullOrEmpty())
{
File.WriteAllBytes(exportFullPath, bytes);
return true;
@@ -233,7 +256,7 @@ namespace AssetStudioCLI
public static bool ExportSprite(AssetItem item, string exportPath)
{
var type = ImageFormat.Png;
var type = Properties.Settings.Default.convertType;
if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath))
return false;
var image = ((Sprite)item.Asset).GetImage();
@@ -251,22 +274,6 @@ namespace AssetStudioCLI
return false;
}
public static bool ExportJsonFile(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".json", out var exportFullPath))
return false;
var str = JsonConvert.SerializeObject(item.Asset, Formatting.Indented);
if (!string.IsNullOrEmpty(str) && str != "{}")
{
File.WriteAllText(exportFullPath, str);
return true;
}
else
{
return ExportRawFile(item, exportPath);
}
}
public static bool ExportRawFile(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath))
@@ -293,16 +300,57 @@ namespace AssetStudioCLI
return false;
}
public static bool ExportAnimationClip(AssetItem item, string exportPath)
public static bool ExportAnimator(AssetItem item, string exportPath, List<AssetItem> animationList = null)
{
if (!TryExportFile(exportPath, item, ".anim", out var exportFullPath))
return false;
var m_AnimationClip = (AnimationClip)item.Asset;
var str = m_AnimationClip.Convert(Studio.Game);
File.WriteAllText(exportFullPath, str);
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 void 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);
exportPath = exportPath + FixFileName(gameObject.m_Name) + ".fbx";
ExportFbx(convert, exportPath);
}
public static void ExportGameObjectMerge(List<GameObject> gameObject, string exportPath, List<AssetItem> animationList = null)
{
var rootName = Path.GetFileNameWithoutExtension(exportPath);
var convert = animationList != null
? new ModelConverter(rootName, gameObject, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations, animationList.Select(x => (AnimationClip)x.Asset).ToArray())
: new ModelConverter(rootName, gameObject, Properties.Settings.Default.convertType, Studio.Game, Properties.Settings.Default.collectAnimations);
ExportFbx(convert, exportPath);
}
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 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, scaleFactor, fbxVersion, fbxFormat == 1);
}
public static bool ExportDumpFile(AssetItem item, string exportPath)
{
if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath))
@@ -341,16 +389,28 @@ namespace AssetStudioCLI
case ClassIDType.Sprite:
return ExportSprite(item, exportPath);
case ClassIDType.Animator:
return false;
return ExportAnimator(item, exportPath, new List<AssetItem>());
case ClassIDType.AnimationClip:
return ExportAnimationClip(item, exportPath);
return false;
case ClassIDType.MiHoYoBinData:
return ExportMiHoYoBinData(item, exportPath);
default:
return ExportJsonFile(item, exportPath);
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();

70
AssetStudioCLI/Options.cs Normal file
View File

@@ -0,0 +1,70 @@
using System;
using AssetStudio;
using System.ComponentModel;
using System.Configuration;
namespace AssetStudioCLI.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)
{
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 encrypted => AppSettings.Get("encrypted", true);
public byte key => AppSettings.Get("key", (byte)0x93);
public bool skipRenderer => AppSettings.Get("skipRenderer", false);
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);
}
}

View File

@@ -32,9 +32,9 @@ namespace AssetStudioCLI
optionsBinder.MapType,
optionsBinder.MapName,
optionsBinder.GroupAssetsType,
optionsBinder.NoAssetBundle,
optionsBinder.NoIndexObject,
optionsBinder.XorByte,
optionsBinder.IncludeAnimators,
optionsBinder.SkipMiHoYoBinData,
optionsBinder.Key,
optionsBinder.AIFile,
optionsBinder.Input,
optionsBinder.Output
@@ -54,36 +54,32 @@ namespace AssetStudioCLI
}
Studio.Game = game;
Logger.Default = new ConsoleLogger();
assetsManager.Silent = o.Silent;
assetsManager.Game = game;
AssetBundle.Exportable = !o.NoAssetBundle;
IndexObject.Exportable = !o.NoIndexObject;
MiHoYoBinData.Exportable = !o.SkipMiHoYoBinData;
if (!o.Silent)
if (o.Key != default)
{
Logger.Default = new ConsoleLogger();
}
if (o.XorKey != default)
{
if (o.NoIndexObject)
if (o.SkipMiHoYoBinData)
{
Logger.Warning("XOR key is set but IndexObject/MiHoYoBinData is excluded, ignoring key...");
Logger.Warning("Key is set but IndexObject/MiHoYoBinData is excluded, ignoring key...");
}
else
{
MiHoYoBinData.doXOR = true;
MiHoYoBinData.Key = o.XorKey;
MiHoYoBinData.Encrypted = true;
MiHoYoBinData.Key = o.Key;
}
}
if (o.AIFile != null && game.Name == "GI" || game.Name == "GI_CB2" || game.Name == "GI_CB3")
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, $"*{game.Extension}", SearchOption.AllDirectories).OrderBy(x => x.Length).ToArray() : new string[] { o.Input.FullName };
Logger.Info(string.Format("Found {0} file(s)", files.Count()));
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))
{
@@ -91,15 +87,18 @@ namespace AssetStudioCLI
foreach (var file in files)
{
assetsManager.LoadFiles(file);
BuildAssetData(o.TypeFilter, o.NameFilter, o.ContainerFilter, ref i);
ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType);
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))
{
CABManager.BuildMap(files.ToList(), game);
AssetsHelper.BuildCABMap(files, "", "", game);
}
if (o.MapOp.HasFlag(MapOpType.AssetMap))
{
@@ -107,16 +106,16 @@ namespace AssetStudioCLI
{
throw new Exception("Unable to build AssetMap with input_path as a file !!");
}
var assets = BuildAssetMap(files.ToList(), o.TypeFilter, o.NameFilter, o.ContainerFilter);
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_{game.Name}";
o.MapName = "assets_map";
}
ExportAssetsMap(o.Output.FullName, assets, o.MapName, o.MapType);
AssetsHelper.ExportAssetsMap(assets, o.MapName, o.Output.FullName, o.MapType);
}
}
catch (Exception e)
@@ -141,9 +140,9 @@ namespace AssetStudioCLI
public ExportListType MapType { get; set; }
public string MapName { get; set; }
public AssetGroupOption GroupAssetsType { get; set; }
public bool NoAssetBundle { get; set; }
public bool NoIndexObject { get; set; }
public byte XorKey { 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; }
@@ -160,9 +159,9 @@ namespace AssetStudioCLI
public readonly Option<ExportListType> MapType;
public readonly Option<string> MapName;
public readonly Option<AssetGroupOption> GroupAssetsType;
public readonly Option<bool> NoAssetBundle;
public readonly Option<bool> NoIndexObject;
public readonly Option<byte> XorByte;
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;
@@ -178,13 +177,13 @@ namespace AssetStudioCLI
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.");
NoAssetBundle = new Option<bool>("--no_asset_bundle", "Exclude AssetBundle from AssetMap/Export.");
NoIndexObject = new Option<bool>("--no_index_object", "Exclude IndexObject/MiHoYoBinData from AssetMap/Export.");
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();
XorByte = new Option<byte>("--xor_key", result =>
Key = new Option<byte>("--key", result =>
{
var value = result.Tokens.Single().Value;
if (value.StartsWith("0x"))
@@ -196,12 +195,12 @@ namespace AssetStudioCLI
{
return byte.Parse(value);
}
}, false, "XOR key to decrypt MiHoYoBinData.");
}, false, "Key to decrypt MiHoYoBinData.");
TypeFilter.AddValidator(FilterValidator);
NameFilter.AddValidator(FilterValidator);
ContainerFilter.AddValidator(FilterValidator);
XorByte.AddValidator(result =>
Key.AddValidator(result =>
{
var value = result.Tokens.Single().Value;
try
@@ -266,9 +265,9 @@ namespace AssetStudioCLI
MapType = bindingContext.ParseResult.GetValueForOption(MapType),
MapName = bindingContext.ParseResult.GetValueForOption(MapName),
GroupAssetsType = bindingContext.ParseResult.GetValueForOption(GroupAssetsType),
NoAssetBundle = bindingContext.ParseResult.GetValueForOption(NoAssetBundle),
NoIndexObject = bindingContext.ParseResult.GetValueForOption(NoIndexObject),
XorKey = bindingContext.ParseResult.GetValueForOption(XorByte),
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

@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Text.RegularExpressions;
using static AssetStudioCLI.Exporter;
using Object = AssetStudio.Object;
using System.Globalization;
namespace AssetStudioCLI
{
@@ -20,11 +21,7 @@ namespace AssetStudioCLI
CABMap,
Both
}
public enum ExportListType
{
XML,
JSON
}
public enum AssetGroupOption
{
ByType,
@@ -35,7 +32,7 @@ namespace AssetStudioCLI
internal static class Studio
{
public static AssetsManager assetsManager = new AssetsManager() { ResolveDependancies = false };
public static AssetsManager assetsManager = new AssetsManager() { ResolveDependencies = false };
public static List<AssetItem> exportableAssets = new List<AssetItem>();
public static Game Game;
@@ -67,13 +64,16 @@ namespace AssetStudioCLI
public static int ExtractFile(string fileName, string savePath)
{
int extractedCount = 0;
var reader = new FileReader(fileName, Game);
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.GameFile)
extractedCount += ExtractGameFile(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;
@@ -82,12 +82,19 @@ namespace AssetStudioCLI
private static int ExtractBundleFile(FileReader reader, string savePath)
{
Logger.Info($"Decompressing {reader.FileName} ...");
var bundleFile = new BundleFile(reader);
reader.Dispose();
if (bundleFile.FileList.Length > 0)
try
{
var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked");
return ExtractStreamFile(extractPath, bundleFile.FileList);
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;
}
@@ -105,16 +112,69 @@ namespace AssetStudioCLI
return 0;
}
private static int ExtractGameFile(FileReader reader, string savePath)
private static int ExtractBlkFile(FileReader reader, string savePath)
{
Logger.Info($"Decompressing {reader.FileName}...");
var gameFile = new GameFile(reader);
reader.Dispose();
var fileList = gameFile.Bundles.SelectMany(x => x.Value).ToList();
if (fileList.Count > 0)
int total = 0;
Logger.Info($"Decompressing {reader.FileName} ...");
try
{
var extractPath = Path.Combine(savePath, Path.GetFileNameWithoutExtension(reader.FileName));
return ExtractStreamFile(extractPath, fileList.ToArray());
using var stream = BlkUtils.Decrypt(reader, (Blk)Game);
do
{
stream.Offset = stream.RelativePosition;
var dummyPath = Path.Combine(reader.FullPath, stream.RelativePosition.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 BlockStream(reader.BaseStream, 0);
do
{
stream.Offset = stream.RelativePosition;
var subSavePath = Path.Combine(savePath, reader.FileName + "_unpacked");
var dummyPath = Path.Combine(reader.FullPath, stream.RelativePosition.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;
}
@@ -143,165 +203,29 @@ namespace AssetStudioCLI
return extractedCount;
}
public static List<AssetEntry> BuildAssetMap(List<string> files, ClassIDType[] typeFilters, Regex[] nameFilters, Regex[] containerFilters)
public static void UpdateContainers()
{
var assets = new List<AssetEntry>();
for (int i = 0; i < files.Count; i++)
if (exportableAssets.Count > 0)
{
var file = files[i];
var reader = new FileReader(file, Game);
var gameFile = new GameFile(reader);
reader.Dispose();
foreach (var bundle in gameFile.Bundles)
Logger.Info("Updating Containers...");
foreach (var asset in exportableAssets)
{
foreach (var cab in bundle.Value)
if (int.TryParse(asset.Container, out var value))
{
var dummyPath = Path.Combine(Path.GetDirectoryName(file), cab.fileName);
using (var cabReader = new FileReader(dummyPath, cab.stream, Game))
var last = unchecked((uint)value);
var path = ResourceIndex.GetAssetPath(last);
if (!string.IsNullOrEmpty(path))
{
if (cabReader.FileType == FileType.AssetsFile)
asset.Container = path;
if (asset.Type == ClassIDType.MiHoYoBinData)
{
var assetsFile = new SerializedFile(cabReader, assetsManager, file);
assetsManager.assetsFileList.Add(assetsFile);
assetsFile.m_Objects = assetsFile.m_Objects.Where(x => x.HasExportableType()).ToList();
IndexObject indexObject = null;
var containers = new List<(PPtr<Object>, string)>(assetsFile.m_Objects.Count);
var animators = new List<(PPtr<GameObject>, AssetEntry)>(assetsFile.m_Objects.Count);
var objectAssetItemDic = new Dictionary<Object, AssetEntry>(assetsFile.m_Objects.Count);
foreach (var objInfo in assetsFile.m_Objects)
{
var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objInfo);
var obj = new Object(objectReader);
var asset = new AssetEntry()
{
Source = file,
PathID = objectReader.m_PathID,
Type = objectReader.type,
Container = ""
};
var exportable = true;
switch (objectReader.type)
{
case ClassIDType.AssetBundle:
var assetBundle = new AssetBundle(objectReader);
foreach (var m_Container in assetBundle.Container)
{
var preloadIndex = m_Container.Value.preloadIndex;
var preloadSize = m_Container.Value.preloadSize;
var preloadEnd = preloadIndex + preloadSize;
for (int k = preloadIndex; k < preloadEnd; k++)
{
if (Game.Name == "GI" || Game.Name == "GI_CB2" || Game.Name == "GI_CB3")
{
if (long.TryParse(m_Container.Key, out var containerValue))
{
var last = unchecked((uint)containerValue);
var path = ResourceIndex.GetBundlePath(last);
if (!string.IsNullOrEmpty(path))
{
containers.Add((assetBundle.PreloadTable[k], path));
continue;
}
}
}
containers.Add((assetBundle.PreloadTable[k], m_Container.Key));
}
}
obj = null;
asset.Name = assetBundle.m_Name;
exportable = AssetBundle.Exportable;
break;
case ClassIDType.GameObject:
var gameObject = new GameObject(objectReader);
obj = gameObject;
asset.Name = gameObject.m_Name;
exportable = false;
break;
case ClassIDType.Shader:
asset.Name = objectReader.ReadAlignedString();
if (string.IsNullOrEmpty(asset.Name))
{
var m_parsedForm = new SerializedShader(objectReader);
asset.Name = m_parsedForm.m_Name;
}
break;
case ClassIDType.Animator:
var component = new PPtr<GameObject>(objectReader);
animators.Add((component, asset));
asset.Name = "Animator";
break;
case ClassIDType.MiHoYoBinData:
if (indexObject.Names.TryGetValue(objectReader.m_PathID, out var binName))
{
var path = ResourceIndex.GetContainerFromBinName(file, binName);
asset.Container = path;
asset.Name = !string.IsNullOrEmpty(path) ? Path.GetFileName(path) : binName;
}
exportable = IndexObject.Exportable;
break;
case ClassIDType.IndexObject:
indexObject = new IndexObject(objectReader);
obj = null;
asset.Name = "IndexObject";
exportable = IndexObject.Exportable;
break;
default:
asset.Name = objectReader.ReadAlignedString();
break;
}
if (obj != null)
{
objectAssetItemDic.Add(obj, asset);
assetsFile.AddObject(obj);
}
var isMatchRegex = nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(asset.Name) || asset.Type == ClassIDType.Animator);
var isFilteredType = typeFilters.Length == 0 || typeFilters.Contains(asset.Type) || asset.Type == ClassIDType.Animator;
if (isMatchRegex && isFilteredType && exportable)
{
assets.Add(asset);
}
}
foreach (var pair in animators)
{
if (pair.Item1.TryGet(out var gameObject) && gameObject is GameObject && (nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(gameObject.m_Name))) && (typeFilters.Length == 0 || typeFilters.Contains(pair.Item2.Type)))
{
pair.Item2.Name = gameObject.m_Name;
}
else
{
assets.Remove(pair.Item2);
}
}
foreach ((var pptr, var container) in containers)
{
if (pptr.TryGet(out var obj))
{
var item = objectAssetItemDic[obj];
if (containerFilters.Length == 0 || containerFilters.Any(x => x.IsMatch(container)))
{
item.Container = container;
}
else
{
assets.Remove(item);
}
}
}
assetsManager.assetsFileList.Clear();
asset.Text = Path.GetFileNameWithoutExtension(path);
}
}
}
}
Logger.Info($"[{i + 1}/{files.Count}] Processed {Path.GetFileName(file)}");
Progress.Report(i + 1, files.Count);
Logger.Info("Updated !!");
}
return assets;
}
public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters, Regex[] containerFilters, ref int i)
@@ -309,6 +233,7 @@ namespace AssetStudioCLI
string productName = null;
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
var objectAssetItemDic = new Dictionary<Object, AssetItem>(objectCount);
var mihoyoBinDataNames = new List<(PPtr<Object>, string)>();
var containers = new List<(PPtr<Object>, string)>();
foreach (var assetsFile in assetsManager.assetsFileList)
{
@@ -359,6 +284,7 @@ namespace AssetStudioCLI
{
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))
@@ -374,35 +300,27 @@ namespace AssetStudioCLI
productName = m_PlayerSettings.productName;
break;
case AssetBundle m_AssetBundle:
foreach (var m_Container in m_AssetBundle.Container)
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++)
{
if (Game.Name == "GI" || Game.Name == "GI_CB2" || Game.Name == "GI_CB3")
{
if (long.TryParse(m_Container.Key, out var containerValue))
{
var last = unchecked((uint)containerValue);
var path = ResourceIndex.GetBundlePath(last);
if (!string.IsNullOrEmpty(path))
{
containers.Add((m_AssetBundle.PreloadTable[k], path));
continue;
}
}
}
containers.Add((m_AssetBundle.PreloadTable[k], m_Container.Key));
containers.Add((m_AssetBundle.m_PreloadTable[k], m_Container.Key));
}
}
assetItem.Text = m_AssetBundle.m_Name;
exportable = AssetBundle.Exportable;
break;
case IndexObject m_IndexObject:
foreach (var index in m_IndexObject.AssetMap)
{
mihoyoBinDataNames.Add((index.Value.Object, index.Key));
}
assetItem.Text = "IndexObject";
exportable = IndexObject.Exportable;
break;
case MiHoYoBinData m_MiHoYoBinData:
exportable = MiHoYoBinData.Exportable;
break;
case ResourceManager m_ResourceManager:
foreach (var m_Container in m_ResourceManager.m_Container)
@@ -410,35 +328,6 @@ namespace AssetStudioCLI
containers.Add((m_Container.Value, m_Container.Key));
}
break;
case MiHoYoBinData m_MiHoYoBinData:
if (m_MiHoYoBinData.assetsFile.ObjectsDic.TryGetValue(2, out var obj) && obj is IndexObject indexObject)
{
if (indexObject.Names.TryGetValue(m_MiHoYoBinData.m_PathID, out var binName))
{
string path = "";
var game = GameManager.GetGame("GI");
if (Path.GetExtension(assetsFile.originalPath) == game.Extension)
{
var blkName = Path.GetFileNameWithoutExtension(assetsFile.originalPath);
var blk = Convert.ToUInt64(blkName);
var lastHex = Convert.ToUInt32(binName, 16);
var blkHash = (blk << 32) | lastHex;
var index = ResourceIndex.GetAssetIndex(blkHash);
var bundleInfo = ResourceIndex.GetBundleInfo(index);
path = bundleInfo != null ? bundleInfo.Path : "";
}
else
{
var last = Convert.ToUInt32(binName, 16);
path = ResourceIndex.GetBundlePath(last) ?? "";
}
assetItem.Container = path;
assetItem.Text = !string.IsNullOrEmpty(path) ? Path.GetFileName(path) : binName;
}
}
else assetItem.Text = string.Format("BinFile #{0}", assetItem.m_PathID);
exportable = IndexObject.Exportable;
break;
case NamedObject m_NamedObject:
assetItem.Text = m_NamedObject.m_Name;
exportable = true;
@@ -457,12 +346,25 @@ namespace AssetStudioCLI
}
}
}
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}";
}
}
foreach ((var pptr, var container) in containers)
{
if (pptr.TryGet(out var obj))
{
var item = objectAssetItemDic[obj];
if (containerFilters.Length == 0 || containerFilters.Any(x => x.IsMatch(container)))
if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container)))
{
item.Container = container;
}
@@ -472,6 +374,10 @@ namespace AssetStudioCLI
}
}
}
if (Game.Type.IsGISubGroup())
{
UpdateContainers();
}
containers.Clear();
}