v0.90.10
This commit is contained in:
@@ -2,15 +2,15 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
<Version>0.90.00</Version>
|
||||
<AssemblyVersion>0.90.00</AssemblyVersion>
|
||||
<FileVersion>0.90.00</FileVersion>
|
||||
<Version>0.90.10</Version>
|
||||
<AssemblyVersion>0.90.10</AssemblyVersion>
|
||||
<FileVersion>0.90.10</FileVersion>
|
||||
<Copyright>Copyright © Razmoth 2022; Copyright © Perfare 2018-2022</Copyright>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.5" />
|
||||
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.6" />
|
||||
<PackageReference Include="MessagePack" Version="2.6.100-alpha" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ZstdSharp.Port" Version="0.7.2" />
|
||||
|
||||
@@ -36,7 +36,9 @@ namespace AssetStudio
|
||||
{
|
||||
Directory.CreateDirectory(MapName);
|
||||
var files = Directory.GetFiles(MapName, "*.bin", SearchOption.TopDirectoryOnly);
|
||||
return files.Select(Path.GetFileNameWithoutExtension).ToArray();
|
||||
var mapNames = files.Select(Path.GetFileNameWithoutExtension).ToArray();
|
||||
Logger.Verbose($"Found {mapNames.Length} CABMaps under Maps folder");
|
||||
return mapNames;
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
@@ -48,34 +50,20 @@ namespace AssetStudio
|
||||
tokenSource.Dispose();
|
||||
tokenSource = new CancellationTokenSource();
|
||||
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
Logger.Verbose("Cleared AssetsHelper successfully !!");
|
||||
}
|
||||
|
||||
public static void ClearOffsets() => Offsets.Clear();
|
||||
|
||||
public static void Remove(string path) => Offsets.Remove(path);
|
||||
|
||||
public static bool TryAdd(string name, out string path)
|
||||
public static void ClearOffsets()
|
||||
{
|
||||
if (CABMap.TryGetValue(name, out var entry))
|
||||
{
|
||||
path = Path.Combine(BaseFolder, entry.Path);
|
||||
if (!Offsets.ContainsKey(path))
|
||||
{
|
||||
Offsets.Add(path, new HashSet<long>());
|
||||
}
|
||||
Offsets[path].Add(entry.Offset);
|
||||
return true;
|
||||
}
|
||||
path = string.Empty;
|
||||
return false;
|
||||
Offsets.Clear();
|
||||
Logger.Verbose("Cleared cached offsets");
|
||||
}
|
||||
|
||||
public static bool TryGet(string path, out long[] offsets)
|
||||
{
|
||||
if (Offsets.TryGetValue(path, out var list))
|
||||
if (Offsets.TryGetValue(path, out var list) && list.Count > 0)
|
||||
{
|
||||
Logger.Verbose($"Found {list.Count} offsets for path {path}");
|
||||
offsets = list.ToArray();
|
||||
return true;
|
||||
}
|
||||
@@ -83,6 +71,67 @@ namespace AssetStudio
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void AddCABOffsets(string[] paths, List<string> cabs)
|
||||
{
|
||||
for (int i = 0; i < cabs.Count; i++)
|
||||
{
|
||||
var cab = cabs[i];
|
||||
if (CABMap.TryGetValue(cab, out var entry))
|
||||
{
|
||||
var fullPath = Path.Combine(BaseFolder, entry.Path);
|
||||
Logger.Verbose($"Found {cab} in {fullPath}");
|
||||
if (!paths.Contains(fullPath))
|
||||
{
|
||||
Offsets.TryAdd(fullPath, new HashSet<long>());
|
||||
Offsets[fullPath].Add(entry.Offset);
|
||||
Logger.Verbose($"Added {fullPath} to Offsets, at offset {entry.Offset}");
|
||||
}
|
||||
foreach (var dep in entry.Dependencies)
|
||||
{
|
||||
if (!cabs.Contains(dep))
|
||||
cabs.Add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool FindCAB(string path, out List<string> cabs)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(BaseFolder, path);
|
||||
cabs = CABMap.AsParallel().Where(x => x.Value.Path.Equals(relativePath, StringComparison.OrdinalIgnoreCase)).Select(x => x.Key).Distinct().ToList();
|
||||
Logger.Verbose($"Found {cabs.Count} that belongs to {relativePath}");
|
||||
return cabs.Count != 0;
|
||||
}
|
||||
|
||||
public static string[] ProcessFiles(string[] files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
Offsets.TryAdd(file, new HashSet<long>());
|
||||
Logger.Verbose($"Added {file} to Offsets dictionary");
|
||||
if (FindCAB(file, out var cabs))
|
||||
{
|
||||
AddCABOffsets(files, cabs);
|
||||
}
|
||||
}
|
||||
Logger.Verbose($"Finished resolving dependncies, the original {files.Length} files will be loaded entirely, and the {Offsets.Count - files.Length} dependicnes will be loaded from cached offsets only");
|
||||
return Offsets.Keys.ToArray();
|
||||
}
|
||||
|
||||
public static string[] ProcessDependencies(string[] files)
|
||||
{
|
||||
if (CABMap.Count == 0)
|
||||
{
|
||||
Logger.Warning("CABMap is not build, skip resolving dependencies...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info("Resolving Dependencies...");
|
||||
files = ProcessFiles(files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
public static void BuildCABMap(string[] files, string mapName, string baseFolder, Game game)
|
||||
{
|
||||
Logger.Info("Building CABMap...");
|
||||
@@ -220,6 +269,7 @@ namespace AssetStudio
|
||||
CABMap.Add(cab, entry);
|
||||
}
|
||||
}
|
||||
Logger.Verbose($"Initialized CABMap with {CABMap.Count} entries");
|
||||
Logger.Info($"Loaded {mapName} !!");
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -342,7 +392,7 @@ namespace AssetStudio
|
||||
case ClassIDType.Texture2D:
|
||||
case ClassIDType.VideoClip:
|
||||
case ClassIDType.AudioClip:
|
||||
case ClassIDType.AnimationClip:
|
||||
case ClassIDType.AnimationClip when AnimationClip.Parsable:
|
||||
asset.Name = objectReader.ReadAlignedString();
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace AssetStudio
|
||||
var path = Path.GetDirectoryName(Path.GetFullPath(files[0]));
|
||||
MergeSplitAssets(path);
|
||||
var toReadFile = ProcessingSplitFiles(files.ToList());
|
||||
if (ResolveDependencies)
|
||||
toReadFile = AssetsHelper.ProcessDependencies(toReadFile);
|
||||
Load(toReadFile);
|
||||
|
||||
if (Silent)
|
||||
@@ -89,6 +91,7 @@ namespace AssetStudio
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
Logger.Verbose($"caching {file} path and name to filter out duplicates");
|
||||
importFiles.Add(file);
|
||||
importFilesHash.Add(Path.GetFileName(file));
|
||||
}
|
||||
@@ -171,6 +174,7 @@ namespace AssetStudio
|
||||
|
||||
foreach (var sharedFile in assetsFile.m_Externals)
|
||||
{
|
||||
Logger.Verbose($"{assetsFile.fileName} needs external file {sharedFile.fileName}, attempting to look it up...");
|
||||
var sharedFileName = sharedFile.fileName;
|
||||
|
||||
if (!importFilesHash.Contains(sharedFileName))
|
||||
@@ -183,6 +187,7 @@ namespace AssetStudio
|
||||
var findFiles = Directory.GetFiles(Path.GetDirectoryName(reader.FullPath), sharedFileName, SearchOption.AllDirectories);
|
||||
if (findFiles.Length > 0)
|
||||
{
|
||||
Logger.Verbose($"Found {findFiles.Length} matching files, picking first file {findFiles[0]} !!");
|
||||
sharedFilePath = findFiles[0];
|
||||
}
|
||||
}
|
||||
@@ -193,6 +198,7 @@ namespace AssetStudio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Verbose("Nothing was found, caching into non existant files to avoid repeated searching !!");
|
||||
noexistFiles.Add(sharedFilePath);
|
||||
}
|
||||
}
|
||||
@@ -214,6 +220,7 @@ namespace AssetStudio
|
||||
|
||||
private void LoadAssetsFromMemory(FileReader reader, string originalPath, string unityVersion = null, long originalOffset = 0)
|
||||
{
|
||||
Logger.Verbose($"Loading asset file {reader.FileName} with version {unityVersion} from {originalPath} at offset 0x{originalOffset:X8}");
|
||||
if (!assetsFileListHash.Contains(reader.FileName))
|
||||
{
|
||||
try
|
||||
@@ -228,38 +235,6 @@ namespace AssetStudio
|
||||
CheckStrippedVersion(assetsFile);
|
||||
assetsFileList.Add(assetsFile);
|
||||
assetsFileListHash.Add(assetsFile.fileName);
|
||||
|
||||
if (ResolveDependencies)
|
||||
{
|
||||
foreach (var sharedFile in assetsFile.m_Externals)
|
||||
{
|
||||
var sharedFileName = sharedFile.fileName;
|
||||
|
||||
if (!importFilesHash.Contains(sharedFileName))
|
||||
{
|
||||
var sharedFilePath = Path.Combine(Path.GetDirectoryName(originalPath), sharedFileName);
|
||||
if (!noexistFiles.Contains(sharedFilePath))
|
||||
{
|
||||
if (AssetsHelper.TryAdd(sharedFileName, out var path))
|
||||
{
|
||||
sharedFilePath = path;
|
||||
}
|
||||
if (File.Exists(sharedFilePath))
|
||||
{
|
||||
if (!importFiles.Contains(sharedFilePath))
|
||||
{
|
||||
importFiles.Add(sharedFilePath);
|
||||
}
|
||||
importFilesHash.Add(sharedFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
noexistFiles.Add(sharedFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -290,6 +265,7 @@ namespace AssetStudio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Verbose("Caching resource stream");
|
||||
resourceFileReaders[file.fileName] = subReader; //TODO
|
||||
}
|
||||
}
|
||||
@@ -335,6 +311,7 @@ namespace AssetStudio
|
||||
LoadWebFile(subReader);
|
||||
break;
|
||||
case FileType.ResourceFile:
|
||||
Logger.Verbose("Caching resource stream");
|
||||
resourceFileReaders[file.fileName] = subReader; //TODO
|
||||
break;
|
||||
}
|
||||
@@ -358,8 +335,7 @@ namespace AssetStudio
|
||||
using (ZipArchive archive = new ZipArchive(reader.BaseStream, ZipArchiveMode.Read))
|
||||
{
|
||||
List<string> splitFiles = new List<string>();
|
||||
// register all files before parsing the assets so that the external references can be found
|
||||
// and find split files
|
||||
Logger.Verbose("Register all files before parsing the assets so that the external references can be found and find split files");
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
if (entry.Name.Contains(".split"))
|
||||
@@ -378,7 +354,7 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
// merge split files and load the result
|
||||
Logger.Verbose("Merge split files and load the result");
|
||||
foreach (string basePath in splitFiles)
|
||||
{
|
||||
try
|
||||
@@ -406,15 +382,14 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
// load all entries
|
||||
Logger.Verbose("Load all entries");
|
||||
Logger.Verbose($"Found {archive.Entries.Count} entries");
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), reader.FileName, entry.FullName);
|
||||
// create a new stream
|
||||
// - to store the deflated stream in
|
||||
// - to keep the data for later extraction
|
||||
Logger.Verbose("Create a new stream to store the deflated stream in and keep the data for later extraction");
|
||||
Stream streamReader = new MemoryStream();
|
||||
using (Stream entryStream = entry.Open())
|
||||
{
|
||||
@@ -429,6 +404,7 @@ namespace AssetStudio
|
||||
entryReader.Position = 0;
|
||||
if (!resourceFileReaders.ContainsKey(entry.Name))
|
||||
{
|
||||
Logger.Verbose("Caching resource file");
|
||||
resourceFileReaders.Add(entry.Name, entryReader);
|
||||
}
|
||||
}
|
||||
@@ -459,27 +435,14 @@ namespace AssetStudio
|
||||
{
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
var name = offset.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
|
||||
stream.Offset = offset;
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
LoadBundleFile(subReader, reader.FullPath, offset, false);
|
||||
LoadBlockSubFile(reader.FullPath, stream, offset);
|
||||
}
|
||||
AssetsHelper.Remove(reader.FullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
{
|
||||
var name = stream.AbsolutePosition.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
|
||||
stream.Offset = stream.AbsolutePosition;
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
LoadBundleFile(subReader, reader.FullPath, stream.AbsolutePosition, false);
|
||||
LoadBlockSubFile(reader.FullPath, stream, stream.AbsolutePosition);
|
||||
} while (stream.Remaining > 0);
|
||||
}
|
||||
}
|
||||
@@ -492,55 +455,38 @@ namespace AssetStudio
|
||||
reader.Dispose();
|
||||
}
|
||||
}
|
||||
private void LoadBlockSubFile(string path, OffsetStream stream, long offset)
|
||||
{
|
||||
var name = offset.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
|
||||
stream.Offset = offset;
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(path), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
LoadBundleFile(subReader, path, offset, false);
|
||||
}
|
||||
private void LoadBlkFile(FileReader reader)
|
||||
{
|
||||
Logger.Info("Loading " + reader.FullPath);
|
||||
try
|
||||
{
|
||||
using var stream = BlkUtils.Decrypt(reader, (Blk)Game);
|
||||
if (AssetsHelper.TryGet(reader.FullPath, out var offsets))
|
||||
foreach (var offset in stream.GetOffsets(reader.FullPath))
|
||||
{
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
var name = offset.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
var name = offset.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
|
||||
stream.Offset = offset;
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
switch (subReader.FileType)
|
||||
{
|
||||
case FileType.BundleFile:
|
||||
LoadBundleFile(subReader, reader.FullPath, offset, false);
|
||||
break;
|
||||
case FileType.Mhy0File:
|
||||
LoadMhy0File(subReader, reader.FullPath, offset, false);
|
||||
break;
|
||||
}
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
switch (subReader.FileType)
|
||||
{
|
||||
case FileType.BundleFile:
|
||||
LoadBundleFile(subReader, reader.FullPath, offset, false);
|
||||
break;
|
||||
case FileType.Mhy0File:
|
||||
LoadMhy0File(subReader, reader.FullPath, offset, false);
|
||||
break;
|
||||
}
|
||||
AssetsHelper.Remove(reader.FullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
do
|
||||
{
|
||||
var name = stream.AbsolutePosition.ToString("X8");
|
||||
Logger.Info($"Loading Block {name}");
|
||||
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name);
|
||||
var subReader = new FileReader(dummyPath, stream, true);
|
||||
switch (subReader.FileType)
|
||||
{
|
||||
case FileType.BundleFile:
|
||||
LoadBundleFile(subReader, reader.FullPath, stream.AbsolutePosition, false);
|
||||
break;
|
||||
case FileType.Mhy0File:
|
||||
LoadMhy0File(subReader, reader.FullPath, stream.AbsolutePosition, false);
|
||||
break;
|
||||
}
|
||||
|
||||
stream.Offset = stream.AbsolutePosition;
|
||||
} while (stream.Remaining > 0);
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
@@ -555,7 +501,7 @@ namespace AssetStudio
|
||||
{
|
||||
reader.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
private void LoadMhy0File(FileReader reader, string originalPath = null, long originalOffset = 0, bool log = true)
|
||||
{
|
||||
if (log)
|
||||
@@ -565,6 +511,7 @@ namespace AssetStudio
|
||||
try
|
||||
{
|
||||
var mhy0File = new Mhy0File(reader, reader.FullPath, (Mhy0)Game);
|
||||
Logger.Verbose($"mhy0 total size: {mhy0File.TotalSize:X8}");
|
||||
foreach (var file in mhy0File.fileList)
|
||||
{
|
||||
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName);
|
||||
@@ -575,6 +522,7 @@ namespace AssetStudio
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Verbose("Caching resource stream");
|
||||
resourceFileReaders[file.fileName] = cabReader; //TODO
|
||||
}
|
||||
}
|
||||
@@ -612,6 +560,8 @@ namespace AssetStudio
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Logger.Verbose("Cleaning up...");
|
||||
|
||||
foreach (var assetsFile in assetsFileList)
|
||||
{
|
||||
assetsFile.Objects.Clear();
|
||||
@@ -653,103 +603,40 @@ namespace AssetStudio
|
||||
var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objectInfo, Game);
|
||||
try
|
||||
{
|
||||
Object obj;
|
||||
switch (objectReader.type)
|
||||
Object obj = objectReader.type switch
|
||||
{
|
||||
case ClassIDType.Animation:
|
||||
obj = new Animation(objectReader);
|
||||
break;
|
||||
case ClassIDType.AnimationClip:
|
||||
obj = new AnimationClip(objectReader);
|
||||
break;
|
||||
case ClassIDType.Animator:
|
||||
obj = new Animator(objectReader);
|
||||
break;
|
||||
case ClassIDType.AnimatorController:
|
||||
obj = new AnimatorController(objectReader);
|
||||
break;
|
||||
case ClassIDType.AnimatorOverrideController:
|
||||
obj = new AnimatorOverrideController(objectReader);
|
||||
break;
|
||||
case ClassIDType.AssetBundle:
|
||||
obj = new AssetBundle(objectReader);
|
||||
break;
|
||||
case ClassIDType.AudioClip:
|
||||
obj = new AudioClip(objectReader);
|
||||
break;
|
||||
case ClassIDType.Avatar:
|
||||
obj = new Avatar(objectReader);
|
||||
break;
|
||||
case ClassIDType.Font:
|
||||
obj = new Font(objectReader);
|
||||
break;
|
||||
case ClassIDType.GameObject:
|
||||
obj = new GameObject(objectReader);
|
||||
break;
|
||||
case ClassIDType.IndexObject:
|
||||
obj = new IndexObject(objectReader);
|
||||
break;
|
||||
case ClassIDType.Material:
|
||||
obj = new Material(objectReader);
|
||||
break;
|
||||
case ClassIDType.Mesh:
|
||||
obj = new Mesh(objectReader);
|
||||
break;
|
||||
case ClassIDType.MeshFilter:
|
||||
obj = new MeshFilter(objectReader);
|
||||
break;
|
||||
case ClassIDType.MeshRenderer when Renderer.Parsable:
|
||||
obj = new MeshRenderer(objectReader);
|
||||
break;
|
||||
case ClassIDType.MiHoYoBinData:
|
||||
obj = new MiHoYoBinData(objectReader);
|
||||
break;
|
||||
case ClassIDType.MonoBehaviour:
|
||||
obj = new MonoBehaviour(objectReader);
|
||||
break;
|
||||
case ClassIDType.MonoScript:
|
||||
obj = new MonoScript(objectReader);
|
||||
break;
|
||||
case ClassIDType.MovieTexture:
|
||||
obj = new MovieTexture(objectReader);
|
||||
break;
|
||||
case ClassIDType.PlayerSettings:
|
||||
obj = new PlayerSettings(objectReader);
|
||||
break;
|
||||
case ClassIDType.RectTransform:
|
||||
obj = new RectTransform(objectReader);
|
||||
break;
|
||||
case ClassIDType.Shader when Shader.Parsable:
|
||||
obj = new Shader(objectReader);
|
||||
break;
|
||||
case ClassIDType.SkinnedMeshRenderer when Renderer.Parsable:
|
||||
obj = new SkinnedMeshRenderer(objectReader);
|
||||
break;
|
||||
case ClassIDType.Sprite:
|
||||
obj = new Sprite(objectReader);
|
||||
break;
|
||||
case ClassIDType.SpriteAtlas:
|
||||
obj = new SpriteAtlas(objectReader);
|
||||
break;
|
||||
case ClassIDType.TextAsset:
|
||||
obj = new TextAsset(objectReader);
|
||||
break;
|
||||
case ClassIDType.Texture2D:
|
||||
obj = new Texture2D(objectReader);
|
||||
break;
|
||||
case ClassIDType.Transform:
|
||||
obj = new Transform(objectReader);
|
||||
break;
|
||||
case ClassIDType.VideoClip:
|
||||
obj = new VideoClip(objectReader);
|
||||
break;
|
||||
case ClassIDType.ResourceManager:
|
||||
obj = new ResourceManager(objectReader);
|
||||
break;
|
||||
default:
|
||||
obj = new Object(objectReader);
|
||||
break;
|
||||
}
|
||||
ClassIDType.Animation => new Animation(objectReader),
|
||||
ClassIDType.AnimationClip when AnimationClip.Parsable => new AnimationClip(objectReader),
|
||||
ClassIDType.Animator => new Animator(objectReader),
|
||||
ClassIDType.AnimatorController => new AnimatorController(objectReader),
|
||||
ClassIDType.AnimatorOverrideController => new AnimatorOverrideController(objectReader),
|
||||
ClassIDType.AssetBundle => new AssetBundle(objectReader),
|
||||
ClassIDType.AudioClip => new AudioClip(objectReader),
|
||||
ClassIDType.Avatar => new Avatar(objectReader),
|
||||
ClassIDType.Font => new Font(objectReader),
|
||||
ClassIDType.GameObject => new GameObject(objectReader),
|
||||
ClassIDType.IndexObject => new IndexObject(objectReader),
|
||||
ClassIDType.Material => new Material(objectReader),
|
||||
ClassIDType.Mesh => new Mesh(objectReader),
|
||||
ClassIDType.MeshFilter => new MeshFilter(objectReader),
|
||||
ClassIDType.MeshRenderer when Renderer.Parsable => new MeshRenderer(objectReader),
|
||||
ClassIDType.MiHoYoBinData => new MiHoYoBinData(objectReader),
|
||||
ClassIDType.MonoBehaviour => new MonoBehaviour(objectReader),
|
||||
ClassIDType.MonoScript => new MonoScript(objectReader),
|
||||
ClassIDType.MovieTexture => new MovieTexture(objectReader),
|
||||
ClassIDType.PlayerSettings => new PlayerSettings(objectReader),
|
||||
ClassIDType.RectTransform => new RectTransform(objectReader),
|
||||
ClassIDType.Shader when Shader.Parsable => new Shader(objectReader),
|
||||
ClassIDType.SkinnedMeshRenderer when Renderer.Parsable => new SkinnedMeshRenderer(objectReader),
|
||||
ClassIDType.Sprite => new Sprite(objectReader),
|
||||
ClassIDType.SpriteAtlas => new SpriteAtlas(objectReader),
|
||||
ClassIDType.TextAsset => new TextAsset(objectReader),
|
||||
ClassIDType.Texture2D => new Texture2D(objectReader),
|
||||
ClassIDType.Transform => new Transform(objectReader),
|
||||
ClassIDType.VideoClip => new VideoClip(objectReader),
|
||||
ClassIDType.ResourceManager => new ResourceManager(objectReader),
|
||||
_ => new Object(objectReader),
|
||||
};
|
||||
assetsFile.AddObject(obj);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -784,57 +671,67 @@ namespace AssetStudio
|
||||
}
|
||||
if (obj is GameObject m_GameObject)
|
||||
{
|
||||
foreach (var pptr in m_GameObject.m_Components)
|
||||
{
|
||||
Logger.Verbose($"GameObject with {m_GameObject.m_PathID} in file {m_GameObject.assetsFile.fileName} has {m_GameObject.m_Components.Length} components, Attempting to fetch them...");
|
||||
foreach (var pptr in m_GameObject.m_Components)
|
||||
{
|
||||
if (pptr.TryGet(out var m_Component))
|
||||
{
|
||||
{
|
||||
switch (m_Component)
|
||||
{
|
||||
{
|
||||
case Transform m_Transform:
|
||||
Logger.Verbose($"Fetched Transform component with {m_Transform.m_PathID} in file {m_Transform.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_Transform = m_Transform;
|
||||
break;
|
||||
case MeshRenderer m_MeshRenderer:
|
||||
Logger.Verbose($"Fetched MeshRenderer component with {m_MeshRenderer.m_PathID} in file {m_MeshRenderer.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_MeshRenderer = m_MeshRenderer;
|
||||
break;
|
||||
case MeshFilter m_MeshFilter:
|
||||
Logger.Verbose($"Fetched MeshFilter component with {m_MeshFilter.m_PathID} in file {m_MeshFilter.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_MeshFilter = m_MeshFilter;
|
||||
break;
|
||||
case SkinnedMeshRenderer m_SkinnedMeshRenderer:
|
||||
Logger.Verbose($"Fetched SkinnedMeshRenderer component with {m_SkinnedMeshRenderer.m_PathID} in file {m_SkinnedMeshRenderer.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_SkinnedMeshRenderer = m_SkinnedMeshRenderer;
|
||||
break;
|
||||
case Animator m_Animator:
|
||||
Logger.Verbose($"Fetched Animator component with {m_Animator.m_PathID} in file {m_Animator.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_Animator = m_Animator;
|
||||
break;
|
||||
case Animation m_Animation:
|
||||
Logger.Verbose($"Fetched Animation component with {m_Animation.m_PathID} in file {m_Animation.assetsFile.fileName}, assigning to GameObject components...");
|
||||
m_GameObject.m_Animation = m_Animation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (obj is SpriteAtlas m_SpriteAtlas)
|
||||
{
|
||||
if (m_SpriteAtlas.m_RenderDataMap.Count > 0)
|
||||
if (m_SpriteAtlas.m_RenderDataMap.Count > 0)
|
||||
{
|
||||
Logger.Verbose($"SpriteAtlas with {m_SpriteAtlas.m_PathID} in file {m_SpriteAtlas.assetsFile.fileName} has {m_SpriteAtlas.m_PackedSprites.Length} packed sprites, Attempting to fetch them...");
|
||||
foreach (var m_PackedSprite in m_SpriteAtlas.m_PackedSprites)
|
||||
{
|
||||
foreach (var m_PackedSprite in m_SpriteAtlas.m_PackedSprites)
|
||||
{
|
||||
if (m_PackedSprite.TryGet(out var m_Sprite))
|
||||
{
|
||||
if (m_Sprite.m_SpriteAtlas.IsNull)
|
||||
{
|
||||
Logger.Verbose($"Fetched Sprite with {m_Sprite.m_PathID} in file {m_Sprite.assetsFile.fileName}, assigning to parent SpriteAtlas...");
|
||||
m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_Sprite.m_SpriteAtlas.IsNull)
|
||||
{
|
||||
m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Sprite.m_SpriteAtlas.TryGet(out var m_SpriteAtlaOld);
|
||||
if (m_SpriteAtlaOld.m_IsVariant)
|
||||
{
|
||||
m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas);
|
||||
}
|
||||
if (m_SpriteAtlaOld.m_IsVariant)
|
||||
{
|
||||
Logger.Verbose($"Fetched Sprite with {m_Sprite.m_PathID} in file {m_Sprite.assetsFile.fileName} has a variant of the origianl SpriteAtlas, disposing of the variant and assinging to the parent SpriteAtlas...");
|
||||
m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
|
||||
namespace AssetStudio
|
||||
{
|
||||
@@ -49,6 +50,20 @@ namespace AssetStudio
|
||||
public uint compressedBlocksInfoSize;
|
||||
public uint uncompressedBlocksInfoSize;
|
||||
public ArchiveFlags flags;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"signature: {signature} | ");
|
||||
sb.Append($"version: {version} | ");
|
||||
sb.Append($"unityVersion: {unityVersion} | ");
|
||||
sb.Append($"unityRevision: {unityRevision} | ");
|
||||
sb.Append($"size: 0x{size:X8} | ");
|
||||
sb.Append($"compressedBlocksInfoSize: 0x{compressedBlocksInfoSize:X8} | ");
|
||||
sb.Append($"uncompressedBlocksInfoSize: 0x{uncompressedBlocksInfoSize:X8} | ");
|
||||
sb.Append($"flags: 0x{(int)flags:X8}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageBlock
|
||||
@@ -56,6 +71,15 @@ namespace AssetStudio
|
||||
public uint compressedSize;
|
||||
public uint uncompressedSize;
|
||||
public StorageBlockFlags flags;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"compressedSize: 0x{compressedSize:X8} | ");
|
||||
sb.Append($"uncompressedSize: 0x{uncompressedSize:X8} | ");
|
||||
sb.Append($"flags: 0x{(int)flags:X8}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class Node
|
||||
@@ -64,6 +88,16 @@ namespace AssetStudio
|
||||
public long size;
|
||||
public uint flags;
|
||||
public string path;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"offset: 0x{offset:X8} | ");
|
||||
sb.Append($"size: 0x{size:X8} | ");
|
||||
sb.Append($"flags: {flags} | ");
|
||||
sb.Append($"path: {path}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private Game Game;
|
||||
@@ -121,6 +155,7 @@ namespace AssetStudio
|
||||
{
|
||||
Header header = new Header();
|
||||
header.signature = reader.ReadStringToNull(20);
|
||||
Logger.Verbose($"Parsed signature {header.signature}");
|
||||
switch (header.signature)
|
||||
{
|
||||
case "UnityFS":
|
||||
@@ -129,6 +164,7 @@ namespace AssetStudio
|
||||
var version = reader.ReadUInt32();
|
||||
if (version > 11)
|
||||
{
|
||||
Logger.Verbose($"Encrypted bundle header with key {version}");
|
||||
XORShift128.InitSeed(version);
|
||||
header.version = 6;
|
||||
header.unityVersion = "5.x.x";
|
||||
@@ -205,6 +241,7 @@ namespace AssetStudio
|
||||
{
|
||||
Stream blocksStream;
|
||||
var uncompressedSizeSum = m_BlocksInfo.Sum(x => x.uncompressedSize);
|
||||
Logger.Verbose($"Total size of decompressed blocks: {uncompressedSizeSum}");
|
||||
if (uncompressedSizeSum >= int.MaxValue)
|
||||
{
|
||||
/*var memoryMappedFile = MemoryMappedFile.CreateNew(null, uncompressedSizeSum);
|
||||
@@ -220,19 +257,17 @@ namespace AssetStudio
|
||||
|
||||
private void ReadBlocksAndDirectory(FileReader reader, Stream blocksStream)
|
||||
{
|
||||
Logger.Verbose($"Writing block and directory to blocks stream...");
|
||||
|
||||
var isCompressed = m_Header.signature == "UnityWeb";
|
||||
foreach (var blockInfo in m_BlocksInfo)
|
||||
{
|
||||
var uncompressedBytes = reader.ReadBytes((int)blockInfo.compressedSize);
|
||||
if (isCompressed)
|
||||
{
|
||||
using (var memoryStream = new MemoryStream(uncompressedBytes))
|
||||
{
|
||||
using (var decompressStream = SevenZipHelper.StreamDecompress(memoryStream))
|
||||
{
|
||||
uncompressedBytes = decompressStream.ToArray();
|
||||
}
|
||||
}
|
||||
using var memoryStream = new MemoryStream(uncompressedBytes);
|
||||
using var decompressStream = SevenZipHelper.StreamDecompress(memoryStream);
|
||||
uncompressedBytes = decompressStream.ToArray();
|
||||
}
|
||||
blocksStream.Write(uncompressedBytes, 0, uncompressedBytes.Length);
|
||||
}
|
||||
@@ -240,6 +275,7 @@ namespace AssetStudio
|
||||
var blocksReader = new EndianBinaryReader(blocksStream);
|
||||
var nodesCount = blocksReader.ReadInt32();
|
||||
m_DirectoryInfo = new Node[nodesCount];
|
||||
Logger.Verbose($"Directory count: {nodesCount}");
|
||||
for (int i = 0; i < nodesCount; i++)
|
||||
{
|
||||
m_DirectoryInfo[i] = new Node
|
||||
@@ -253,6 +289,8 @@ namespace AssetStudio
|
||||
|
||||
public void ReadFiles(Stream blocksStream, string path)
|
||||
{
|
||||
Logger.Verbose($"Writing files from blocks stream...");
|
||||
|
||||
fileList = new StreamFile[m_DirectoryInfo.Length];
|
||||
for (int i = 0; i < m_DirectoryInfo.Length; i++)
|
||||
{
|
||||
@@ -286,6 +324,7 @@ namespace AssetStudio
|
||||
m_Header.uncompressedBlocksInfoSize ^= XORShift128.NextDecryptUInt();
|
||||
m_Header.compressedBlocksInfoSize ^= XORShift128.NextDecryptUInt();
|
||||
XORShift128.Init = false;
|
||||
Logger.Verbose($"Bundle header decrypted");
|
||||
}
|
||||
|
||||
private void ReadHeader(FileReader reader)
|
||||
@@ -317,10 +356,13 @@ namespace AssetStudio
|
||||
m_Header.compressedBlocksInfoSize -= 0xCA;
|
||||
m_Header.uncompressedBlocksInfoSize -= 0xCA;
|
||||
}
|
||||
|
||||
Logger.Verbose($"Bundle header Info: {m_Header}");
|
||||
}
|
||||
|
||||
private void ReadUnityCN(EndianBinaryReader reader)
|
||||
private void ReadUnityCN(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with UnityCN encryption");
|
||||
ArchiveFlags mask;
|
||||
|
||||
var version = ParseVersion();
|
||||
@@ -339,8 +381,11 @@ namespace AssetStudio
|
||||
HasBlockInfoNeedPaddingAtStart = true;
|
||||
}
|
||||
|
||||
Logger.Verbose($"Mask set to 0x{mask:X8}");
|
||||
|
||||
if ((m_Header.flags & mask) != 0)
|
||||
{
|
||||
Logger.Verbose($"Encryption flag exist, file is encrypted, attempting to decrypt");
|
||||
UnityCN = new UnityCN(reader);
|
||||
}
|
||||
}
|
||||
@@ -367,6 +412,7 @@ namespace AssetStudio
|
||||
var blocksInfoBytesSpan = blocksInfoBytes.AsSpan();
|
||||
var uncompressedSize = m_Header.uncompressedBlocksInfoSize;
|
||||
var compressionType = (CompressionType)(m_Header.flags & ArchiveFlags.CompressionTypeMask);
|
||||
Logger.Verbose($"BlockInfo compression type: {compressionType}");
|
||||
switch (compressionType) //kArchiveCompressionTypeMask
|
||||
{
|
||||
case CompressionType.None: //None
|
||||
@@ -399,6 +445,7 @@ namespace AssetStudio
|
||||
case CompressionType.Lz4Mr0k: //Lz4Mr0k
|
||||
if (Mr0kUtils.IsMr0k(blocksInfoBytesSpan))
|
||||
{
|
||||
Logger.Verbose($"Header encrypted with mr0k, decrypting...");
|
||||
blocksInfoBytesSpan = Mr0kUtils.Decrypt(blocksInfoBytesSpan, (Mr0k)Game).ToArray();
|
||||
}
|
||||
goto case CompressionType.Lz4HC;
|
||||
@@ -413,6 +460,7 @@ namespace AssetStudio
|
||||
}
|
||||
var blocksInfoCount = blocksInfoReader.ReadInt32();
|
||||
m_BlocksInfo = new StorageBlock[blocksInfoCount];
|
||||
Logger.Verbose($"Blocks count: {blocksInfoCount}");
|
||||
for (int i = 0; i < blocksInfoCount; i++)
|
||||
{
|
||||
m_BlocksInfo[i] = new StorageBlock
|
||||
@@ -421,10 +469,13 @@ namespace AssetStudio
|
||||
compressedSize = blocksInfoReader.ReadUInt32(),
|
||||
flags = (StorageBlockFlags)blocksInfoReader.ReadUInt16()
|
||||
};
|
||||
|
||||
Logger.Verbose($"Block {i} Info: {m_BlocksInfo[i]}");
|
||||
}
|
||||
|
||||
var nodesCount = blocksInfoReader.ReadInt32();
|
||||
m_DirectoryInfo = new Node[nodesCount];
|
||||
Logger.Verbose($"Directory count: {nodesCount}");
|
||||
for (int i = 0; i < nodesCount; i++)
|
||||
{
|
||||
m_DirectoryInfo[i] = new Node
|
||||
@@ -434,6 +485,8 @@ namespace AssetStudio
|
||||
flags = blocksInfoReader.ReadUInt32(),
|
||||
path = blocksInfoReader.ReadStringToNull(),
|
||||
};
|
||||
|
||||
Logger.Verbose($"Directory {i} Info: {m_DirectoryInfo[i]}");
|
||||
}
|
||||
}
|
||||
if (HasBlockInfoNeedPaddingAtStart && (m_Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0)
|
||||
@@ -444,10 +497,14 @@ namespace AssetStudio
|
||||
|
||||
private void ReadBlocks(FileReader reader, Stream blocksStream)
|
||||
{
|
||||
Logger.Verbose($"Writing block to blocks stream...");
|
||||
|
||||
for (int i = 0; i < m_BlocksInfo.Length; i++)
|
||||
{
|
||||
Logger.Verbose($"Reading block {i}...");
|
||||
var blockInfo = m_BlocksInfo[i];
|
||||
var compressionType = (CompressionType)(blockInfo.flags & StorageBlockFlags.CompressionTypeMask);
|
||||
Logger.Verbose($"Block compression type {compressionType}");
|
||||
switch (compressionType) //kStorageBlockCompressionTypeMask
|
||||
{
|
||||
case CompressionType.None: //None
|
||||
@@ -470,10 +527,12 @@ namespace AssetStudio
|
||||
var compressedBytesSpan = compressedBytes.AsSpan(0, compressedSize);
|
||||
if (compressionType == CompressionType.Lz4Mr0k && Mr0kUtils.IsMr0k(compressedBytes))
|
||||
{
|
||||
Logger.Verbose($"Block encrypted with mr0k, decrypting...");
|
||||
compressedBytesSpan = Mr0kUtils.Decrypt(compressedBytesSpan, (Mr0k)Game);
|
||||
}
|
||||
if (Game.Type.IsUnityCN() && ((int)blockInfo.flags & 0x100) != 0)
|
||||
{
|
||||
Logger.Verbose($"Decrypting block with UnityCN...");
|
||||
UnityCN.DecryptBlock(compressedBytes, compressedSize, i);
|
||||
}
|
||||
if (Game.Type.IsNetEase() && i == 0)
|
||||
|
||||
@@ -965,6 +965,8 @@ namespace AssetStudio
|
||||
|
||||
public sealed class AnimationClip : NamedObject
|
||||
{
|
||||
public static bool Parsable;
|
||||
|
||||
public AnimationType m_AnimationType;
|
||||
public bool m_Legacy;
|
||||
public bool m_Compressed;
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace AssetStudio
|
||||
serializedType = reader.serializedType;
|
||||
byteSize = reader.byteSize;
|
||||
|
||||
Logger.Verbose($"Attempting to read object {type} with {m_PathID} in file {assetsFile.fileName}, starting from offset 0x{reader.byteStart:X8} with expected size of 0x{byteSize:X8} !!");
|
||||
|
||||
if (platform == BuildTarget.NoTarget)
|
||||
{
|
||||
var m_ObjectHideFlags = reader.ReadUInt32();
|
||||
@@ -81,6 +83,7 @@ namespace AssetStudio
|
||||
|
||||
public byte[] GetRawData()
|
||||
{
|
||||
Logger.Verbose($"Dumping raw bytes of the object with {m_PathID} in file {assetsFile.fileName}...");
|
||||
reader.Reset();
|
||||
return reader.ReadBytes((int)byteSize);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace AssetStudio
|
||||
var m_ReadAllowed = reader.ReadBoolean();
|
||||
}
|
||||
}
|
||||
if (version[0] > 2018 || (version[0] == 2018 && version[1] >= 2) || reader.Game.Type.IsGI()) //2018.2 and up
|
||||
if (version[0] > 2018 || (version[0] == 2018 && version[1] >= 2)) //2018.2 and up
|
||||
{
|
||||
var m_StreamingMipmaps = reader.ReadBoolean();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AssetStudio
|
||||
{
|
||||
@@ -8,16 +10,20 @@ namespace AssetStudio
|
||||
private const int DataOffset = 0x2A;
|
||||
private const int KeySize = 0x1000;
|
||||
private const int SeedBlockSize = 0x800;
|
||||
private const int BufferSize = 0x10000;
|
||||
|
||||
public static XORStream Decrypt(FileReader reader, Blk blk)
|
||||
{
|
||||
reader.Endian = EndianType.LittleEndian;
|
||||
|
||||
var signature = reader.ReadStringToNull();
|
||||
Logger.Verbose($"Signature: {signature}");
|
||||
var count = reader.ReadInt32();
|
||||
Logger.Verbose($"Key size: {count}");
|
||||
var key = reader.ReadBytes(count);
|
||||
reader.Position += count;
|
||||
var seedSize = Math.Min(reader.ReadInt16(), blk.SBox.IsNullOrEmpty() ? SeedBlockSize : SeedBlockSize * 2);
|
||||
Logger.Verbose($"Seed size: 0x{seedSize:X8}");
|
||||
|
||||
if (!blk.SBox.IsNullOrEmpty() && blk.Type.IsGI())
|
||||
{
|
||||
@@ -47,6 +53,8 @@ namespace AssetStudio
|
||||
var keyHigh = BinaryPrimitives.ReadUInt64LittleEndian(key.AsSpan(8, 8));
|
||||
var seed = keyLow ^ keyHigh ^ keySeed ^ blk.InitSeed;
|
||||
|
||||
Logger.Verbose($"Seed: 0x{seed:X8}");
|
||||
|
||||
var mt64 = new MT19937_64(seed);
|
||||
var xorpad = new byte[KeySize];
|
||||
for (int i = 0; i < KeySize; i += 8)
|
||||
@@ -56,5 +64,48 @@ namespace AssetStudio
|
||||
|
||||
return new XORStream(reader.BaseStream, DataOffset, xorpad);
|
||||
}
|
||||
|
||||
public static IEnumerable<long> GetOffsets(this XORStream stream, string path)
|
||||
{
|
||||
if (AssetsHelper.TryGet(path, out var offsets))
|
||||
{
|
||||
foreach(var offset in offsets)
|
||||
{
|
||||
stream.Offset = offset;
|
||||
yield return offset;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = new FileReader(path, stream, true);
|
||||
var signature = reader.FileType switch
|
||||
{
|
||||
FileType.BundleFile => "UnityFS\x00",
|
||||
FileType.Mhy0File => "mhy0",
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
Logger.Verbose($"Prased signature: {signature}");
|
||||
|
||||
var signatureBytes = Encoding.UTF8.GetBytes(signature);
|
||||
var buffer = BigArrayPool<byte>.Shared.Rent(BufferSize);
|
||||
while (stream.Remaining > 0)
|
||||
{
|
||||
var index = 0;
|
||||
var absOffset = stream.AbsolutePosition;
|
||||
var read = stream.Read(buffer);
|
||||
while (index < read)
|
||||
{
|
||||
index = buffer.AsSpan(0, read).Search(signatureBytes, index);
|
||||
if (index == -1) break;
|
||||
var offset = absOffset + index;
|
||||
stream.Offset = offset;
|
||||
yield return offset;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
BigArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace AssetStudio
|
||||
|
||||
var encryptedBlockSize = Math.Min(0x10 * ((data.Length - 0x94) >> 7), BlockSize);
|
||||
|
||||
Logger.Verbose($"Encrypted block size: {encryptedBlockSize}");
|
||||
if (!mr0k.InitVector.IsNullOrEmpty())
|
||||
{
|
||||
for (int i = 0; i < mr0k.InitVector.Length; i++)
|
||||
@@ -47,6 +48,8 @@ namespace AssetStudio
|
||||
var seed2 = BinaryPrimitives.ReadUInt64LittleEndian(key3);
|
||||
var seed = seed2 ^ seed1 ^ (seed1 + (uint)data.Length - 20);
|
||||
|
||||
Logger.Verbose($"Seed: 0x{seed:X8}");
|
||||
|
||||
var encryptedBlock = data.Slice(0x94, encryptedBlockSize);
|
||||
var seedSpan = BitConverter.GetBytes(seed);
|
||||
for (var i = 0; i < encryptedBlockSize; i++)
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace AssetStudio
|
||||
private static readonly byte[] Signature = new byte[] { 0xEE, 0xDD };
|
||||
public static void Decrypt(Span<byte> bytes)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt block with NetEase encryption...");
|
||||
|
||||
var (encryptedOffset, encryptedSize) = ReadHeader(bytes);
|
||||
var encrypted = bytes.Slice(encryptedOffset, encryptedSize);
|
||||
var encryptedInts = MemoryMarshal.Cast<byte, int>(encrypted);
|
||||
@@ -77,7 +79,7 @@ namespace AssetStudio
|
||||
}
|
||||
private static (int, int) ReadHeader(Span<byte> bytes)
|
||||
{
|
||||
var index = bytes.Search(Signature, 0);
|
||||
var index = bytes.Search(Signature);
|
||||
if (index == -1 || index >= 0x40)
|
||||
{
|
||||
throw new Exception("Header not found !!");
|
||||
@@ -127,6 +129,7 @@ namespace AssetStudio
|
||||
throw new Exception("Unsupported version");
|
||||
}
|
||||
var versionString = version.ToString("X4");
|
||||
Logger.Verbose($"Bundle version: {versionString}");
|
||||
Encoding.UTF8.GetBytes(versionString, bytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace AssetStudio
|
||||
|
||||
public static void Decrypt(Span<byte> data, string path)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt block with OPFP encryption...");
|
||||
if (IsEncryptionBundle(path, out var key, out var version))
|
||||
{
|
||||
switch (version)
|
||||
@@ -41,30 +42,39 @@ namespace AssetStudio
|
||||
{
|
||||
if (V1_Prefixes.Any(prefix => relativePath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Logger.Verbose("Path matches with V1 prefixes, generatring key...");
|
||||
key = (byte)Path.GetFileName(relativePath).Length;
|
||||
version = 1;
|
||||
Logger.Verbose($"version: {version}, key: {key}");
|
||||
return true;
|
||||
}
|
||||
else if (V0_Prefixes.Any(prefix => relativePath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Logger.Verbose("Path matches with V2 prefixes, generatring key...");
|
||||
|
||||
key = (byte)relativePath.Length;
|
||||
version = 0;
|
||||
Logger.Verbose($"version: {version}, key: {key}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Logger.Verbose($"Unknown encryption type");
|
||||
key = 0x00;
|
||||
version = 0;
|
||||
return false;
|
||||
}
|
||||
private static bool IsFixedPath(string path, out string fixedPath)
|
||||
{
|
||||
Logger.Verbose($"Fixing path before checking...");
|
||||
var dirs = path.Split(Path.DirectorySeparatorChar);
|
||||
if (dirs.Contains(BaseFolder))
|
||||
{
|
||||
var idx = Array.IndexOf(dirs, BaseFolder);
|
||||
Logger.Verbose($"Seperator found at index {idx}");
|
||||
fixedPath = string.Join(Path.DirectorySeparatorChar, dirs[(idx+1)..]).Replace("\\", "/");
|
||||
return true;
|
||||
}
|
||||
Logger.Verbose($"Unknown path");
|
||||
fixedPath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,11 @@ namespace AssetStudio
|
||||
DecryptKey(signatureKey, signatureBytes);
|
||||
|
||||
var str = Encoding.UTF8.GetString(signatureBytes);
|
||||
Logger.Verbose($"Decrypted signature is {str}");
|
||||
if (str != Signature)
|
||||
throw new Exception("Invalid Signature !!");
|
||||
{
|
||||
throw new Exception($"Invalid Signature, Expected {Signature} but found {str} instead");
|
||||
}
|
||||
|
||||
DecryptKey(infoKey, infoBytes);
|
||||
|
||||
@@ -41,19 +44,20 @@ namespace AssetStudio
|
||||
var idx = (i % 4 * 4) + (i / 4);
|
||||
Sub[idx] = subBytes[i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static bool SetKey(Entry entry)
|
||||
{
|
||||
Logger.Verbose($"Initializing decryptor with key {entry.Key}");
|
||||
try
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Key = Convert.FromHexString(entry.Key);
|
||||
using var aes = Aes.Create();
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Key = Convert.FromHexString(entry.Key);
|
||||
|
||||
Encryptor = aes.CreateEncryptor();
|
||||
}
|
||||
Encryptor = aes.CreateEncryptor();
|
||||
Logger.Verbose($"Decryptor initialized !!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace AssetStudio
|
||||
return buffer;
|
||||
}
|
||||
public static int Search(this byte[] src, string value, int offset = 0) => Search(src.AsSpan(), Encoding.UTF8.GetBytes(value), offset);
|
||||
public static int Search(this Span<byte> src, byte[] pattern, int offset)
|
||||
public static int Search(this Span<byte> src, byte[] pattern, int offset = 0)
|
||||
{
|
||||
int maxFirstCharSlot = src.Length - pattern.Length + 1;
|
||||
for (int i = offset; i < maxFirstCharSlot; i++)
|
||||
|
||||
@@ -13,5 +13,15 @@ namespace AssetStudio
|
||||
|
||||
//custom
|
||||
public string fileName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"Guid: {guid} | ");
|
||||
sb.Append($"type: {type} | ");
|
||||
sb.Append($"pathName: {pathName} | ");
|
||||
sb.Append($"fileName: {fileName}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,14 @@ namespace AssetStudio
|
||||
FullPath = Path.GetFullPath(path);
|
||||
FileName = Path.GetFileName(path);
|
||||
FileType = CheckFileType();
|
||||
Logger.Verbose($"File {path} type is {FileType}");
|
||||
}
|
||||
|
||||
private FileType CheckFileType()
|
||||
{
|
||||
var signature = this.ReadStringToNull(20);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {signature}");
|
||||
switch (signature)
|
||||
{
|
||||
case "ENCR":
|
||||
@@ -47,46 +49,59 @@ namespace AssetStudio
|
||||
return FileType.BlkFile;
|
||||
default:
|
||||
{
|
||||
Logger.Verbose("signature does not match any of the supported string signatures, attempting to check bytes signatures");
|
||||
byte[] magic = ReadBytes(2);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {Convert.ToHexString(magic)}");
|
||||
if (gzipMagic.SequenceEqual(magic))
|
||||
{
|
||||
return FileType.GZipFile;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(gzipMagic)}");
|
||||
Position = 0x20;
|
||||
magic = ReadBytes(6);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {Convert.ToHexString(magic)}");
|
||||
if (brotliMagic.SequenceEqual(magic))
|
||||
{
|
||||
return FileType.BrotliFile;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(brotliMagic)}");
|
||||
if (IsSerializedFile())
|
||||
{
|
||||
return FileType.AssetsFile;
|
||||
}
|
||||
magic = ReadBytes(4);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {Convert.ToHexString(magic)}");
|
||||
if (zipMagic.SequenceEqual(magic) || zipSpannedMagic.SequenceEqual(magic))
|
||||
{
|
||||
return FileType.ZipFile;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(zipMagic)} or {Convert.ToHexString(zipSpannedMagic)}");
|
||||
if (mhy0Magic.SequenceEqual(magic))
|
||||
{
|
||||
return FileType.Mhy0File;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(mhy0Magic)}");
|
||||
magic = ReadBytes(7);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {Convert.ToHexString(magic)}");
|
||||
if (narakaMagic.SequenceEqual(magic))
|
||||
{
|
||||
return FileType.BundleFile;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(narakaMagic)}");
|
||||
magic = ReadBytes(9);
|
||||
Position = 0;
|
||||
Logger.Verbose($"Parsed signature is {Convert.ToHexString(magic)}");
|
||||
if (gunfireMagic.SequenceEqual(magic))
|
||||
{
|
||||
Position = 0x32;
|
||||
return FileType.BundleFile;
|
||||
}
|
||||
Logger.Verbose($"Parsed signature does not match with expected signature {Convert.ToHexString(gunfireMagic)}");
|
||||
Logger.Verbose($"Parsed signature does not match any of the supported signatures, assuming resource file");
|
||||
return FileType.ResourceFile;
|
||||
}
|
||||
}
|
||||
@@ -94,9 +109,12 @@ namespace AssetStudio
|
||||
|
||||
private bool IsSerializedFile()
|
||||
{
|
||||
Logger.Verbose($"Attempting to check if the file is serialized file...");
|
||||
|
||||
var fileSize = BaseStream.Length;
|
||||
if (fileSize < 20)
|
||||
{
|
||||
Logger.Verbose($"File size 0x{fileSize:X8} is too small, minimal acceptable size is 0x14, aborting...");
|
||||
return false;
|
||||
}
|
||||
var m_MetadataSize = ReadUInt32();
|
||||
@@ -109,6 +127,7 @@ namespace AssetStudio
|
||||
{
|
||||
if (fileSize < 48)
|
||||
{
|
||||
Logger.Verbose($"File size 0x{fileSize:X8} for version {m_Version} is too small, minimal acceptable size is 0x30, aborting...");
|
||||
Position = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -119,12 +138,15 @@ namespace AssetStudio
|
||||
Position = 0;
|
||||
if (m_FileSize != fileSize)
|
||||
{
|
||||
Logger.Verbose($"Parsed file size 0x{m_FileSize:X8} does not match stream size {fileSize}, file might be corrupted, aborting...");
|
||||
return false;
|
||||
}
|
||||
if (m_DataOffset > fileSize)
|
||||
{
|
||||
Logger.Verbose($"Parsed data offset 0x{m_DataOffset:X8} is outside the stream of the size {fileSize}, file might be corrupted, aborting...");
|
||||
return false;
|
||||
}
|
||||
Logger.Verbose($"Valid serialized file !!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -133,8 +155,10 @@ namespace AssetStudio
|
||||
{
|
||||
public static FileReader PreProcessing(this FileReader reader, Game game)
|
||||
{
|
||||
Logger.Verbose($"Applying preprocessing to file {reader.FileName}");
|
||||
if (reader.FileType == FileType.ResourceFile || !game.Type.IsNormal())
|
||||
{
|
||||
Logger.Verbose("File is encrypted !!");
|
||||
switch (game.Type)
|
||||
{
|
||||
case GameType.GI_Pack:
|
||||
@@ -147,16 +171,13 @@ namespace AssetStudio
|
||||
reader = DecryptEnsembleStar(reader);
|
||||
break;
|
||||
case GameType.OPFP:
|
||||
reader = ParseOPFP(reader);
|
||||
break;
|
||||
case GameType.FakeHeader:
|
||||
case GameType.ShiningNikki:
|
||||
reader = ParseFakeHeader(reader);
|
||||
break;
|
||||
case GameType.FantasyOfWind:
|
||||
reader = DecryptFantasyOfWind(reader);
|
||||
break;
|
||||
case GameType.ShiningNikki:
|
||||
reader = ParseShiningNikki(reader);
|
||||
break;
|
||||
case GameType.HelixWaltz2:
|
||||
reader = ParseHelixWaltz2(reader);
|
||||
@@ -174,6 +195,7 @@ namespace AssetStudio
|
||||
}
|
||||
if (reader.FileType == FileType.BundleFile && game.Type.IsBlockFile())
|
||||
{
|
||||
Logger.Verbose("File might have multiple bundles !!");
|
||||
try
|
||||
{
|
||||
var signature = reader.ReadStringToNull();
|
||||
@@ -183,6 +205,8 @@ namespace AssetStudio
|
||||
var size = reader.ReadInt64();
|
||||
if (!(signature == "UnityFS" && size == reader.BaseStream.Length))
|
||||
{
|
||||
Logger.Verbose($"Found signature UnityFS, expected bundle size is 0x{size:X8}, found 0x{reader.BaseStream.Length} instead !!");
|
||||
Logger.Verbose("Loading as block file !!");
|
||||
reader.FileType = FileType.BlockFile;
|
||||
}
|
||||
}
|
||||
@@ -190,6 +214,7 @@ namespace AssetStudio
|
||||
reader.Position = 0;
|
||||
}
|
||||
|
||||
Logger.Verbose("No preprocessing is needed");
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace AssetStudio
|
||||
public class ImportedFrame
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Vector3 LocalRotation { get; set; }
|
||||
public Quaternion LocalRotation { get; set; }
|
||||
public Vector3 LocalPosition { get; set; }
|
||||
public Vector3 LocalScale { get; set; }
|
||||
public ImportedFrame Parent { get; set; }
|
||||
@@ -248,7 +248,7 @@ namespace AssetStudio
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public List<ImportedKeyframe<Vector3>> Scalings = new List<ImportedKeyframe<Vector3>>();
|
||||
public List<ImportedKeyframe<Vector3>> Rotations = new List<ImportedKeyframe<Vector3>>();
|
||||
public List<ImportedKeyframe<Quaternion>> Rotations = new List<ImportedKeyframe<Quaternion>>();
|
||||
public List<ImportedKeyframe<Vector3>> Translations = new List<ImportedKeyframe<Vector3>>();
|
||||
public ImportedBlendShape BlendShape;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -34,4 +35,39 @@ namespace AssetStudio
|
||||
Console.WriteLine("[{0}] {1}", loggerEvent, message);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FileLogger : ILogger
|
||||
{
|
||||
private const string LogFileName = "log.txt";
|
||||
private const string PrevLogFileName = "log_prev.txt";
|
||||
private readonly object LockWriter = new object();
|
||||
|
||||
public StreamWriter Writer;
|
||||
|
||||
public FileLogger()
|
||||
{
|
||||
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LogFileName);
|
||||
var prevLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PrevLogFileName);
|
||||
|
||||
if (File.Exists(logPath))
|
||||
{
|
||||
File.Move(logPath, prevLogPath, true);
|
||||
}
|
||||
Writer = new StreamWriter(logPath, true) { AutoFlush = true };
|
||||
}
|
||||
~FileLogger()
|
||||
{
|
||||
Writer?.Dispose();
|
||||
}
|
||||
public void Log(LoggerEvent loggerEvent, string message, bool silent = false)
|
||||
{
|
||||
if (silent)
|
||||
return;
|
||||
|
||||
lock (LockWriter)
|
||||
{
|
||||
Writer.WriteLine($"[{DateTime.Now}][{loggerEvent}] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ namespace AssetStudio
|
||||
{
|
||||
public static void MergeSplitAssets(string path, bool allDirectories = false)
|
||||
{
|
||||
Logger.Verbose($"Processing split assets (.splitX) prior to loading files...");
|
||||
var splitFiles = Directory.GetFiles(path, "*.split0", allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
Logger.Verbose($"Found {splitFiles.Length} split files, attempting to merge...");
|
||||
foreach (var splitFile in splitFiles)
|
||||
{
|
||||
var destFile = Path.GetFileNameWithoutExtension(splitFile);
|
||||
@@ -27,6 +29,7 @@ namespace AssetStudio
|
||||
if (!File.Exists(destFull))
|
||||
{
|
||||
var splitParts = Directory.GetFiles(destPath, destFile + ".split*");
|
||||
Logger.Verbose($"Creating {destFull} where split files will be combined");
|
||||
using (var destStream = File.Create(destFull))
|
||||
{
|
||||
for (int i = 0; i < splitParts.Length; i++)
|
||||
@@ -35,6 +38,7 @@ namespace AssetStudio
|
||||
using (var sourceStream = File.OpenRead(splitPart))
|
||||
{
|
||||
sourceStream.CopyTo(destStream);
|
||||
Logger.Verbose($"{splitPart} has been combined into {destFull}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +48,7 @@ namespace AssetStudio
|
||||
|
||||
public static string[] ProcessingSplitFiles(List<string> selectFile)
|
||||
{
|
||||
Logger.Verbose("Filter out paths that has .split and has the same name");
|
||||
var splitFiles = selectFile.Where(x => x.Contains(".split"))
|
||||
.Select(x => Path.Combine(Path.GetDirectoryName(x), Path.GetFileNameWithoutExtension(x)))
|
||||
.Distinct()
|
||||
@@ -61,6 +66,7 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecompressGZip(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Decompressing GZip file {reader.FileName} into memory");
|
||||
using (reader)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
@@ -75,6 +81,7 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecompressBrotli(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Decompressing Brotli file {reader.FileName} into memory");
|
||||
using (reader)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
@@ -89,24 +96,31 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecryptPack(FileReader reader, Game game)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Pack encryption");
|
||||
|
||||
const int PackSize = 0x880;
|
||||
const string PackSignature = "pack";
|
||||
const string UnityFSSignature = "UnityFS";
|
||||
|
||||
var data = reader.ReadBytes((int)reader.Length);
|
||||
var idx = data.Search(PackSignature);
|
||||
if (idx == -1)
|
||||
var packIdx = data.Search(PackSignature);
|
||||
if (packIdx == -1)
|
||||
{
|
||||
Logger.Verbose($"Signature {PackSignature} was not found, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
idx = data.Search("mr0k", idx);
|
||||
if (idx == -1)
|
||||
Logger.Verbose($"Found signature {PackSignature} at offset 0x{packIdx:X8}");
|
||||
var mr0kIdx = data.Search("mr0k", packIdx);
|
||||
if (mr0kIdx == -1)
|
||||
{
|
||||
Logger.Verbose("Signature mr0k was not found, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
Logger.Verbose($"Found signature mr0k signature at offset 0x{mr0kIdx:X8}");
|
||||
|
||||
Logger.Verbose("Attempting to process pack chunks...");
|
||||
var ms = new MemoryStream();
|
||||
try
|
||||
{
|
||||
@@ -121,9 +135,12 @@ namespace AssetStudio
|
||||
var signature = reader.ReadStringToNull(4);
|
||||
if (signature == PackSignature)
|
||||
{
|
||||
Logger.Verbose($"Found {PackSignature} chunk at position {reader.Position - PackSignature.Length}");
|
||||
var isMr0k = reader.ReadBoolean();
|
||||
Logger.Verbose("Chunk is mr0k encrypted");
|
||||
var blockSize = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4));
|
||||
|
||||
Logger.Verbose($"Chunk size is 0x{blockSize:X8}");
|
||||
Span<byte> buffer = new byte[blockSize];
|
||||
reader.Read(buffer);
|
||||
if (isMr0k)
|
||||
@@ -134,6 +151,7 @@ namespace AssetStudio
|
||||
|
||||
if (bundleSize == 0)
|
||||
{
|
||||
Logger.Verbose("This is header chunk !! attempting to read the bundle size");
|
||||
using var blockReader = new EndianBinaryReader(new MemoryStream(buffer.ToArray()));
|
||||
var header = new Header()
|
||||
{
|
||||
@@ -144,17 +162,21 @@ namespace AssetStudio
|
||||
size = blockReader.ReadInt64()
|
||||
};
|
||||
bundleSize = header.size;
|
||||
Logger.Verbose($"Bundle size is 0x{bundleSize:X8}");
|
||||
}
|
||||
|
||||
readSize += buffer.Length;
|
||||
|
||||
if (readSize % (PackSize - 0x80) == 0)
|
||||
{
|
||||
reader.Position += PackSize - 9 - blockSize;
|
||||
var padding = PackSize - 9 - blockSize;
|
||||
reader.Position += padding;
|
||||
Logger.Verbose($"Skip 0x{padding:X8} padding");
|
||||
}
|
||||
|
||||
if (readSize == bundleSize)
|
||||
{
|
||||
Logger.Verbose($"Bundle has been read entirely !!");
|
||||
readSize = 0;
|
||||
bundleSize = 0;
|
||||
}
|
||||
@@ -166,6 +188,7 @@ namespace AssetStudio
|
||||
signature = reader.ReadStringToNull();
|
||||
if (signature == UnityFSSignature)
|
||||
{
|
||||
Logger.Verbose($"Found {UnityFSSignature} chunk at position {reader.Position - (UnityFSSignature.Length + 1)}");
|
||||
var header = new Header()
|
||||
{
|
||||
signature = reader.ReadStringToNull(),
|
||||
@@ -175,6 +198,7 @@ namespace AssetStudio
|
||||
size = reader.ReadInt64()
|
||||
};
|
||||
|
||||
Logger.Verbose($"Bundle size is 0x{header.size:X8}");
|
||||
reader.Position = pos;
|
||||
reader.BaseStream.CopyTo(ms, header.size);
|
||||
continue;
|
||||
@@ -196,15 +220,19 @@ namespace AssetStudio
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted pack file successfully !!");
|
||||
ms.Position = 0;
|
||||
return new FileReader(reader.FullPath, ms);
|
||||
}
|
||||
|
||||
public static FileReader DecryptMark(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Mark encryption");
|
||||
|
||||
var signature = reader.ReadStringToNull(4);
|
||||
if (signature != "mark")
|
||||
{
|
||||
Logger.Verbose($"Expected signature mark, found {signature} instead, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
@@ -240,6 +268,7 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted mark file successfully !!");
|
||||
reader.Dispose();
|
||||
dataStream.Position = 0;
|
||||
return new FileReader(reader.FullPath, dataStream);
|
||||
@@ -247,8 +276,10 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecryptEnsembleStar(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Ensemble Star encryption");
|
||||
if (Path.GetExtension(reader.FileName) != ".z")
|
||||
{
|
||||
Logger.Verbose($"Expected file extension .z, found {Path.GetExtension(reader.FileName)} instead, aborting...");
|
||||
return reader;
|
||||
}
|
||||
using (reader)
|
||||
@@ -269,46 +300,42 @@ namespace AssetStudio
|
||||
data[i] = (byte)(EnsembleStarKey1[k1] ^ ((size ^ EnsembleStarKey3[k3] ^ data[i] ^ EnsembleStarKey2[k2]) + remaining));
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted Ensemble Star file successfully !!");
|
||||
return new FileReader(reader.FullPath, new MemoryStream(data));
|
||||
}
|
||||
}
|
||||
|
||||
public static FileReader ParseOPFP(FileReader reader)
|
||||
{
|
||||
var stream = reader.BaseStream;
|
||||
var data = reader.ReadBytes(0x1000);
|
||||
var idx = data.Search("UnityFS");
|
||||
if (idx != -1)
|
||||
{
|
||||
stream = new OffsetStream(stream, idx);
|
||||
}
|
||||
|
||||
return new FileReader(reader.FullPath, stream);
|
||||
}
|
||||
|
||||
public static FileReader ParseFakeHeader(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to parse file {reader.FileName} with fake header");
|
||||
|
||||
var stream = reader.BaseStream;
|
||||
var data = reader.ReadBytes(0x1000);
|
||||
var idx = data.Search("UnityFS");
|
||||
if (idx != -1)
|
||||
{
|
||||
Logger.Verbose($"Found fake header at offset 0x{idx:X8}");
|
||||
var idx2 = data[(idx + 1)..].Search("UnityFS");
|
||||
if (idx2 != -1)
|
||||
{
|
||||
Logger.Verbose($"Found real header at offset 0x{idx + idx2 + 1:X8}");
|
||||
stream = new OffsetStream(stream, idx + idx2 + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Verbose("Real header was not found, assuming fake header is the real one");
|
||||
stream = new OffsetStream(stream, idx);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Verbose("Parsed fake header file successfully !!");
|
||||
return new FileReader(reader.FullPath, stream);
|
||||
}
|
||||
|
||||
public static FileReader DecryptFantasyOfWind(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Fantasy of Wind encryption");
|
||||
|
||||
byte[] encryptKeyName = Encoding.UTF8.GetBytes("28856");
|
||||
const int MinLength = 0xC8;
|
||||
const int KeyLength = 8;
|
||||
@@ -319,6 +346,7 @@ namespace AssetStudio
|
||||
var signature = reader.ReadStringToNull(HeadLength);
|
||||
if (string.Compare(signature, "K9999") > 0 || reader.Length <= MinLength)
|
||||
{
|
||||
Logger.Verbose($"Signature version {signature} is higher than K9999 or stream length {reader.Length} is less than minimum length {MinLength}, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
@@ -360,23 +388,13 @@ namespace AssetStudio
|
||||
reader.BaseStream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
Logger.Verbose("Decrypted Fantasy of Wind file successfully !!");
|
||||
return new FileReader(reader.FullPath, ms);
|
||||
}
|
||||
|
||||
public static FileReader ParseShiningNikki(FileReader reader)
|
||||
{
|
||||
var data = reader.ReadBytes(0x1000);
|
||||
var idx = data.Search("UnityFS");
|
||||
if (idx == -1)
|
||||
{
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
var stream = new OffsetStream(reader.BaseStream, idx);
|
||||
return new FileReader(reader.FullPath, stream);
|
||||
}
|
||||
public static FileReader ParseHelixWaltz2(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Helix Waltz 2 encryption");
|
||||
|
||||
var originalHeader = new byte[] { 0x55, 0x6E, 0x69, 0x74, 0x79, 0x46, 0x53, 0x00, 0x00, 0x00, 0x00, 0x07, 0x35, 0x2E, 0x78, 0x2E };
|
||||
|
||||
var signature = reader.ReadStringToNull();
|
||||
@@ -384,6 +402,7 @@ namespace AssetStudio
|
||||
|
||||
if (signature != "SzxFS")
|
||||
{
|
||||
Logger.Verbose($"Expected signature SzxFS, found {signature} instead, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
@@ -415,6 +434,7 @@ namespace AssetStudio
|
||||
data[i] = key[idx];
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted Helix Waltz 2 file successfully !!");
|
||||
MemoryStream ms = new();
|
||||
ms.Write(originalHeader);
|
||||
ms.Write(data);
|
||||
@@ -424,6 +444,8 @@ namespace AssetStudio
|
||||
}
|
||||
public static FileReader DecryptAnchorPanic(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Anchor Panic encryption");
|
||||
|
||||
const int BlockSize = 0x800;
|
||||
|
||||
var data = reader.ReadBytes(0x1000);
|
||||
@@ -432,25 +454,31 @@ namespace AssetStudio
|
||||
var idx = data.Search("UnityFS");
|
||||
if (idx != -1)
|
||||
{
|
||||
Logger.Verbose("Found UnityFS signature, file might not be encrypted");
|
||||
return ParseFakeHeader(reader);
|
||||
}
|
||||
|
||||
var key = GetKey(Path.GetFileNameWithoutExtension(reader.FileName));
|
||||
Logger.Verbose($"Calculated key is {key}");
|
||||
|
||||
var chunkIndex = 0;
|
||||
MemoryStream ms = new();
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
var chunkSize = Math.Min((int)reader.Remaining, BlockSize);
|
||||
Logger.Verbose($"Chunk {chunkIndex} size is {chunkSize}");
|
||||
var chunk = reader.ReadBytes(chunkSize);
|
||||
if (IsEncrypt((int)reader.Length, chunkIndex++))
|
||||
{
|
||||
Logger.Verbose($"Chunk {chunkIndex} is encrypted, decrypting...");
|
||||
RC4(chunk, key);
|
||||
}
|
||||
|
||||
ms.Write(chunk);
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted Anchor Panic file successfully !!");
|
||||
ms.Position = 0;
|
||||
|
||||
return new FileReader(reader.FullPath, ms);
|
||||
|
||||
bool IsEncrypt(int fileSize, int chunkIndex)
|
||||
@@ -533,11 +561,14 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecryptDreamscapeAlbireo(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Dreamscape Albireo encryption");
|
||||
|
||||
var key = new byte[] { 0x1E, 0x1E, 0x01, 0x01, 0xFC };
|
||||
|
||||
var signature = reader.ReadStringToNull(4);
|
||||
if (signature != "MJJ")
|
||||
{
|
||||
Logger.Verbose($"Expected signature MJJ, found {signature} instead, aborting...");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
@@ -559,6 +590,8 @@ namespace AssetStudio
|
||||
var sizeLow = (u5 >> 24 | (u2 << 8)) ^ u1;
|
||||
var size = (long)(sizeHigh << 32 | sizeLow);
|
||||
|
||||
Logger.Verbose($"Decrypted File info: Flag 0x{flag:X8} | Compressed blockInfo size 0x{compressedBlocksInfoSize:X8} | Decompressed blockInfo size 0x{uncompressedBlocksInfoSize:X8} | Bundle size 0x{size:X8}");
|
||||
|
||||
var blocksInfo = reader.ReadBytes((int)compressedBlocksInfoSize);
|
||||
for(int i = 0; i < blocksInfo.Length; i++)
|
||||
{
|
||||
@@ -587,6 +620,7 @@ namespace AssetStudio
|
||||
reader.BaseStream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
Logger.Verbose("Decrypted Dreamscape Albireo file successfully !!");
|
||||
return new FileReader(reader.FullPath, ms);
|
||||
|
||||
static uint Scrample(uint value) => (value >> 5) & 0xFFE000 | (value >> 29) | (value << 14) & 0xFF000000 | (8 * value) & 0x1FF8;
|
||||
@@ -594,6 +628,8 @@ namespace AssetStudio
|
||||
|
||||
public static FileReader DecryptImaginaryFest(FileReader reader)
|
||||
{
|
||||
Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Imaginary Fest encryption");
|
||||
|
||||
const string dataRoot = "data";
|
||||
var key = new byte[] { 0xBD, 0x65, 0xF2, 0x4F, 0xBE, 0xD1, 0x36, 0xD4, 0x95, 0xFE, 0x64, 0x94, 0xCB, 0xD3, 0x7E, 0x91, 0x57, 0xB7, 0x94, 0xB7, 0x9F, 0x70, 0xB2, 0xA9, 0xA0, 0xD5, 0x4E, 0x36, 0xC6, 0x40, 0xE0, 0x2F, 0x4E, 0x6E, 0x76, 0x6D, 0xCD, 0xAE, 0xEA, 0x05, 0x13, 0x6B, 0xA7, 0x84, 0xFF, 0xED, 0x90, 0x91, 0x15, 0x7E, 0xF1, 0xF8, 0xA5, 0x9C, 0xB6, 0xDE, 0xF9, 0x56, 0x57, 0x18, 0xBF, 0x94, 0x63, 0x6F, 0x1B, 0xE2, 0x92, 0xD2, 0x7E, 0x25, 0x95, 0x23, 0x24, 0xCB, 0x93, 0xD3, 0x36, 0xD9, 0x18, 0x11, 0xF5, 0x50, 0x18, 0xE4, 0x22, 0x28, 0xD8, 0xE2, 0x1A, 0x57, 0x1E, 0x04, 0x88, 0xA5, 0x84, 0xC0, 0x6C, 0x3B, 0x46, 0x62, 0xCE, 0x85, 0x10, 0x2E, 0xA0, 0xDC, 0xD3, 0x09, 0xB2, 0xB6, 0xA4, 0x8D, 0xAF, 0x74, 0x36, 0xF7, 0x9A, 0x3F, 0x98, 0xDA, 0x62, 0x57, 0x71, 0x75, 0x92, 0x05, 0xA3, 0xB2, 0x7C, 0xCA, 0xFB, 0x1E, 0xBE, 0xC9, 0x24, 0xC1, 0xD2, 0xB9, 0xDE, 0xE4, 0x7E, 0xF3, 0x0F, 0xB4, 0xFB, 0xA2, 0xC1, 0xC2, 0x14, 0x5C, 0x78, 0x13, 0x74, 0x41, 0x8D, 0x79, 0xF4, 0x3C, 0x49, 0x92, 0x98, 0xF2, 0xCD, 0x8C, 0x09, 0xA6, 0x40, 0x34, 0x51, 0x1C, 0x11, 0x2B, 0xE0, 0x6B, 0x42, 0x9C, 0x86, 0x41, 0x06, 0xF6, 0xD2, 0x87, 0xF1, 0x10, 0x26, 0x89, 0xC2, 0x7B, 0x2A, 0x5D, 0x1C, 0xDA, 0x92, 0xC8, 0x93, 0x59, 0xF9, 0x60, 0xD0, 0xB5, 0x1E, 0xD5, 0x75, 0x56, 0xA0, 0x05, 0x83, 0x90, 0xAC, 0x72, 0xC8, 0x10, 0x09, 0xED, 0x1A, 0x46, 0xD9, 0x39, 0x6B, 0x9E, 0x19, 0x5E, 0x51, 0x44, 0x09, 0x0D, 0x74, 0xAB, 0xA8, 0xF9, 0x32, 0x43, 0xBC, 0xD2, 0xED, 0x7B, 0x6C, 0x75, 0x32, 0x24, 0x14, 0x43, 0x5D, 0x98, 0xB2, 0xFC, 0xFB, 0xF5, 0x9A, 0x19, 0x03, 0xB0, 0xB7, 0xAC, 0xAE, 0x8B };
|
||||
|
||||
@@ -601,12 +637,14 @@ namespace AssetStudio
|
||||
var signature = Encoding.UTF8.GetString(signatureBytes[..7]);
|
||||
if (signature == "UnityFS")
|
||||
{
|
||||
Logger.Verbose("Found UnityFS signature, file might not be encrypted");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
}
|
||||
|
||||
if (signatureBytes[7] != 0)
|
||||
{
|
||||
Logger.Verbose($"File might be encrypted with a byte xorkey 0x{signatureBytes[7]:X8}, attemping to decrypting...");
|
||||
var xorKey = signatureBytes[7];
|
||||
for (int i = 0; i < signatureBytes.Length; i++)
|
||||
{
|
||||
@@ -615,12 +653,14 @@ namespace AssetStudio
|
||||
signature = Encoding.UTF8.GetString(signatureBytes[..7]);
|
||||
if (signature == "UnityFS")
|
||||
{
|
||||
Logger.Verbose("Found UnityFS signature, key is valid, decrypting the rest of the stream");
|
||||
var remaining = reader.ReadBytes((int)reader.Remaining);
|
||||
for (int i = 0; i < remaining.Length; i++)
|
||||
{
|
||||
remaining[i] ^= xorKey;
|
||||
}
|
||||
|
||||
Logger.Verbose("Decrypted Imaginary Fest file successfully !!");
|
||||
var stream = new MemoryStream();
|
||||
stream.Write(signatureBytes);
|
||||
stream.Write(remaining);
|
||||
@@ -635,25 +675,33 @@ namespace AssetStudio
|
||||
var startIdx = Array.FindIndex(paths, x => x == dataRoot);
|
||||
if (startIdx != -1 && startIdx != paths.Length - 1)
|
||||
{
|
||||
Logger.Verbose("File is in the data folder !!");
|
||||
var path = string.Join(Path.AltDirectorySeparatorChar, paths[(startIdx+1)..]);
|
||||
var offset = GetLoadAssetBundleOffset(path);
|
||||
if (offset > 0 && offset < reader.Length)
|
||||
{
|
||||
Logger.Verbose($"Calculated offset is 0x{offset:X8}, attempting to read signature...");
|
||||
reader.Position = offset;
|
||||
signature = reader.ReadStringToNull(7);
|
||||
if (signature == "UnityFS")
|
||||
{
|
||||
Logger.Verbose($"Found UnityFS signature, file starts at 0x{offset:X8} !!");
|
||||
Logger.Verbose("Parsed Imaginary Fest file successfully !!");
|
||||
reader.Position = offset;
|
||||
return new FileReader(reader.FullPath, new MemoryStream(reader.ReadBytes((int)reader.Remaining)));
|
||||
}
|
||||
}
|
||||
Logger.Verbose($"Invalid offset, attempting to generate key...");
|
||||
reader.Position = 0;
|
||||
var data = reader.ReadBytes((int)reader.Remaining);
|
||||
var key_value = GetHashCode(path);
|
||||
Logger.Verbose($"Generated key is 0x{key_value:X8}, decrypting...");
|
||||
Decrypt(data, key_value);
|
||||
Logger.Verbose("Decrypted Imaginary Fest file successfully !!");
|
||||
return new FileReader(reader.FullPath, new MemoryStream(data));
|
||||
}
|
||||
|
||||
Logger.Verbose("File doesn't match any of the encryption types");
|
||||
reader.Position = 0;
|
||||
return reader;
|
||||
|
||||
|
||||
@@ -9,5 +9,13 @@ namespace AssetStudio
|
||||
{
|
||||
public int localSerializedFileIndex;
|
||||
public long localIdentifierInFile;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"localSerializedFileIndex: {localSerializedFileIndex} | ");
|
||||
sb.Append($"localIdentifierInFile: {localIdentifierInFile}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -7,21 +8,79 @@ namespace AssetStudio
|
||||
{
|
||||
public static class Logger
|
||||
{
|
||||
public static ILogger Default = new DummyLogger();
|
||||
public static bool Silent = false;
|
||||
private static bool _fileLogging = true;
|
||||
|
||||
public static void Verbose(string message) => Default.Log(LoggerEvent.Verbose, message, Silent);
|
||||
public static void Debug(string message) => Default.Log(LoggerEvent.Debug, message, Silent);
|
||||
public static void Info(string message) => Default.Log(LoggerEvent.Info, message, Silent);
|
||||
public static void Warning(string message) => Default.Log(LoggerEvent.Warning, message, Silent);
|
||||
public static void Error(string message) => Default.Log(LoggerEvent.Error, message, Silent);
|
||||
public static ILogger Default = new DummyLogger();
|
||||
public static ILogger File;
|
||||
public static bool Silent = false;
|
||||
public static bool LogVerbose = false;
|
||||
|
||||
public static bool FileLogging
|
||||
{
|
||||
get => _fileLogging;
|
||||
set
|
||||
{
|
||||
_fileLogging = value;
|
||||
if (_fileLogging)
|
||||
{
|
||||
File = new FileLogger();
|
||||
}
|
||||
else
|
||||
{
|
||||
((FileLogger)File)?.Writer?.Dispose();
|
||||
File = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Verbose(string message)
|
||||
{
|
||||
if (LogVerbose)
|
||||
{
|
||||
try
|
||||
{
|
||||
var callerMethod = new StackTrace().GetFrame(1).GetMethod();
|
||||
var callerMethodClass = callerMethod.ReflectedType.Name;
|
||||
if (!string.IsNullOrEmpty(callerMethodClass))
|
||||
{
|
||||
message = $"[{callerMethodClass}] {message}";
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
if (FileLogging) File.Log(LoggerEvent.Verbose, message);
|
||||
Default.Log(LoggerEvent.Verbose, message, Silent);
|
||||
}
|
||||
}
|
||||
public static void Debug(string message)
|
||||
{
|
||||
if (FileLogging) File.Log(LoggerEvent.Debug, message);
|
||||
Default.Log(LoggerEvent.Debug, message, Silent);
|
||||
}
|
||||
public static void Info(string message)
|
||||
{
|
||||
if (FileLogging) File.Log(LoggerEvent.Info, message);
|
||||
Default.Log(LoggerEvent.Info, message, Silent);
|
||||
}
|
||||
public static void Warning(string message)
|
||||
{
|
||||
if (FileLogging) File.Log(LoggerEvent.Warning, message);
|
||||
Default.Log(LoggerEvent.Warning, message, Silent);
|
||||
}
|
||||
public static void Error(string message)
|
||||
{
|
||||
if (FileLogging) File.Log(LoggerEvent.Error, message);
|
||||
Default.Log(LoggerEvent.Error, message, Silent);
|
||||
}
|
||||
|
||||
public static void Error(string message, Exception e)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(message);
|
||||
sb.AppendLine(e.ToString());
|
||||
Default.Log(LoggerEvent.Error, sb.ToString(), Silent);
|
||||
|
||||
message = sb.ToString();
|
||||
if (FileLogging) File.Log(LoggerEvent.Error, message);
|
||||
Default.Log(LoggerEvent.Error, message, Silent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace AssetStudio
|
||||
return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z) && W.Equals(other.W);
|
||||
}
|
||||
|
||||
public static Quaternion Zero => new Quaternion(0, 0, 0, 1);
|
||||
public static float Dot(Quaternion a, Quaternion b)
|
||||
{
|
||||
return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W;
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace AssetStudio
|
||||
public long Offset;
|
||||
public Mhy0 mhy0;
|
||||
|
||||
public long TotalSize => 8 + m_Header.compressedBlocksInfoSize + m_BlocksInfo.Sum(x => x.compressedSize);
|
||||
|
||||
public Mhy0File(FileReader reader, string path, Mhy0 mhy0)
|
||||
{
|
||||
this.mhy0 = mhy0;
|
||||
@@ -22,6 +24,10 @@ namespace AssetStudio
|
||||
reader.Endian = EndianType.LittleEndian;
|
||||
|
||||
var signature = reader.ReadStringToNull(4);
|
||||
Logger.Verbose($"Parsed signature {signature}");
|
||||
if (signature != "mhy0")
|
||||
throw new Exception("not a mhy0");
|
||||
|
||||
m_Header = new BundleFile.Header
|
||||
{
|
||||
version = 6,
|
||||
@@ -30,6 +36,7 @@ namespace AssetStudio
|
||||
compressedBlocksInfoSize = reader.ReadUInt32(),
|
||||
flags = (ArchiveFlags)0x43
|
||||
};
|
||||
Logger.Verbose($"Header: {m_Header}");
|
||||
ReadBlocksInfoAndDirectory(reader);
|
||||
using var blocksStream = CreateBlocksStream(path);
|
||||
ReadBlocks(reader, blocksStream);
|
||||
@@ -41,9 +48,11 @@ namespace AssetStudio
|
||||
var blocksInfo = reader.ReadBytes((int)m_Header.compressedBlocksInfoSize);
|
||||
DescrambleHeader(blocksInfo);
|
||||
|
||||
Logger.Verbose($"Descrambled blocksInfo signature {Convert.ToHexString(blocksInfo, 0 , 4)}");
|
||||
using var blocksInfoStream = new MemoryStream(blocksInfo, 0x20, (int)m_Header.compressedBlocksInfoSize - 0x20);
|
||||
using var blocksInfoReader = new EndianBinaryReader(blocksInfoStream);
|
||||
m_Header.uncompressedBlocksInfoSize = blocksInfoReader.ReadMhy0UInt();
|
||||
Logger.Verbose($"uncompressed blocksInfo size: 0x{m_Header.uncompressedBlocksInfoSize:X8}");
|
||||
var compressedBlocksInfo = blocksInfoReader.ReadBytes((int)blocksInfoReader.Remaining);
|
||||
var uncompressedBlocksInfo = new byte[(int)m_Header.uncompressedBlocksInfoSize];
|
||||
var numWrite = LZ4Codec.Decode(compressedBlocksInfo, uncompressedBlocksInfo);
|
||||
@@ -52,10 +61,12 @@ namespace AssetStudio
|
||||
throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {m_Header.uncompressedBlocksInfoSize} bytes");
|
||||
}
|
||||
|
||||
Logger.Verbose($"Writing block and directory to blocks stream...");
|
||||
using var blocksInfoUncompressedStream = new MemoryStream(uncompressedBlocksInfo);
|
||||
using var blocksInfoUncompressedReader = new EndianBinaryReader(blocksInfoUncompressedStream);
|
||||
var nodesCount = blocksInfoUncompressedReader.ReadMhy0Int();
|
||||
m_DirectoryInfo = new BundleFile.Node[nodesCount];
|
||||
Logger.Verbose($"Directory count: {nodesCount}");
|
||||
for (int i = 0; i < nodesCount; i++)
|
||||
{
|
||||
m_DirectoryInfo[i] = new BundleFile.Node
|
||||
@@ -65,10 +76,13 @@ namespace AssetStudio
|
||||
offset = blocksInfoUncompressedReader.ReadMhy0Int(),
|
||||
size = blocksInfoUncompressedReader.ReadMhy0UInt()
|
||||
};
|
||||
|
||||
Logger.Verbose($"Directory {i} Info: {m_DirectoryInfo[i]}");
|
||||
}
|
||||
|
||||
var blocksInfoCount = blocksInfoUncompressedReader.ReadMhy0Int();
|
||||
m_BlocksInfo = new BundleFile.StorageBlock[blocksInfoCount];
|
||||
Logger.Verbose($"Blocks count: {blocksInfoCount}");
|
||||
for (int i = 0; i < blocksInfoCount; i++)
|
||||
{
|
||||
m_BlocksInfo[i] = new BundleFile.StorageBlock
|
||||
@@ -77,6 +91,8 @@ namespace AssetStudio
|
||||
uncompressedSize = blocksInfoUncompressedReader.ReadMhy0UInt(),
|
||||
flags = (StorageBlockFlags)0x43
|
||||
};
|
||||
|
||||
Logger.Verbose($"Block {i} Info: {m_BlocksInfo[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +100,7 @@ namespace AssetStudio
|
||||
{
|
||||
Stream blocksStream;
|
||||
var uncompressedSizeSum = (int)m_BlocksInfo.Sum(x => x.uncompressedSize);
|
||||
Logger.Verbose($"Total size of decompressed blocks: 0x{uncompressedSizeSum:X8}");
|
||||
if (uncompressedSizeSum >= int.MaxValue)
|
||||
blocksStream = new FileStream(path + ".temp", FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
|
||||
else
|
||||
@@ -110,6 +127,7 @@ namespace AssetStudio
|
||||
var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize);
|
||||
DescrambleEntry(compressedBytesSpan);
|
||||
|
||||
Logger.Verbose($"Descrambled block signature {Convert.ToHexString(compressedBytes, 0, 4)}");
|
||||
var numWrite = LZ4Codec.Decode(compressedBytesSpan[0xC..compressedSize], uncompressedBytesSpan);
|
||||
if (numWrite != uncompressedSize)
|
||||
{
|
||||
@@ -124,6 +142,8 @@ namespace AssetStudio
|
||||
|
||||
private void ReadFiles(Stream blocksStream, string path)
|
||||
{
|
||||
Logger.Verbose($"Writing files from blocks stream...");
|
||||
|
||||
fileList = new StreamFile[m_DirectoryInfo.Length];
|
||||
for (int i = 0; i < m_DirectoryInfo.Length; i++)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace AssetStudio
|
||||
using System.Text;
|
||||
|
||||
namespace AssetStudio
|
||||
{
|
||||
public class ObjectInfo
|
||||
{
|
||||
@@ -11,5 +13,18 @@
|
||||
|
||||
public long m_PathID;
|
||||
public SerializedType serializedType;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"byteStart: 0x{byteStart:X8} | ");
|
||||
sb.Append($"byteSize: 0x{byteSize:X8} | ");
|
||||
sb.Append($"typeID: {typeID} | ");
|
||||
sb.Append($"classID: {classID} | ");
|
||||
sb.Append($"isDestroyed: {isDestroyed} | ");
|
||||
sb.Append($"stripped: {stripped} | ");
|
||||
sb.Append($"PathID: {m_PathID}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,13 @@ namespace AssetStudio
|
||||
serializedType = objectInfo.serializedType;
|
||||
platform = assetsFile.m_TargetPlatform;
|
||||
m_Version = assetsFile.header.m_Version;
|
||||
|
||||
Logger.Verbose($"Initialized reader for {type} object with {m_PathID} in file {assetsFile.fileName} !!");
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Logger.Verbose($"Resetting reader position to object offset 0x{byteStart:X8}...");
|
||||
Position = byteStart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,7 @@ namespace AssetStudio
|
||||
_baseStream.Seek(target, SeekOrigin.Begin);
|
||||
return Position;
|
||||
}
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (offset > _baseStream.Length || Position + count > _baseStream.Length)
|
||||
{
|
||||
throw new IOException("Unable to read beyond stream bound");
|
||||
}
|
||||
return _baseStream.Read(buffer, offset, count);
|
||||
}
|
||||
public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count);
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
|
||||
@@ -60,23 +60,28 @@ namespace AssetStudio
|
||||
reader.Position = header.m_FileSize - header.m_MetadataSize;
|
||||
m_FileEndianess = reader.ReadByte();
|
||||
}
|
||||
|
||||
|
||||
if (header.m_Version >= SerializedFileFormatVersion.LargeFilesSupport)
|
||||
{
|
||||
header.m_MetadataSize = reader.ReadUInt32();
|
||||
header.m_FileSize = reader.ReadInt64();
|
||||
header.m_DataOffset = reader.ReadInt64();
|
||||
reader.ReadInt64(); // unknown
|
||||
|
||||
}
|
||||
|
||||
Logger.Verbose($"File {fileName} Info: {header}");
|
||||
|
||||
// ReadMetadata
|
||||
if (m_FileEndianess == 0)
|
||||
{
|
||||
reader.Endian = EndianType.LittleEndian;
|
||||
Logger.Verbose($"Endianness {reader.Endian}");
|
||||
}
|
||||
if (header.m_Version >= SerializedFileFormatVersion.Unknown_7)
|
||||
{
|
||||
unityVersion = reader.ReadStringToNull();
|
||||
Logger.Verbose($"Unity version {unityVersion}");
|
||||
SetVersion(unityVersion);
|
||||
}
|
||||
if (header.m_Version >= SerializedFileFormatVersion.Unknown_8)
|
||||
@@ -84,12 +89,15 @@ namespace AssetStudio
|
||||
m_TargetPlatform = (BuildTarget)reader.ReadInt32();
|
||||
if (!Enum.IsDefined(typeof(BuildTarget), m_TargetPlatform))
|
||||
{
|
||||
Logger.Verbose($"Parsed target format {m_TargetPlatform} doesn't match any of supported formats, defaulting to {BuildTarget.UnknownPlatform}");
|
||||
m_TargetPlatform = BuildTarget.UnknownPlatform;
|
||||
}
|
||||
else if (game.Type.IsMhyGroup())
|
||||
{
|
||||
Logger.Verbose($"Selected game {game.Name} is a mhy game, forcing target format {BuildTarget.StandaloneWindows64}");
|
||||
m_TargetPlatform = BuildTarget.StandaloneWindows64;
|
||||
}
|
||||
Logger.Verbose($"Target format {m_TargetPlatform}");
|
||||
}
|
||||
if (header.m_Version >= SerializedFileFormatVersion.HasTypeTreeHashes)
|
||||
{
|
||||
@@ -99,6 +107,7 @@ namespace AssetStudio
|
||||
// Read Types
|
||||
int typeCount = reader.ReadInt32();
|
||||
m_Types = new List<SerializedType>(typeCount);
|
||||
Logger.Verbose($"Found {typeCount} serialized types");
|
||||
for (int i = 0; i < typeCount; i++)
|
||||
{
|
||||
m_Types.Add(ReadSerializedType(false));
|
||||
@@ -114,6 +123,7 @@ namespace AssetStudio
|
||||
m_Objects = new List<ObjectInfo>(objectCount);
|
||||
Objects = new List<Object>(objectCount);
|
||||
ObjectsDic = new Dictionary<long, Object>(objectCount);
|
||||
Logger.Verbose($"Found {objectCount} objects");
|
||||
for (int i = 0; i < objectCount; i++)
|
||||
{
|
||||
var objectInfo = new ObjectInfo();
|
||||
@@ -164,12 +174,14 @@ namespace AssetStudio
|
||||
{
|
||||
objectInfo.stripped = reader.ReadByte();
|
||||
}
|
||||
Logger.Verbose($"Object Info: {objectInfo}");
|
||||
m_Objects.Add(objectInfo);
|
||||
}
|
||||
|
||||
if (header.m_Version >= SerializedFileFormatVersion.HasScriptTypeIndex)
|
||||
{
|
||||
int scriptCount = reader.ReadInt32();
|
||||
Logger.Verbose($"Found {scriptCount} scripts");
|
||||
m_ScriptTypes = new List<LocalSerializedObjectIdentifier>(scriptCount);
|
||||
for (int i = 0; i < scriptCount; i++)
|
||||
{
|
||||
@@ -184,12 +196,14 @@ namespace AssetStudio
|
||||
reader.AlignStream();
|
||||
m_ScriptType.localIdentifierInFile = reader.ReadInt64();
|
||||
}
|
||||
Logger.Verbose($"Script Info: {m_ScriptType}");
|
||||
m_ScriptTypes.Add(m_ScriptType);
|
||||
}
|
||||
}
|
||||
|
||||
int externalsCount = reader.ReadInt32();
|
||||
m_Externals = new List<FileIdentifier>(externalsCount);
|
||||
Logger.Verbose($"Found {externalsCount} externals");
|
||||
for (int i = 0; i < externalsCount; i++)
|
||||
{
|
||||
var m_External = new FileIdentifier();
|
||||
@@ -204,6 +218,7 @@ namespace AssetStudio
|
||||
}
|
||||
m_External.pathName = reader.ReadStringToNull();
|
||||
m_External.fileName = Path.GetFileName(m_External.pathName);
|
||||
Logger.Verbose($"External Info: {m_External}");
|
||||
m_Externals.Add(m_External);
|
||||
}
|
||||
|
||||
@@ -211,6 +226,7 @@ namespace AssetStudio
|
||||
{
|
||||
int refTypesCount = reader.ReadInt32();
|
||||
m_RefTypes = new List<SerializedType>(refTypesCount);
|
||||
Logger.Verbose($"Found {refTypesCount} reference types");
|
||||
for (int i = 0; i < refTypesCount; i++)
|
||||
{
|
||||
m_RefTypes.Add(ReadSerializedType(true));
|
||||
@@ -239,12 +255,14 @@ namespace AssetStudio
|
||||
|
||||
private SerializedType ReadSerializedType(bool isRefType)
|
||||
{
|
||||
Logger.Verbose($"Attempting to parse serialized" + (isRefType ? " reference" : " ") + "type");
|
||||
var type = new SerializedType();
|
||||
|
||||
type.classID = reader.ReadInt32();
|
||||
|
||||
if (game.Type.IsGIGroup() && BitConverter.ToBoolean(header.m_Reserved))
|
||||
{
|
||||
Logger.Verbose($"Encoded class ID {type.classID}, decoding...");
|
||||
type.classID = DecodeClassID(type.classID);
|
||||
}
|
||||
|
||||
@@ -273,6 +291,7 @@ namespace AssetStudio
|
||||
|
||||
if (m_EnableTypeTree)
|
||||
{
|
||||
Logger.Verbose($"File has type tree enabled !!");
|
||||
type.m_Type = new TypeTree();
|
||||
type.m_Type.m_Nodes = new List<TypeTreeNode>();
|
||||
if (header.m_Version >= SerializedFileFormatVersion.Unknown_12 || header.m_Version == SerializedFileFormatVersion.Unknown_10)
|
||||
@@ -298,11 +317,13 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Verbose($"Serialized type info: {type}");
|
||||
return type;
|
||||
}
|
||||
|
||||
private void ReadTypeTree(TypeTree m_Type, int level = 0)
|
||||
{
|
||||
Logger.Verbose($"Attempting to parse type tree...");
|
||||
var typeTreeNode = new TypeTreeNode();
|
||||
m_Type.m_Nodes.Add(typeTreeNode);
|
||||
typeTreeNode.m_Level = level;
|
||||
@@ -329,12 +350,16 @@ namespace AssetStudio
|
||||
{
|
||||
ReadTypeTree(m_Type, level + 1);
|
||||
}
|
||||
|
||||
Logger.Verbose($"Type Tree Info: {m_Type}");
|
||||
}
|
||||
|
||||
private void TypeTreeBlobRead(TypeTree m_Type)
|
||||
{
|
||||
Logger.Verbose($"Attempting to parse blob type tree...");
|
||||
int numberOfNodes = reader.ReadInt32();
|
||||
int stringBufferSize = reader.ReadInt32();
|
||||
Logger.Verbose($"Found {numberOfNodes} nodes and {stringBufferSize} strings");
|
||||
for (int i = 0; i < numberOfNodes; i++)
|
||||
{
|
||||
var typeTreeNode = new TypeTreeNode();
|
||||
@@ -364,6 +389,8 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Verbose($"Type Tree Info: {m_Type}");
|
||||
|
||||
string ReadString(EndianBinaryReader stringBufferReader, uint value)
|
||||
{
|
||||
var isOffset = (value & 0x80000000) == 0;
|
||||
@@ -383,6 +410,7 @@ namespace AssetStudio
|
||||
|
||||
public void AddObject(Object obj)
|
||||
{
|
||||
Logger.Verbose($"Caching object with {obj.m_PathID} in file {fileName}...");
|
||||
Objects.Add(obj);
|
||||
ObjectsDic.Add(obj.m_PathID, obj);
|
||||
}
|
||||
|
||||
@@ -13,5 +13,16 @@ namespace AssetStudio
|
||||
public long m_DataOffset;
|
||||
public byte m_Endianess;
|
||||
public byte[] m_Reserved;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"MetadataSize: 0x{m_MetadataSize:X8} | ");
|
||||
sb.Append($"FileSize: 0x{m_FileSize:X8} | ");
|
||||
sb.Append($"Version: {m_Version} | ");
|
||||
sb.Append($"DataOffset: 0x{m_DataOffset:X8} | ");
|
||||
sb.Append($"Endianness: {(EndianType)m_Endianess}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,16 @@ namespace AssetStudio
|
||||
public string m_AsmName;
|
||||
|
||||
public bool Match(string hash) => Convert.ToHexString(m_OldTypeHash) == hash;
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"classID: {classID} | ");
|
||||
sb.Append($"IsStrippedType: {m_IsStrippedType} | ");
|
||||
sb.Append($"ScriptTypeIndex: {m_ScriptTypeIndex} | ");
|
||||
sb.Append($"KlassName: {m_KlassName} | ");
|
||||
sb.Append($"NameSpace: {m_NameSpace} | ");
|
||||
sb.Append($"AsmName: {m_AsmName}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ namespace AssetStudio
|
||||
{
|
||||
var m_Node = m_Nodes[i];
|
||||
var varTypeStr = m_Node.m_Type;
|
||||
Logger.Verbose($"Reading {m_Node.m_Name} of type {varTypeStr}");
|
||||
object value;
|
||||
var align = (m_Node.m_MetaFlag & 0x4000) != 0;
|
||||
switch (varTypeStr)
|
||||
|
||||
@@ -28,5 +28,19 @@ namespace AssetStudio
|
||||
m_Level = level;
|
||||
m_MetaFlag = align ? 0x4000 : 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"Type: {m_Type} | ");
|
||||
sb.Append($"Name: {m_Name} | ");
|
||||
sb.Append($"ByteSize: 0x{m_ByteSize:X8} | ");
|
||||
sb.Append($"Index: {m_Index} | ");
|
||||
sb.Append($"TypeFlags: {m_TypeFlags} | ");
|
||||
sb.Append($"Version: {m_Version} | ");
|
||||
sb.Append($"TypeStrOffset: 0x{m_TypeStrOffset:X8} | ");
|
||||
sb.Append($"NameStrOffset: 0x{m_NameStrOffset:X8}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace AssetStudio
|
||||
public int dataOffset;
|
||||
public int dataLength;
|
||||
public string path;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"dataOffset: 0x{dataOffset:X8} | ");
|
||||
sb.Append($"dataOffset: 0x{dataLength:X8} | ");
|
||||
sb.Append($"path: {path}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public WebFile(EndianBinaryReader reader)
|
||||
@@ -21,15 +30,19 @@ namespace AssetStudio
|
||||
var signature = reader.ReadStringToNull();
|
||||
var headLength = reader.ReadInt32();
|
||||
var dataList = new List<WebData>();
|
||||
Logger.Verbose($"Header size: 0x{headLength:X8}");
|
||||
while (reader.BaseStream.Position < headLength)
|
||||
{
|
||||
var data = new WebData();
|
||||
data.dataOffset = reader.ReadInt32();
|
||||
data.dataLength = reader.ReadInt32();
|
||||
var pathLength = reader.ReadInt32();
|
||||
Logger.Verbose($"Path length: {pathLength}");
|
||||
data.path = Encoding.UTF8.GetString(reader.ReadBytes(pathLength));
|
||||
Logger.Verbose($"Web data Info: {data}");
|
||||
dataList.Add(data);
|
||||
}
|
||||
Logger.Verbose("Writing files to streams...");
|
||||
fileList = new StreamFile[dataList.Count];
|
||||
for (int i = 0; i < dataList.Count; i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user