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

@@ -4,21 +4,21 @@ using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Globalization;
using System.Xml.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
using System.Xml;
namespace AssetStudio
{
public static class AssetsHelper
{
public const string CABMapName = "Maps";
public const string MapName = "Maps";
public static CancellationTokenSource tokenSource = new CancellationTokenSource();
private static string BaseFolder = "";
private static Dictionary<string, Entry> CABMap = new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase);
private static Dictionary<string, Entry> Map = new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase);
private static Dictionary<string, HashSet<long>> Offsets = new Dictionary<string, HashSet<long>>();
private static AssetsManager assetsManager = new AssetsManager() { Silent = true, SkipProcess = true, ResolveDependencies = false };
@@ -31,14 +31,14 @@ namespace AssetStudio
public static string[] GetMaps()
{
Directory.CreateDirectory(CABMapName);
var files = Directory.GetFiles(CABMapName, "*.bin", SearchOption.TopDirectoryOnly);
Directory.CreateDirectory(MapName);
var files = Directory.GetFiles(MapName, "*.bin", SearchOption.TopDirectoryOnly);
return files.Select(x => Path.GetFileNameWithoutExtension(x)).ToArray();
}
public static void Clear()
{
CABMap.Clear();
Map.Clear();
Offsets.Clear();
BaseFolder = string.Empty;
@@ -55,7 +55,7 @@ namespace AssetStudio
public static bool TryAdd(string name, out string path)
{
if (CABMap.TryGetValue(name, out var entry))
if (Map.TryGetValue(name, out var entry))
{
path = Path.Combine(BaseFolder, entry.Path);
if (!Offsets.ContainsKey(path))
@@ -80,12 +80,12 @@ namespace AssetStudio
return false;
}
public static void BuildCABMap(string[] files, string mapName, string baseFolder, Game game)
public static void BuildMap(string[] files, string mapName, string baseFolder, Game game)
{
Logger.Info($"Processing...");
try
{
CABMap.Clear();
Map.Clear();
var collision = 0;
BaseFolder = baseFolder;
assetsManager.Game = game;
@@ -100,7 +100,7 @@ namespace AssetStudio
{
if (tokenSource.IsCancellationRequested)
{
Logger.Info("Building CABMap has been aborted !!");
Logger.Info("Building Map has been aborted !!");
return;
}
var dependencies = assetsFile.m_Externals.Select(x => x.fileName).ToArray();
@@ -111,20 +111,20 @@ namespace AssetStudio
Dependencies = dependencies
};
if (CABMap.ContainsKey(assetsFile.fileName))
if (Map.ContainsKey(assetsFile.fileName))
{
collision++;
continue;
}
CABMap.Add(assetsFile.fileName, entry);
Map.Add(assetsFile.fileName, entry);
}
Logger.Info($"Processed {Path.GetFileName(file)}");
}
assetsManager.Clear();
}
CABMap = CABMap.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
var outputFile = Path.Combine(CABMapName, $"{mapName}.bin");
Map = Map.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
var outputFile = Path.Combine(MapName, $"{mapName}.bin");
Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
@@ -132,8 +132,8 @@ namespace AssetStudio
using (var writer = new BinaryWriter(binaryFile))
{
writer.Write(BaseFolder);
writer.Write(CABMap.Count);
foreach (var kv in CABMap)
writer.Write(Map.Count);
foreach (var kv in Map)
{
writer.Write(kv.Key);
writer.Write(kv.Value.Path);
@@ -146,21 +146,243 @@ namespace AssetStudio
}
}
Logger.Info($"CABMap build successfully !! {collision} collisions found");
Logger.Info($"Map build successfully !! {collision} collisions found");
}
catch (Exception e)
{
Logger.Warning($"CABMap was not build, {e}");
Logger.Warning($"Map was not build, {e}");
}
}
public static void LoadMap(string mapName)
public static void BuildBoth(string[] files, string mapName, string baseFolder, Game game, string savePath, ExportListType exportListType, ManualResetEvent resetEvent = null, Regex[] nameFilters = null, Regex[] containerFilters = null)
{
Map.Clear();
var collision = 0;
BaseFolder = baseFolder;
assetsManager.Game = game;
var assets = new List<AssetEntry>();
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
assetsManager.LoadFiles(file);
if (assetsManager.assetsFileList.Count > 0)
{
var relativePath = Path.GetRelativePath(BaseFolder, file);
var containers = new List<(PPtr<Object>, string)>();
var mihoyoBinDataNames = new List<(PPtr<Object>, string)>();
var objectAssetItemDic = new Dictionary<Object, AssetEntry>();
var animators = new List<(PPtr<Object>, AssetEntry)>();
foreach (var assetsFile in assetsManager.assetsFileList)
{
if (tokenSource.IsCancellationRequested)
{
Logger.Info("Building Map has been aborted !!");
return;
}
var dependencies = assetsFile.m_Externals.Select(x => x.fileName).ToArray();
var entry = new Entry()
{
Path = relativePath,
Offset = assetsFile.offset,
Dependencies = dependencies
};
if (Map.ContainsKey(assetsFile.fileName))
{
collision++;
continue;
}
Map.Add(assetsFile.fileName, entry);
foreach (var objInfo in assetsFile.m_Objects)
{
var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objInfo, game);
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.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((assetBundle.m_PreloadTable[k], m_Container.Key));
}
}
obj = null;
asset.Name = assetBundle.m_Name;
exportable = false;
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<Object>(objectReader);
animators.Add((component, asset));
break;
case ClassIDType.MiHoYoBinData:
var MiHoYoBinData = new MiHoYoBinData(objectReader);
obj = MiHoYoBinData;
exportable = true;
break;
case ClassIDType.IndexObject:
var indexObject = new IndexObject(objectReader);
obj = null;
foreach (var index in indexObject.AssetMap)
{
mihoyoBinDataNames.Add((index.Value.Object, index.Key));
}
asset.Name = "IndexObject";
break;
case ClassIDType.Font:
case ClassIDType.Material:
case ClassIDType.Texture:
case ClassIDType.Mesh:
case ClassIDType.Sprite:
case ClassIDType.TextAsset:
case ClassIDType.Texture2D:
case ClassIDType.VideoClip:
case ClassIDType.AudioClip:
asset.Name = objectReader.ReadAlignedString();
break;
default:
exportable = false;
break;
}
if (obj != null)
{
objectAssetItemDic.Add(obj, asset);
assetsFile.AddObject(obj);
}
var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(asset.Name) || asset.Type == ClassIDType.Animator);
if (isMatchRegex && exportable)
{
assets.Add(asset);
}
}
}
foreach ((var pptr, var asset) in animators)
{
if (pptr.TryGet<GameObject>(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name))))
{
asset.Name = gameObject.m_Name;
}
}
foreach ((var pptr, var name) in mihoyoBinDataNames)
{
if (pptr.TryGet<MiHoYoBinData>(out var miHoYoBinData))
{
var asset = objectAssetItemDic[miHoYoBinData];
if (int.TryParse(name, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hash))
{
asset.Name = name;
asset.Container = hash.ToString();
}
else asset.Name = $"BinFile #{asset.PathID}";
}
}
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
{
assets.Remove(item);
}
}
}
Logger.Info($"Processed {Path.GetFileName(file)}");
}
assetsManager.Clear();
}
if (game.Type.IsGISubGroup() && assets.Count > 0)
{
Logger.Info("Updating Containers...");
foreach (var asset in assets)
{
if (int.TryParse(asset.Container, out var value))
{
var last = unchecked((uint)value);
var name = Path.GetFileNameWithoutExtension(asset.Source);
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.Name = Path.GetFileNameWithoutExtension(path);
}
}
}
}
}
Logger.Info("Updated !!");
}
Map = Map.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
var outputFile = Path.Combine(MapName, $"{mapName}.bin");
Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
using (var binaryFile = File.OpenWrite(outputFile))
using (var writer = new BinaryWriter(binaryFile))
{
writer.Write(BaseFolder);
writer.Write(Map.Count);
foreach (var kv in Map)
{
writer.Write(kv.Key);
writer.Write(kv.Value.Path);
writer.Write(kv.Value.Offset);
writer.Write(kv.Value.Dependencies.Length);
foreach (var cab in kv.Value.Dependencies)
{
writer.Write(cab);
}
}
}
Logger.Info($"Map build successfully !! {collision} collisions found");
ExportAssetsMap(assets.ToArray(), mapName, savePath, exportListType, resetEvent);
}
public static bool LoadMap(string mapName)
{
Logger.Info($"Loading {mapName}");
try
{
CABMap.Clear();
using (var fs = File.OpenRead(Path.Combine(CABMapName, $"{mapName}.bin")))
Map.Clear();
using (var fs = File.OpenRead(Path.Combine(MapName, $"{mapName}.bin")))
using (var reader = new BinaryReader(fs))
{
BaseFolder = reader.ReadString();
@@ -183,18 +405,21 @@ namespace AssetStudio
Offset = offset,
Dependencies = dependencies.ToArray()
};
CABMap.Add(cab, entry);
Map.Add(cab, entry);
}
}
Logger.Info($"Loaded {mapName} !!");
return true;
}
catch (Exception e)
{
Logger.Warning($"{mapName} was not loaded, {e}");
Logger.Warning($"{mapName} was not loaded, {e}");
}
return false;
}
public static AssetEntry[] BuildAssetMap(string[] files, Game game, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null)
public static AssetEntry[] BuildAssetMap(string[] files, Game game, Regex[] nameFilters = null, Regex[] containerFilters = null)
{
assetsManager.Game = game;
var assets = new List<AssetEntry>();
@@ -210,8 +435,6 @@ namespace AssetStudio
var animators = new List<(PPtr<Object>, AssetEntry)>();
foreach (var assetsFile in assetsManager.assetsFileList)
{
assetsFile.m_Objects = ObjectInfo.Filter(assetsFile.m_Objects);
foreach (var objInfo in assetsFile.m_Objects)
{
var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objInfo, game);
@@ -264,7 +487,7 @@ namespace AssetStudio
case ClassIDType.MiHoYoBinData:
var MiHoYoBinData = new MiHoYoBinData(objectReader);
obj = MiHoYoBinData;
exportable = MiHoYoBinData.Exportable;
exportable = true;
break;
case ClassIDType.IndexObject:
var indexObject = new IndexObject(objectReader);
@@ -275,9 +498,20 @@ namespace AssetStudio
}
asset.Name = "IndexObject";
break;
default:
case ClassIDType.Font:
case ClassIDType.Material:
case ClassIDType.Texture:
case ClassIDType.Mesh:
case ClassIDType.Sprite:
case ClassIDType.TextAsset:
case ClassIDType.Texture2D:
case ClassIDType.VideoClip:
case ClassIDType.AudioClip:
asset.Name = objectReader.ReadAlignedString();
break;
default:
exportable = false;
break;
}
if (obj != null)
{
@@ -285,8 +519,7 @@ namespace AssetStudio
assetsFile.AddObject(obj);
}
var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(asset.Name) || asset.Type == ClassIDType.Animator);
var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(asset.Type) || asset.Type == ClassIDType.Animator;
if (isMatchRegex && isFilteredType && exportable)
if (isMatchRegex && exportable)
{
assets.Add(asset);
}
@@ -294,7 +527,7 @@ namespace AssetStudio
}
foreach ((var pptr, var asset) in animators)
{
if (pptr.TryGet<GameObject>(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name))) && (typeFilters.IsNullOrEmpty() || typeFilters.Contains(asset.Type)))
if (pptr.TryGet<GameObject>(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name))))
{
asset.Name = gameObject.m_Name;
}
@@ -339,23 +572,27 @@ namespace AssetStudio
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.Source);
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.Name = Path.GetFileNameWithoutExtension(path);
asset.Container = path;
if (asset.Type == ClassIDType.MiHoYoBinData)
{
asset.Name = Path.GetFileNameWithoutExtension(path);
}
}
}
}
Logger.Info("Updated !!");
}
Logger.Info("Updated !!");
}
return assets.ToArray();
}
public static void ExportAssetsMap(AssetEntry[] toExportAssets, string name, string savePath, ExportListType exportListType)
public static void ExportAssetsMap(AssetEntry[] toExportAssets, string name, string savePath, ExportListType exportListType, ManualResetEvent resetEvent = null)
{
ThreadPool.QueueUserWorkItem(state =>
{
@@ -368,28 +605,35 @@ namespace AssetStudio
{
case ExportListType.XML:
filename = Path.Combine(savePath, $"{name}.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, $"{name}.json");
using (StreamWriter file = File.CreateText(filename))
{
var serializer = new JsonSerializer() { Formatting = Formatting.Indented };
var serializer = new JsonSerializer() { Formatting = Newtonsoft.Json.Formatting.Indented };
serializer.Converters.Add(new StringEnumConverter());
serializer.Serialize(file, toExportAssets);
}
@@ -398,6 +642,8 @@ namespace AssetStudio
Logger.Info($"Finished exporting asset list with {toExportAssets.Length} items.");
Logger.Info($"AssetMap build successfully !!");
resetEvent?.Set();
});
}
}