init repo

This commit is contained in:
StarBeats 2025-10-22 10:20:34 +08:00
commit fc84dc5f7e
39 changed files with 3782 additions and 0 deletions

58
.gitignore vendored Normal file
View File

@ -0,0 +1,58 @@
## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
.vs
.vscode
.idea
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# Others
~$*
*~
CodeCoverage/
# MSBuild Binary and Structured Log
*.binlog
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml

1031
AssetDependencyGraph.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityFileDumper", "UnityFileDumper\UnityFileDumper.csproj", "{AA81D2D2-75E6-483A-A682-61A8F9A26023}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityDependencyAnalyzer", "UnityDependencyAnalyzer\UnityDependencyAnalyzer.csproj", "{FC9CD587-C3AF-4892-9270-94BCC7E7C3D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AA81D2D2-75E6-483A-A682-61A8F9A26023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA81D2D2-75E6-483A-A682-61A8F9A26023}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA81D2D2-75E6-483A-A682-61A8F9A26023}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA81D2D2-75E6-483A-A682-61A8F9A26023}.Release|Any CPU.Build.0 = Release|Any CPU
{FC9CD587-C3AF-4892-9270-94BCC7E7C3D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC9CD587-C3AF-4892-9270-94BCC7E7C3D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC9CD587-C3AF-4892-9270-94BCC7E7C3D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC9CD587-C3AF-4892-9270-94BCC7E7C3D3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,318 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AssetDependencyGraph
{
[BsonIgnoreExtraElements]
public class AssetIdentify
{
public string Path = null!;
public string AssetType = null!;
[AllowNull]
public string Guid;
[AllowNull]
public string Md5;
}
public sealed class AssetIdentifyJsonConverter : JsonConverter<AssetIdentify>
{
static JsonSerializerOptions serializerOptions = new JsonSerializerOptions() { IncludeFields = true };
public override AssetIdentify? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<AssetIdentify>(reader.GetString()!, serializerOptions);
}
public override void Write(Utf8JsonWriter writer, AssetIdentify value, JsonSerializerOptions options)
{
writer.WriteStringValue(JsonSerializer.Serialize(value, serializerOptions));
}
public override AssetIdentify ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Read(ref reader, typeToConvert, serializerOptions)!;
}
public override void WriteAsPropertyName(Utf8JsonWriter writer, [DisallowNull] AssetIdentify value, JsonSerializerOptions options)
{
writer.WritePropertyName(JsonSerializer.Serialize(value, serializerOptions));
}
}
[BsonIgnoreExtraElements]
public class AssetNode
{
public AssetIdentify Self=null!;
public string AssetType=null!;
[JsonIgnore]
public ConcurrentBag<AssetIdentify> Dependencies = new();
[JsonIgnore]
public ConcurrentBag<AssetIdentify> Dependent = new();
[AllowNull]
public HashSet<AssetIdentify> DependencySet;
[AllowNull]
public HashSet<AssetIdentify> DependentSet;
}
public sealed class AssetNodeJsonConverter : JsonConverter<AssetNode>
{
static JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
IncludeFields = true,
Converters = { new AssetIdentifyJsonConverter() }
};
public override AssetNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<AssetNode>(reader.GetString()!, serializerOptions);
}
public override void Write(Utf8JsonWriter writer, AssetNode value, JsonSerializerOptions options)
{
writer.WriteStringValue(JsonSerializer.Serialize(value, serializerOptions));
}
}
[BsonIgnoreExtraElements]
public sealed class FolderNode : AssetNode
{
}
[BsonIgnoreExtraElements]
public sealed class PackageNode : AssetNode
{
}
public class AssetDependencyGraphDB
{
MongoClient client;
IMongoCollection<FolderNode> FolderNodes;
IMongoCollection<PackageNode> PackageNodes;
IMongoCollection<AssetNode> AssetNodes;
Dictionary<string, AssetNode> findCacheDic = new();
public AssetDependencyGraphDB(string user, string passwd, string ip)
{
MongoClientSettings settings;
if(string.IsNullOrWhiteSpace(user) && !string.IsNullOrEmpty(ip))
{
settings = MongoClientSettings.FromUrl(new MongoUrl($"mongodb://{ip}:27017/"));
}
else
{
settings = MongoClientSettings.FromUrl(new MongoUrl($"mongodb://{user}:{passwd}@{ip}:27017/"));
}
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
settings.MinConnectionPoolSize = 1;
settings.MaxConnectionPoolSize = 25;
client = new MongoClient(settings);
var db = client.GetDatabase("assetgraph");
FolderNodes = db.GetCollection<FolderNode>("folder_nodes");
PackageNodes = db.GetCollection<PackageNode>("package_nodes");
AssetNodes = db.GetCollection<AssetNode>("asset_nodes");
}
public void Clean()
{
client.DropDatabase("assetgraph");
var db = client.GetDatabase("assetgraph");
FolderNodes = db.GetCollection<FolderNode>("folder_nodes");
PackageNodes = db.GetCollection<PackageNode>("package_nodes");
AssetNodes = db.GetCollection<AssetNode>("asset_nodes");
}
public void Insert<T>(T node) where T : AssetNode
{
switch (node)
{
case FolderNode folderNode:
{
FolderNodes.InsertOne(folderNode);
break;
}
case PackageNode packageNode:
{
PackageNodes.InsertOne(packageNode);
break;
}
case AssetNode assetNode:
{
AssetNodes.InsertOne(assetNode);
break;
}
default:
break;
}
}
public void UpdateOrInsert<T>(T node) where T : AssetNode
{
switch (node)
{
case FolderNode folderNode:
{
var filter = Builders<FolderNode>.Filter.And(
Builders<FolderNode>.Filter.Eq(fn=>fn.Self.Path,node.Self.Path)
);
var found = FolderNodes.Find(filter);
if (found == null || found.CountDocuments() == 0)
{
FolderNodes.InsertOne(folderNode);
}
else
{
var result = FolderNodes.UpdateOne(filter, Builders<FolderNode>.Update.Combine(
Builders<FolderNode>.Update.Set(fn => fn.Self, folderNode.Self),
Builders<FolderNode>.Update.Set(fn => fn.AssetType, folderNode.AssetType),
Builders<FolderNode>.Update.Set(fn => fn.Dependencies, folderNode.Dependencies),
Builders<FolderNode>.Update.Set(fn => fn.Dependent, folderNode.Dependent)
));
}
break;
}
case PackageNode packageNode:
{
var filter = Builders<PackageNode>.Filter.And(
Builders<PackageNode>.Filter.Eq(fn => fn.Self.Path, node.Self.Path)
);
var found = PackageNodes.Find(filter);
if (found == null || found.CountDocuments() == 0)
{
PackageNodes.InsertOne(packageNode);
}
else
{
var result = PackageNodes.UpdateOne(filter, Builders<PackageNode>.Update.Combine(
Builders<PackageNode>.Update.Set(fn => fn.Self, packageNode.Self),
Builders<PackageNode>.Update.Set(fn => fn.AssetType, packageNode.AssetType),
Builders<PackageNode>.Update.Set(fn => fn.Dependencies, packageNode.Dependencies),
Builders<PackageNode>.Update.Set(fn => fn.Dependent, packageNode.Dependent)
));
}
break;
}
case AssetNode assetNode:
{
var filter = Builders<AssetNode>.Filter.And(
Builders<AssetNode>.Filter.Eq(fn => fn.Self.Path, node.Self.Path)
);
var found = AssetNodes.Find(filter);
if (found == null || found.CountDocuments() == 0)
{
AssetNodes.InsertOne(assetNode);
}
else
{
var result = AssetNodes.UpdateOne(filter, Builders<AssetNode>.Update.Combine(
Builders<AssetNode>.Update.Set(fn => fn.Self, assetNode.Self),
Builders<AssetNode>.Update.Set(fn => fn.AssetType, assetNode.AssetType),
Builders<AssetNode>.Update.Set(fn => fn.Dependencies, assetNode.Dependencies),
Builders<AssetNode>.Update.Set(fn => fn.Dependent, assetNode.Dependent)
));
}
break;
}
default:
break;
}
}
public void Delete<T>(T node) where T : AssetNode
{
switch (node)
{
case FolderNode folderNode:
{
var filter = Builders<FolderNode>.Filter.And(
Builders<FolderNode>.Filter.Eq(fn => fn.Self.Path, node.Self.Path)
);
var found = FolderNodes.Find(filter);
if (found != null && found.CountDocuments() == 0)
{
// TODO: del ref dep
FolderNodes.DeleteOne(filter);
}
break;
}
case PackageNode packageNode:
{
var filter = Builders<PackageNode>.Filter.And(
Builders<PackageNode>.Filter.Eq(fn => fn.Self.Path, node.Self.Path)
);
var found = PackageNodes.Find(filter);
if (found != null && found.CountDocuments() == 0)
{
// TODO: del ref dep
PackageNodes.DeleteOne(filter);
}
break;
}
case AssetNode assetNode:
{
var filter = Builders<AssetNode>.Filter.And(
Builders<AssetNode>.Filter.Eq(fn => fn.Self.Path, node.Self.Path)
);
var found = AssetNodes.Find(filter);
if (found != null && found.CountDocuments() == 0)
{
// TODO: del ref dep
AssetNodes.DeleteOne(filter);
}
break;
}
default:
break;
}
}
public AssetNode Find(string path)
{
if(findCacheDic.TryGetValue(path, out var assetNode))
{
return assetNode;
}
var filter = Builders<AssetNode>.Filter.And(
Builders<AssetNode>.Filter.Eq(fn => fn.Self.Path, path)
);
var found = AssetNodes.Find(filter);
if (found != null && found.CountDocuments() != 0)
{
assetNode = found.First();
findCacheDic[path] = assetNode;
return assetNode;
}
var filter1 = Builders<PackageNode>.Filter.And(
Builders<PackageNode>.Filter.Eq(fn => fn.Self.Path, path)
);
var found1 = PackageNodes.Find(filter1);
if (found1 != null && found1.CountDocuments() != 0)
{
assetNode = found1.First();
findCacheDic[path] = assetNode;
return assetNode;
}
var filter2 = Builders<FolderNode>.Filter.And(
Builders<FolderNode>.Filter.Eq(fn => fn.Self.Path, path)
);
var found2 = FolderNodes.Find(filter2);
if (found2 != null && found2.CountDocuments() != 0)
{
assetNode = found2.First();
findCacheDic[path] = assetNode;
return assetNode;
}
return null!;
}
}
}

View File

@ -0,0 +1,557 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Options;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace AssetDependencyGraph
{
public static class FileExtensionHelper
{
public static string GetTypeByExtension(string ext)
{
switch (ext.ToLowerInvariant())
{
case ".a":
case ".dll":
case ".so":
case ".exe":
case ".dynlib":
return "Executable";
case ".asmdef":
case ".asmref":
return "UnityAssembly";
case ".cs":
case ".lua":
case ".js":
case ".ts":
case ".java":
case ".h":
case ".cpp":
case ".cxx":
case ".mm":
case ".py":
case ".bat":
case ".jar":
case ".arr":
case ".jslib":
return "SourceFile";
case ".gradle":
return "MakeFile";
case ".dat":
case ".data":
return "DatFile";
case ".mp3":
case ".ogg":
case ".wav":
return "AudioClip";
case ".mp4":
case ".webm":
return "VideoClip";
case ".mat":
return "Material";
case ".rendertexture":
case ".dds":
case ".exr":
case ".hdr":
case ".png":
case ".jpg":
case ".gif":
case ".psd":
case ".bmp":
case ".tiff":
case ".tga":
case ".gradient":
case ".spriteatlas":
return "Texture";
case ".obj":
case ".fbx":
case ".mesh":
return "Mesh";
case ".shader":
case ".surfshader":
case ".shadergraph":
return "Shader";
case ".compute":
return "ComputeShader";
case ".hlsl":
case ".cginc":
case ".shadersubgraph":
return "ShaderHeader";
case ".otf":
case ".ttf":
return "Font";
case ".byte":
case ".bytes":
case ".bin":
return "Binary";
case ".txt":
case ".md":
case ".chm":
case ".yml":
case ".url":
case ".json":
case ".json5":
case ".xml":
case ".uxml":
case ".nson":
case ".config":
case ".pdf":
return "TextFile";
case ".xlsx":
case ".xls":
return "Excel";
case ".unity":
case ".scene":
return "Scene";
case ".prefab":
return "Prefab";
default:
return "UnknowFileType";
}
}
public static bool IsPackage(string ext)
{
switch (ext.ToLowerInvariant())
{
case ".prefab":
case ".unity":
return true;
default:
return false;
}
}
public static bool NeedAnalyzeDepend(string ext)
{
switch (ext.ToLowerInvariant())
{
case ".prefab":
case ".unity":
case ".asset":
case ".mat":
return true;
default:
return false;
}
}
public static bool Exclude(string path) => path.EndsWith(".meta")
|| path.EndsWith(".unitypackage")
|| path.EndsWith(".preset")
|| path.EndsWith(".backup")
|| path.EndsWith(".tmp")
|| path.EndsWith(".editor")
|| path.EndsWith(".zip")
|| path.EndsWith(".scenetemplate");
}
public interface IDependencyAnalysis
{
void Analyze(string path, Dictionary<string, HashSet<string>> result);
}
public class FolderDependencyAnalysis : IDependencyAnalysis
{
public void Analyze(string path, Dictionary<string, HashSet<string>> result)
{
if (!result.TryGetValue(path, out var list))
{
result[path] = list = new();
}
foreach (string file in Directory.EnumerateFiles(path))
{
if (FileExtensionHelper.Exclude(file))
{
continue;
}
var p = file.ToUniversalPath().ToUnityRelatePath();
list.Add(p);
}
foreach (string directory in Directory.EnumerateDirectories(path))
{
var p = directory.ToUniversalPath().ToUnityRelatePath();
list.Add(p);
}
}
}
public class UnityDependencyAnalysis2 : IDependencyAnalysis
{
Regex guidRegex = new Regex("guid:\\s?([\\da-f]+)");
List<string> GetDepGuidByFile(string path)
{
List<string> result;
try
{
result = UnityFileApi.DependencyTool.GetDependencies(path);
}
catch (NotSupportedException)
{
var str = File.ReadAllText(path);
result = new();
var matches = guidRegex.Matches(str);
for (int i = 0; i < matches.Count; i++)
{
var guid = matches[i].Groups[1].Value;
if (!result.Contains(guid))
{
result.Add(guid);
}
}
}
return result;
}
public void Analyze(string path, Dictionary<string, HashSet<string>> result)
{
if (!result.TryGetValue(path, out var list))
{
result[path] = list = new();
}
var ext = Path.GetExtension(path);
if (FileExtensionHelper.NeedAnalyzeDepend(ext))
{
var dependencies = GetDepGuidByFile(path);
for (int i = 0; i < dependencies.Count; i++)
{
var dep = dependencies[i];
list.Add(dep);
}
}
}
}
internal class DependencyAnalyzer
{
private Dictionary<Predicate<(string path, bool isDir)>, IDependencyAnalysis> dependencyAnalysisDic = new();
private Dictionary<string, HashSet<string>> path2Dependences = new();
private JsonSerializerOptions options = new JsonSerializerOptions { IncludeFields = true };
[BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)]
private ConcurrentDictionary<AssetIdentify, AssetNode> assetIdentify2AssetNodeDic = new();
private ConcurrentDictionary<string, AssetIdentify> path2Id = new();
private List<(string path, bool isDir)> allPath = new();
private UnityLmdb unityLmdb;
private static Regex isGuid = new Regex("^[\\da-f]{32}$");
public DependencyAnalyzer()
{
unityLmdb = new UnityLmdb();
dependencyAnalysisDic.Add(new Predicate<(string path, bool isDir)>(pi => !pi.isDir), new UnityDependencyAnalysis2());
dependencyAnalysisDic.Add(new Predicate<(string path, bool isDir)>(pi => pi.isDir), new FolderDependencyAnalysis());
}
private void Visivt(string path)
{
path = path.ToUniversalPath();
if (FileExtensionHelper.Exclude(path))
{
return;
}
allPath.Add((path, Directory.Exists(path)));
}
public static bool IsGuid(string str)
{
if (str.Length == 32)
{
return isGuid.IsMatch(str);
}
return false;
}
public (AssetIdentify id, AssetNode node) GetOrCreateFolderNode(string path)
{
if (!path2Id.TryGetValue(path, out var k))
{
if (k == null)
{
k = new AssetIdentify()
{
Path = path,
AssetType = "Folder",
Guid = null,
Md5 = null
};
assetIdentify2AssetNodeDic[k] = new FolderNode()
{
Self = k,
AssetType = "Folder",
};
}
path2Id[path] = k;
}
return (k, assetIdentify2AssetNodeDic[k]);
}
public (AssetIdentify id, AssetNode node) GetOrCreateAssetNode(string path)
{
if (!path2Id.TryGetValue(path, out var k))
{
if (k == null)
{
var ext = Path.GetExtension(path);
k = new AssetIdentify()
{
Path = path,
Guid = null,
AssetType = FileExtensionHelper.GetTypeByExtension(ext)
//Md5 = Utils.Md5(path)
};
if (FileExtensionHelper.IsPackage(ext))
{
assetIdentify2AssetNodeDic[k] = new PackageNode()
{
Self = k,
AssetType = k.AssetType,
};
}
else
{
assetIdentify2AssetNodeDic[k] = new AssetNode()
{
Self = k,
AssetType = k.AssetType,
};
}
path2Id[path] = k;
}
}
return (k, assetIdentify2AssetNodeDic[k]);
}
public void ResolveGuidDatabase()
{
unityLmdb.ResolveGuidPath();
}
public void AnalyzeMainProcess(string rootFolder, int processCnt = 8)
{
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
Utils.TraverseDirectory(rootFolder, Visivt, -1);
sw.Stop();
Console.WriteLine($"遍历目录耗时:{sw.ElapsedMilliseconds / 1000f}s");
sw.Restart();
var itemCnt = allPath.Count / processCnt;
List<string> subProcessArgs = new();
List<string> resultJsonPaths = new();
var projectPath = Environment.GetCommandLineArgs()[1];
for (int i = 0; i < processCnt; i++)
{
int r = (itemCnt * (i + 1));
if (r >= allPath.Count)
{
r = allPath.Count;
}
var s = JsonSerializer.Serialize(allPath[(i * itemCnt)..r], options);
var jsonPath = Path.Combine(Path.GetTempPath(), $"path{i}.json");
var resulJsonPath = Path.Combine(Path.GetTempPath(), $"result{i}.json");
resultJsonPaths.Add(resulJsonPath);
subProcessArgs.Add($"-reference {projectPath} SubProcess {jsonPath} {resulJsonPath}");
File.WriteAllText(jsonPath, s);
}
Task[] subProcessTask = new Task[subProcessArgs.Count];
var exe = Environment.GetCommandLineArgs()[0];
if (exe.EndsWith(".dll"))
{
exe = exe.Replace(".dll", ".exe");
}
for (int i = 0; i < subProcessArgs.Count; i++)
{
int index = i;
subProcessTask[i] = Task.Factory.StartNew(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo()
{
FileName = exe,
Arguments = subProcessArgs[index],
UseShellExecute = true,
};
p.Start();
p.WaitForExit();
if (p.ExitCode != 0)
{
Console.WriteLine("Sub Process Error.");
}
});
}
Stopwatch sw1 = Stopwatch.StartNew();
sw1.Start();
ResolveGuidDatabase();
sw1.Stop();
Console.WriteLine($"加载数据库耗时:{sw1.ElapsedMilliseconds / 1000f}s");
Task.WaitAll(subProcessTask);
List<Dictionary<string, HashSet<string>>> subProcessResults = new();
foreach (var item in resultJsonPaths)
{
var s = File.ReadAllText(item);
subProcessResults.Add(JsonSerializer.Deserialize<Dictionary<string, HashSet<string>>>(s, options)!);
}
sw.Stop();
Console.WriteLine($"分析引用耗时:{sw.ElapsedMilliseconds / 1000f}s");
sw.Restart();
Parallel.ForEach(subProcessResults, arg => ResolveSubProcessResult(arg));
sw.Stop();
Console.WriteLine($"合并数据耗时:{sw.ElapsedMilliseconds / 1000f}s");
sw.Restart();
foreach (var item in assetIdentify2AssetNodeDic)
{
item.Value.DependencySet = item.Value.Dependencies.ToHashSet();
item.Value.DependentSet = item.Value.Dependent.ToHashSet();
}
string js = JsonSerializer.Serialize(assetIdentify2AssetNodeDic, options: new()
{
IncludeFields = true,
Converters = { new AssetIdentifyJsonConverter(), new AssetNodeJsonConverter() }
});
File.WriteAllText(Path.Combine(UnityLmdb.ProjPath, "Library", "dependencyGraph.json"), js);
//AssetDependencyGraphDB db = new AssetDependencyGraphDB(Environment.GetCommandLineArgs()[2], Environment.GetCommandLineArgs()[3], Environment.GetCommandLineArgs()[4]);
//sw.Restart();
//db.Clean();
//Parallel.ForEach(assetIdentify2AssetNodeDic, item =>
//{
// db.Insert(item.Value);
//});
sw.Stop();
Console.WriteLine($"更新数据库:{sw.ElapsedMilliseconds / 1000f}s");
}
private void ResolveSubProcessResult(Dictionary<string, HashSet<string>> subProcessResult)
{
Parallel.ForEach(subProcessResult, item =>
{
var relPath = item.Key.ToLowerInvariant().ToUnityRelatePath();
var fullPath = relPath.ToUnityFullPath();
if (File.Exists(fullPath))
{
var selfNode = GetOrCreateAssetNode(relPath);
selfNode.id.Guid = unityLmdb.GetGuidByPath(relPath);
foreach (var dep in item.Value)
{
var depPath = dep;
if (IsGuid(dep))
{
depPath = unityLmdb.GetPathByGuid(dep.ToLowerInvariant());
if (string.IsNullOrEmpty(depPath))
{
depPath = dep;
}
}
depPath = depPath.ToLowerInvariant();
var depNode = GetOrCreateAssetNode(depPath);
depNode.node.Dependent.Add(selfNode.id);
selfNode.node.Dependencies.Add(depNode.id);
}
}
else
{
var selfNode = GetOrCreateFolderNode(relPath);
selfNode.id.Guid = unityLmdb.GetGuidByPath(relPath);
foreach (var dep in item.Value)
{
var depPath = dep.ToLowerInvariant().ToUnityRelatePath();
fullPath = depPath.ToUnityFullPath();
(AssetIdentify id, AssetNode node) depNode;
if (File.Exists(fullPath))
{
depNode = GetOrCreateAssetNode(depPath);
}
else
{
depNode = GetOrCreateFolderNode(depPath);
}
depNode.node.Dependent.Add(selfNode.id);
selfNode.node.Dependencies.Add(depNode.id);
}
}
});
}
public void AnalyzeSubProcess(string pathFile, string resultFilePath)
{
var s = File.ReadAllText(pathFile);
allPath = JsonSerializer.Deserialize<List<(string path, bool isDir)>>(s, options)!;
if (allPath != null)
{
foreach (var item in allPath)
{
foreach (var item1 in dependencyAnalysisDic)
{
if (item1.Key(item))
{
item1.Value.Analyze(item.path, path2Dependences);
}
}
}
var json = JsonSerializer.Serialize(path2Dependences, options);
File.WriteAllText(resultFilePath, json);
}
}
public void Analyze(string rootFolder)
{
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
Utils.TraverseDirectory(rootFolder, Visivt, -1);
foreach (var item in allPath)
{
foreach (var item1 in dependencyAnalysisDic)
{
if (item1.Key(item))
{
item1.Value.Analyze(item.path, path2Dependences);
}
}
}
//Parallel.ForEach(allPath, (pi) =>
//{
// foreach (var item in dependencyAnalysisDic)
// {
// if (item.Key(pi))
// {
// item.Value.Analyze(pi.path, path2Dependences);
// }
// }
//});
sw.Stop();
Console.WriteLine($"分析引用耗时:{sw.ElapsedMilliseconds / 1000f}s");
//AssetDependencyGraphDB db = new AssetDependencyGraphDB("", "", "localhost");
//sw.Restart();
//db.Clean();
//Parallel.ForEach(assetIdentify2AssetNodeDic, item =>
//{
// db.UpdateOrInsert(item.Value);
//});
//sw.Stop();
//Console.WriteLine($"更新数据库:{sw.ElapsedMilliseconds / 1000f}s");
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,51 @@
using AssetDependencyGraph;
using System.Text.Json;
switch (Environment.GetCommandLineArgs()[1])
{
case "-dump":
{
UnityLmdb unityLmdb = new();
var projDir = Environment.GetCommandLineArgs()[2].TrimEnd('/').TrimEnd('\\');
unityLmdb.ResolveGuidPathByDBPath($"{projDir}/Library/SourceAssetDB");
var js = unityLmdb.ResultToJson();
File.WriteAllText($"{projDir}/Library/SourceAssetDB.json", js);
break;
}
case "-verify":
{
Console.WriteLine("Start");
UnityLmdb unityLmdb = new();
var projDir = Environment.GetCommandLineArgs()[2].TrimEnd('/').TrimEnd('\\');
Directory.SetCurrentDirectory(projDir);
unityLmdb.ResolveGuidPathByDBPath($"{projDir}/Library/SourceAssetDB");
var res = unityLmdb.VerifyGUID();
if (res.Count > 0)
{
var js = JsonSerializer.Serialize(res, new JsonSerializerOptions { IncludeFields = true });
Console.WriteLine("Has Error.");
File.WriteAllText($"{projDir}/verify-result.json", js);
}
Console.WriteLine("End");
break;
}
case "-reference":
{
UnityLmdb.ProjPath = Environment.GetCommandLineArgs()[2];
Utils.DataPath = Path.Combine(UnityLmdb.ProjPath, "Assets").ToUniversalPath();
if (Environment.GetCommandLineArgs().Length > 3 && Environment.GetCommandLineArgs()[3].Equals("SubProcess"))
{
new DependencyAnalyzer().AnalyzeSubProcess(Environment.GetCommandLineArgs()[4], Environment.GetCommandLineArgs()[5]);
}
else
{
new DependencyAnalyzer().AnalyzeMainProcess(Utils.DataPath, 10);
}
break;
}
default:
break;
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0\publish\win-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<History>True|2025-07-08T03:14:03.4969097Z||;True|2025-07-07T16:09:23.5193447+08:00||;True|2025-07-07T16:07:31.9772710+08:00||;True|2025-07-07T16:07:24.6544267+08:00||;True|2025-07-07T16:06:58.2999030+08:00||;True|2025-07-07T16:06:47.5176281+08:00||;True|2025-07-07T16:06:30.9937062+08:00||;True|2025-07-07T16:06:16.1827410+08:00||;True|2025-07-04T20:51:36.2342905+08:00||;True|2025-07-04T20:49:41.4585526+08:00||;True|2025-07-04T20:46:06.1627373+08:00||;True|2025-07-04T20:39:45.9777563+08:00||;True|2025-07-04T20:38:51.7922210+08:00||;True|2025-07-04T20:38:30.7653415+08:00||;True|2025-07-04T20:37:48.1335374+08:00||;True|2025-07-04T20:37:32.2007568+08:00||;True|2025-07-04T20:37:03.6443318+08:00||;True|2025-07-04T20:35:04.7675245+08:00||;True|2025-04-16T20:37:48.8637838+08:00||;True|2025-04-16T20:23:11.1448355+08:00||;True|2025-04-16T16:49:35.4476343+08:00||;True|2025-04-16T16:36:22.8513231+08:00||;True|2025-04-16T12:23:59.6108463+08:00||;True|2025-04-16T12:08:50.9715020+08:00||;True|2025-04-16T11:57:10.7843966+08:00||;False|2025-04-16T11:02:10.7479206+08:00||;False|2025-04-16T10:59:42.9239923+08:00||;False|2025-04-16T10:54:01.4031170+08:00||;True|2025-04-16T10:29:13.7528358+08:00||;False|2025-04-16T10:27:03.3522077+08:00||;False|2025-04-16T10:26:07.1734998+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"profiles": {
"UnityDependencyAnalyzer": {
"commandName": "Project",
"commandLineArgs": "G:/G_android \" \" \" \" localhost"
}
}
}

View File

@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>False</PublishAot>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\UnityFileDumper\UnityFileDumper.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="AWSSDK.Core">
<HintPath>Libs\AWSSDK.Core.dll</HintPath>
</Reference>
<Reference Include="Crc32.NET">
<HintPath>Libs\Crc32.NET.dll</HintPath>
</Reference>
<Reference Include="DnsClient">
<HintPath>Libs\DnsClient.dll</HintPath>
</Reference>
<Reference Include="LightningDB">
<HintPath>Libs\LightningDB.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>Libs\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="MongoDB.Bson">
<HintPath>Libs\MongoDB.Bson.dll</HintPath>
</Reference>
<Reference Include="MongoDB.Driver">
<HintPath>Libs\MongoDB.Driver.dll</HintPath>
</Reference>
<Reference Include="MongoDB.Driver.Core">
<HintPath>Libs\MongoDB.Driver.Core.dll</HintPath>
</Reference>
<Reference Include="MongoDB.Libmongocrypt">
<HintPath>Libs\MongoDB.Libmongocrypt.dll</HintPath>
</Reference>
<Reference Include="SharpCompress">
<HintPath>Libs\SharpCompress.dll</HintPath>
</Reference>
<Reference Include="Snappier">
<HintPath>Libs\Snappier.dll</HintPath>
</Reference>
<Reference Include="ZstdSharp">
<HintPath>Libs\ZstdSharp.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\xinyt\source\repos\UnityFileDumper\UnityDependencyAnalyzer\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,164 @@
using LightningDB;
using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
namespace AssetDependencyGraph
{
public sealed class UnityLmdb
{
private Dictionary<string, string> guid2Path = new();
private Dictionary<string, string> path2Guid = new();
public static string ProjPath = null!;
private string dbFilePath = null!;
public static byte[] Guid2LmdbKey(string guid)
{
var inputByteArray = new byte[guid.Length / 2];
StringBuilder sb = new StringBuilder();
for (int i = 0; i < guid.Length; i += 2)
{
sb.Append(guid[i + 1]);
sb.Append(guid[i]);
}
guid = sb.ToString();
for (var x = 0; x < inputByteArray.Length; x++)
{
inputByteArray[x] = (byte)Convert.ToInt32(guid.Substring(x * 2, 2), 16);
}
return inputByteArray;
}
public static string LmdbKey2Guid(byte[] bytes)
{
StringBuilder ret = new StringBuilder();
for (var i = 0; i < bytes.Length; i++)
{
ret.AppendFormat("{0:x2}", bytes[i]);
if (ret.Length == 32)
{
break;
}
}
for (int i = 0; i < ret.Length; i += 2)
{
var c = ret[i];
ret[i] = ret[i + 1];
ret[i + 1] = c;
}
var hex = ret.ToString();
return hex;
}
public void ResolveGuidPath()
{
var sourceDbPath = Path.Combine(ProjPath, "Library", "SourceAssetDB");
var dbPath = dbFilePath = Path.Combine(ProjPath, "Library", "SourceAssetDB1");
File.Copy(sourceDbPath, dbPath, true);
using var env = new LightningEnvironment(dbPath, configuration: new()
{
MaxDatabases = 64,
MaxReaders = 64,
});
env.Open(EnvironmentOpenFlags.NoSubDir | EnvironmentOpenFlags.ReadOnly);
using var tx = env.BeginTransaction(TransactionBeginFlags.ReadOnly);
using (var db = tx.OpenDatabase("GuidToPath", closeOnDispose: true))
using (var cursor = tx.CreateCursor(db))
{
foreach (var item in cursor.AsEnumerable())
{
guid2Path[LmdbKey2Guid(item.Item1.AsSpan().ToArray())] = Encoding.UTF8.GetString(item.Item2.AsSpan()).ToLowerInvariant().Trim('\0');
}
}
using (var db = tx.OpenDatabase("PathToGuid", closeOnDispose: true))
using (var cursor = tx.CreateCursor(db))
{
foreach (var item in cursor.AsEnumerable())
{
path2Guid[Encoding.UTF8.GetString(item.Item1.AsSpan()).ToLowerInvariant().Trim('\0')] = LmdbKey2Guid(item.Item2.AsSpan().ToArray());
}
}
}
public void ResolveGuidPathByDBPath(string dbPath)
{
dbFilePath = dbPath;
using var env = new LightningEnvironment(dbPath, configuration: new()
{
MaxDatabases = 64,
MaxReaders = 64,
});
env.Open(EnvironmentOpenFlags.NoSubDir | EnvironmentOpenFlags.ReadOnly);
using var tx = env.BeginTransaction(TransactionBeginFlags.ReadOnly);
using (var db = tx.OpenDatabase("GuidToPath", closeOnDispose: true))
using (var cursor = tx.CreateCursor(db))
{
foreach (var item in cursor.AsEnumerable())
{
guid2Path[LmdbKey2Guid(item.Item1.AsSpan().ToArray())] = Encoding.UTF8.GetString(item.Item2.AsSpan()).ToLowerInvariant().Trim('\0');
}
}
using (var db = tx.OpenDatabase("PathToGuid", closeOnDispose: true))
using (var cursor = tx.CreateCursor(db))
{
foreach (var item in cursor.AsEnumerable())
{
path2Guid[Encoding.UTF8.GetString(item.Item1.AsSpan()).ToLowerInvariant().Trim('\0')] = LmdbKey2Guid(item.Item2.AsSpan().ToArray());
}
}
}
public ConcurrentBag<string> VerifyGUID()
{
ConcurrentBag<string> result = new ();
Parallel.ForEach(path2Guid, (item) =>
{
var f = item.Key + ".meta";
if (File.Exists(f))
{
var ftext = File.ReadAllText(f);
if (!ftext.Contains(item.Value))
{
result.Add(item.Key);
}
}
});
return result;
}
public string ResultToJson()
{
return JsonSerializer.Serialize(path2Guid, new JsonSerializerOptions { IncludeFields = true });
}
public string GetGuidByPath(string path)
{
if (path2Guid.ContainsKey(path))
{
return path2Guid[path];
}
else
{
return null!;
}
}
public string GetPathByGuid(string guid)
{
if (guid2Path.ContainsKey(guid))
{
return guid2Path[guid];
}
else
{
return null!;
}
}
}
}

View File

@ -0,0 +1,94 @@
using System.Text;
namespace AssetDependencyGraph
{
public static class Utils
{
public static string DataPath = null!;
public static string DataPathLow = null!;
public static string Md5(string filename)
{
try
{
FileStream fs = new FileStream(filename, FileMode.Open);
#pragma warning disable SYSLIB0021 // 类型或成员已过时
System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
#pragma warning restore SYSLIB0021 // 类型或成员已过时
byte[] retVal = md5.ComputeHash(fs);
fs.Close();
return BitConverter.ToString(retVal).ToLower().Replace("-", "");
}
catch
{
throw;
}
}
public static void TraverseDirectory(string path, Action<string> action, int depth = 1)
{
if(depth == 0)
{
return;
}
foreach (string file in Directory.EnumerateFiles(path))
{
action.Invoke(file);
}
foreach (string directory in Directory.EnumerateDirectories(path))
{
action.Invoke(directory);
TraverseDirectory(directory, action, --depth);
}
}
public static string ToUniversalPath(this string path)
{
return path.Replace("\\", "/");
}
public static string ToUnityRelatePath(this string path)
{
DataPathLow ??= DataPath.ToLowerInvariant();
if (path.StartsWith(DataPathLow.Replace("assets", "")) && !path.StartsWith(DataPathLow + "/assets"))
{
return path.Replace(DataPathLow.Replace("assets", ""), "");
}
return path.Replace(DataPathLow, "assets");
}
public static string ToUnityFullPath(this string path)
{
if(path.StartsWith("packages"))
{
var fullPath = (DataPath.Replace("Assets", "") + path);
if (!File.Exists(fullPath) && Directory.Exists(fullPath))
{
fullPath = (DataPath.Replace("Assets", "Library/PackageCache") + path);
}
if (!File.Exists(fullPath) && Directory.Exists(fullPath))
{
Console.WriteLine($"ToUnityFullPath failure:{path}");
}
return fullPath;
}
return Path.Combine(DataPath.Replace("Assets", "") , path);
}
public static string ByteString(this byte[] bytes)
{
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
stringBuilder.Append(Convert.ToString(bytes[i], 2) );
}
return stringBuilder.ToString();
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
namespace UnityFileApi
{
public static class DependencyTool
{
static DependencyTool()
{
UnityFileSystem.Init();
}
public static List<string> GetDependencies(string path)
{
List<string> dependencies = new List<string>();
try
{
using var archive = UnityFileSystem.MountArchive(path, "/");
foreach (var node in archive.Nodes)
{
Console.WriteLine($"Processing {node.Path} {node.Size} {node.Flags}");
if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile))
{
using (var serializedFile = UnityFileSystem.OpenSerializedFile(path))
{
foreach (var extRef in serializedFile.ExternalReferences)
{
dependencies.Add(extRef.Guid);
}
}
}
}
return dependencies;
}
catch (NotSupportedException)
{
// Try as SerializedFile
using (var serializedFile = UnityFileSystem.OpenSerializedFile(path))
{
foreach (var extRef in serializedFile.ExternalReferences)
{
dependencies.Add(extRef.Guid);
}
}
return dependencies;
}
}
}
}

View File

@ -0,0 +1,253 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace UnityFileApi
{
public class UnityArchiveHandle : SafeHandle
{
public UnityArchiveHandle() : base(IntPtr.Zero, true)
{
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return DllWrapper.UnmountArchive(handle) == ReturnCode.Success;
}
}
public class UnityFileHandle : SafeHandle
{
public UnityFileHandle() : base(IntPtr.Zero, true)
{
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return DllWrapper.CloseFile(handle) == ReturnCode.Success;
}
}
public class SerializedFileHandle : SafeHandle
{
public SerializedFileHandle() : base(IntPtr.Zero, true)
{
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return DllWrapper.CloseSerializedFile(handle) == ReturnCode.Success;
}
}
public class TypeTreeHandle : SafeHandle
{
public TypeTreeHandle() : base(IntPtr.Zero, true)
{
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return true;
}
internal IntPtr Handle => handle;
}
public enum ReturnCode
{
Success,
AlreadyInitialized,
NotInitialized,
FileNotFound,
FileFormatError,
InvalidArgument,
HigherSerializedFileVersion,
DestinationBufferTooSmall,
InvalidObjectId,
UnknownError,
FileError,
ErrorCreatingArchiveFile,
ErrorAddingFileToArchive,
TypeNotFound,
}
[Flags]
public enum ArchiveNodeFlags
{
None = 0,
Directory = 1 << 0,
Deleted = 1 << 1,
SerializedFile = 1 << 2,
}
public enum CompressionType
{
None,
Lzma,
Lz4,
Lz4HC,
};
public enum SeekOrigin
{
Begin,
Current,
End,
}
public enum ExternalReferenceType
{
NonAssetType,
DeprecatedCachedAssetType,
SerializedAssetType,
MetaAssetType,
}
[StructLayout(LayoutKind.Sequential)]
public struct ObjectInfo
{
public readonly long Id;
public readonly long Offset;
public readonly long Size;
public readonly int TypeId;
}
[Flags]
public enum TypeTreeFlags
{
None = 0,
IsArray = 1 << 0,
IsManagedReference = 1 << 1,
IsManagedReferenceRegistry = 1 << 2,
IsArrayOfRefs = 1 << 3,
}
[Flags]
public enum TypeTreeMetaFlags
{
None = 0,
AlignBytes = 1 << 14,
AnyChildUsesAlignBytes = 1 << 15,
}
public static class DllWrapper
{
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_Init")]
public static extern ReturnCode Init();
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_Cleanup")]
public static extern ReturnCode Cleanup();
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_MountArchive")]
public static extern ReturnCode MountArchive([MarshalAs(UnmanagedType.LPStr)] string path, [MarshalAs(UnmanagedType.LPStr)] string mountPoint, out UnityArchiveHandle handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_UnmountArchive")]
public static extern ReturnCode UnmountArchive(IntPtr handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetArchiveNodeCount")]
public static extern ReturnCode GetArchiveNodeCount(UnityArchiveHandle handle, out int count);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetArchiveNode")]
public static extern ReturnCode GetArchiveNode(UnityArchiveHandle handle, int nodeIndex, StringBuilder path, int pathLen, out long size, out ArchiveNodeFlags flags);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_CreateArchive")]
public static extern ReturnCode CreateArchive([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] sourceFiles,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] aliases, bool[] isSerializedFile, int count,
[MarshalAs(UnmanagedType.LPStr)] string archiveFile, CompressionType compression, out int crc);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_OpenFile")]
public static extern ReturnCode OpenFile([MarshalAs(UnmanagedType.LPStr)] string path, out UnityFileHandle handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl, EntryPoint = "UFS_ReadFile")]
public static extern ReturnCode ReadFile(UnityFileHandle handle, long size,
[MarshalAs(UnmanagedType.LPArray)] byte[] buffer, out long actualSize);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_SeekFile")]
public static extern ReturnCode SeekFile(UnityFileHandle handle, long offset, SeekOrigin origin, out long newPosition);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetFileSize")]
public static extern ReturnCode GetFileSize(UnityFileHandle handle, out long size);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_CloseFile")]
public static extern ReturnCode CloseFile(IntPtr handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_OpenSerializedFile")]
public static extern ReturnCode OpenSerializedFile([MarshalAs(UnmanagedType.LPStr)] string path, out SerializedFileHandle handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_CloseSerializedFile")]
public static extern ReturnCode CloseSerializedFile(IntPtr handle);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetExternalReferenceCount")]
public static extern ReturnCode GetExternalReferenceCount(SerializedFileHandle handle, out int count);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetExternalReference")]
public static extern ReturnCode GetExternalReference(SerializedFileHandle handle, int index, StringBuilder path, int pathLen, StringBuilder guid, out ExternalReferenceType type);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetObjectCount")]
public static extern ReturnCode GetObjectCount(SerializedFileHandle handle, out int count);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetObjectInfo")]
public static extern ReturnCode GetObjectInfo(SerializedFileHandle handle, [In, Out] ObjectInfo[] objectData, int len);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetTypeTree")]
public static extern ReturnCode GetTypeTree(SerializedFileHandle handle, long objectId, out TypeTreeHandle typeTree);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetRefTypeTypeTree")]
public static extern ReturnCode GetRefTypeTypeTree(SerializedFileHandle handle, [MarshalAs(UnmanagedType.LPStr)] string className,
[MarshalAs(UnmanagedType.LPStr)] string namespaceName, [MarshalAs(UnmanagedType.LPStr)] string assemblyName, out TypeTreeHandle typeTree);
[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetTypeTreeNodeInfo")]
public static extern ReturnCode GetTypeTreeNodeInfo(TypeTreeHandle handle, int node, StringBuilder type, int typeLen,
StringBuilder name, int nameLen, out int offset, out int size, [MarshalAs(UnmanagedType.U4)] out TypeTreeFlags flags,
[MarshalAs(UnmanagedType.U4)] out TypeTreeMetaFlags metaFlags, out int firstChildNode,
out int nextNode);
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace UnityFileApi
{
public struct ExternalReference
{
public string Path;
public string Guid;
public ExternalReferenceType Type;
}
public class SerializedFile : IDisposable
{
Lazy<List<ExternalReference>> m_ExternalReferences;
Lazy<ObjectInfo[]> m_Objects;
Dictionary<IntPtr, TypeTreeNode> m_TypeTreeCache = new Dictionary<IntPtr, TypeTreeNode>();
internal SerializedFileHandle m_Handle;
public IReadOnlyList<ExternalReference> ExternalReferences => m_ExternalReferences.Value.AsReadOnly();
public IReadOnlyList<ObjectInfo> Objects => Array.AsReadOnly(m_Objects.Value);
internal SerializedFile()
{
m_ExternalReferences = new Lazy<List<ExternalReference>>(GetExternalReferences);
m_Objects = new Lazy<ObjectInfo[]>(GetObjects);
}
public TypeTreeNode GetTypeTreeRoot(long objectId)
{
var r = DllWrapper.GetTypeTree(m_Handle, objectId, out var typeTreeHandle);
UnityFileSystem.HandleErrors(r);
if (m_TypeTreeCache.TryGetValue(typeTreeHandle.Handle, out var node))
{
return node;
}
node = new TypeTreeNode(typeTreeHandle, 0);
m_TypeTreeCache.Add(typeTreeHandle.Handle, node);
return node;
}
public TypeTreeNode GetRefTypeTypeTreeRoot(string className, string namespaceName, string assemblyName)
{
var r = DllWrapper.GetRefTypeTypeTree(m_Handle, className, namespaceName, assemblyName, out var typeTreeHandle);
UnityFileSystem.HandleErrors(r);
if (m_TypeTreeCache.TryGetValue(typeTreeHandle.Handle, out var node))
{
return node;
}
node = new TypeTreeNode(typeTreeHandle, 0);
m_TypeTreeCache.Add(typeTreeHandle.Handle, node);
return node;
}
private List<ExternalReference> GetExternalReferences()
{
var r = DllWrapper.GetExternalReferenceCount(m_Handle, out var count);
UnityFileSystem.HandleErrors(r);
var externalReferences = new List<ExternalReference>(count);
var path = new StringBuilder(512);
var guid = new StringBuilder(32);
for (var i = 0; i < count; ++i)
{
DllWrapper.GetExternalReference(m_Handle, i, path, path.Capacity, guid, out var externalReferenceType);
UnityFileSystem.HandleErrors(r);
externalReferences.Add(new ExternalReference() { Path = path.ToString(), Guid = guid.ToString(), Type = externalReferenceType });
}
return externalReferences;
}
private ObjectInfo[] GetObjects()
{
var r = DllWrapper.GetObjectCount(m_Handle, out var count);
UnityFileSystem.HandleErrors(r);
if (count == 0)
return null;
var objs = new ObjectInfo[count];
DllWrapper.GetObjectInfo(m_Handle, objs, count);
UnityFileSystem.HandleErrors(r);
return objs;
}
public void Dispose()
{
if (m_Handle != null && !m_Handle.IsInvalid)
{
m_Handle.Dispose();
}
}
}
}

View File

@ -0,0 +1,432 @@
using System;
using System.IO;
using System.Text;
namespace UnityFileApi
{
public class TextDumperTool
{
StringBuilder m_StringBuilder = new StringBuilder(1024);
bool m_SkipLargeArrays;
UnityFileReader m_Reader;
SerializedFile m_SerializedFile;
StreamWriter m_Writer;
public int Dump(string path, string outputPath, bool skipLargeArrays)
{
m_SkipLargeArrays = skipLargeArrays;
try
{
try
{
using var archive = UnityFileSystem.MountArchive(path, "/");
foreach (var node in archive.Nodes)
{
Console.WriteLine($"Processing {node.Path} {node.Size} {node.Flags}");
if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile))
{
using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(node.Path) + ".txt"), false))
{
OutputSerializedFile("/" + node.Path);
}
}
}
}
catch (NotSupportedException)
{
// Try as SerializedFile
using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(path) + ".txt"), false))
{
OutputSerializedFile(path);
}
}
}
catch (Exception e)
{
Console.WriteLine("Error!");
Console.Write($"{e.GetType()}: ");
Console.WriteLine(e.Message);
Console.WriteLine(e.StackTrace);
return 1;
}
return 0;
}
void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex = -1)
{
bool skipChildren = false;
if (!node.IsArray)
{
m_StringBuilder.Append(' ', level * 2);
if (level != 0)
{
m_StringBuilder.Append(node.Name);
if (arrayIndex >= 0)
{
m_StringBuilder.Append('[');
m_StringBuilder.Append(arrayIndex);
m_StringBuilder.Append(']');
}
m_StringBuilder.Append(' ');
m_StringBuilder.Append('(');
m_StringBuilder.Append(node.Type);
m_StringBuilder.Append(')');
}
else
{
m_StringBuilder.Append(node.Type);
}
// Basic data type.
if (node.IsBasicType)
{
m_StringBuilder.Append(' ');
m_StringBuilder.Append(ReadValue(node, offset));
offset += node.Size;
}
else if (node.Type == "string")
{
var stringSize = m_Reader.ReadInt32(offset);
m_StringBuilder.Append(' ');
m_StringBuilder.Append(m_Reader.ReadString(offset + 4, stringSize));
offset += stringSize + 4;
// Skip child nodes as they were already processed here.
skipChildren = true;
}
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
if (node.IsManagedReferenceRegistry)
{
DumpManagedReferenceRegistry(node, ref offset, level + 1);
// Skip child nodes as they were already processed here.
skipChildren = true;
}
}
else
{
DumpArray(node, ref offset, level);
// Skip child nodes as they were already processed here.
skipChildren = true;
}
if (!skipChildren)
{
foreach (var child in node.Children)
{
RecursiveDump(child, ref offset, level + 1);
}
}
if (
((int)node.MetaFlags & (int)TypeTreeMetaFlags.AlignBytes) != 0 ||
((int)node.MetaFlags & (int)TypeTreeMetaFlags.AnyChildUsesAlignBytes) != 0
)
{
offset = (offset + 3) & ~(3);
}
}
void DumpArray(TypeTreeNode node, ref long offset, int level)
{
// First child contains array size.
var sizeNode = node.Children[0];
// Second child contains array type information.
var dataNode = node.Children[1];
if (sizeNode.Size != 4 || !sizeNode.IsLeaf)
throw new Exception("Invalid array size");
var arraySize = m_Reader.ReadInt32(offset);
offset += 4;
m_StringBuilder.Append(' ', level * 2);
m_StringBuilder.Append("Array");
m_StringBuilder.Append('<');
m_StringBuilder.Append(dataNode.Type);
m_StringBuilder.Append(">[");
m_StringBuilder.Append(arraySize);
m_StringBuilder.Append(']');
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
if (arraySize > 0)
{
if (dataNode.IsBasicType)
{
m_StringBuilder.Append(' ', (level + 1) * 2);
if (arraySize > 256 && m_SkipLargeArrays)
{
m_StringBuilder.Append("<Skipped>");
offset += dataNode.Size * arraySize;
}
else
{
var array = ReadBasicTypeArray(dataNode, offset, arraySize);
offset += dataNode.Size * arraySize;
m_StringBuilder.Append(array.GetValue(0));
for (int i = 1; i < arraySize; ++i)
{
m_StringBuilder.Append(", ");
m_StringBuilder.Append(array.GetValue(i));
}
}
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
}
else
{
++level;
for (int i = 0; i < arraySize; ++i)
{
RecursiveDump(dataNode, ref offset, level, i);
}
}
}
}
void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level)
{
if (node.Children.Count < 2)
throw new Exception("Invalid ManagedReferenceRegistry");
// First child is version number.
var version = m_Reader.ReadInt32(offset);
RecursiveDump(node.Children[0], ref offset, level);
TypeTreeNode refTypeNode;
TypeTreeNode refObjData;
if (version == 1)
{
// Second child is the ReferencedObject.
var refObjNode = node.Children[1];
// And its children are the referenced type and data nodes.
refTypeNode = refObjNode.Children[0];
refObjData = refObjNode.Children[1];
int i = 0;
while (DumpManagedReferenceData(refTypeNode, refObjData, ref offset, level, i++))
{ }
}
else if (version == 2)
{
// Second child is the RefIds vector.
var refIdsVectorNode = node.Children[1];
if (refIdsVectorNode.Children.Count < 1 || refIdsVectorNode.Name != "RefIds")
throw new Exception("Invalid ManagedReferenceRegistry RefIds vector");
var refIdsArrayNode = refIdsVectorNode.Children[0];
if (refIdsArrayNode.Children.Count != 2 || !refIdsArrayNode.IsArray)
throw new Exception("Invalid ManagedReferenceRegistry RefIds array");
// First child is the array size.
int arraySize = m_Reader.ReadInt32(offset);
offset += 4;
// Second child is the ReferencedObject.
var refObjNode = refIdsArrayNode.Children[1];
for (int i = 0; i < arraySize; ++i)
{
// First child is the rid.
long rid = m_Reader.ReadInt64(offset);
offset += 8;
// And the next children are the referenced type and data nodes.
refTypeNode = refObjNode.Children[1];
refObjData = refObjNode.Children[2];
DumpManagedReferenceData(refTypeNode, refObjData, ref offset, level, rid);
}
}
else
{
throw new Exception($"Unsupported ManagedReferenceRegistry version {version}");
}
}
bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedTypeDataNode, ref long offset, int level, long id)
{
if (refTypeNode.Children.Count < 3)
throw new Exception("Invalid ReferencedManagedType");
m_StringBuilder.Append(' ', level * 2);
m_StringBuilder.Append($"rid(");
m_StringBuilder.Append(id);
m_StringBuilder.Append(") ReferencedObject");
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
++level;
var refTypeOffset = offset;
var stringSize = m_Reader.ReadInt32(offset);
var className = m_Reader.ReadString(offset + 4, stringSize);
offset += stringSize + 4;
offset = (offset + 3) & ~(3);
stringSize = m_Reader.ReadInt32(offset);
var namespaceName = m_Reader.ReadString(offset + 4, stringSize);
offset += stringSize + 4;
offset = (offset + 3) & ~(3);
stringSize = m_Reader.ReadInt32(offset);
var assemblyName = m_Reader.ReadString(offset + 4, stringSize);
offset += stringSize + 4;
offset = (offset + 3) & ~(3);
if (className == "Terminus" && namespaceName == "UnityEngine.DMAT" && assemblyName == "FAKE_ASM")
return false;
// Not the most efficient way, but it simplifies the code.
RecursiveDump(refTypeNode, ref refTypeOffset, level);
m_StringBuilder.Append(' ', level * 2);
m_StringBuilder.Append(referencedTypeDataNode.Name);
m_StringBuilder.Append(' ');
m_StringBuilder.Append(referencedTypeDataNode.Type);
m_StringBuilder.Append(' ');
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
if (id == -1 || id == -2)
{
m_StringBuilder.Append(' ', level * 2);
m_StringBuilder.Append(id == -1 ? " unknown" : " null");
m_Writer.WriteLine(m_StringBuilder);
m_StringBuilder.Clear();
return true;
}
var refTypeRoot = m_SerializedFile.GetRefTypeTypeTreeRoot(className, namespaceName, assemblyName);
// Dump the ReferencedObject using its own TypeTree, but skip the root.
foreach (var child in refTypeRoot.Children)
{
RecursiveDump(child, ref offset, level + 1);
}
return true;
}
void OutputSerializedFile(string path)
{
using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024))
using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path))
{
var i = 1;
m_Writer.WriteLine("External References");
foreach (var extRef in m_SerializedFile.ExternalReferences)
{
m_Writer.WriteLine($"path({i}): \"{extRef.Path}\" GUID: {extRef.Guid} Type: {(int)extRef.Type}");
++i;
}
m_Writer.WriteLine();
foreach (var obj in m_SerializedFile.Objects)
{
var root = m_SerializedFile.GetTypeTreeRoot(obj.Id);
var offset = obj.Offset;
m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) ");
RecursiveDump(root, ref offset, 0);
m_Writer.WriteLine();
}
}
}
string ReadValue(TypeTreeNode node, long offset)
{
switch (Type.GetTypeCode(node.CSharpType))
{
case TypeCode.Int32:
return m_Reader.ReadInt32(offset).ToString();
case TypeCode.UInt32:
return m_Reader.ReadUInt32(offset).ToString();
case TypeCode.Single:
return m_Reader.ReadFloat(offset).ToString();
case TypeCode.Double:
return m_Reader.ReadDouble(offset).ToString();
case TypeCode.Int16:
return m_Reader.ReadInt16(offset).ToString();
case TypeCode.UInt16:
return m_Reader.ReadUInt16(offset).ToString();
case TypeCode.Int64:
return m_Reader.ReadInt64(offset).ToString();
case TypeCode.UInt64:
return m_Reader.ReadUInt64(offset).ToString();
case TypeCode.SByte:
return m_Reader.ReadUInt8(offset).ToString();
case TypeCode.Byte:
case TypeCode.Char:
return m_Reader.ReadUInt8(offset).ToString();
case TypeCode.Boolean:
return (m_Reader.ReadUInt8(offset) != 0).ToString();
default:
throw new Exception($"Can't get value of {node.Type} type");
}
}
Array ReadBasicTypeArray(TypeTreeNode node, long offset, int arraySize)
{
// Special case for boolean arrays.
if (node.CSharpType == typeof(bool))
{
var tmpArray = new byte[arraySize];
var boolArray = new bool[arraySize];
m_Reader.ReadArray(offset, arraySize * node.Size, tmpArray);
for (int i = 0; i < arraySize; ++i)
{
boolArray[i] = tmpArray[i] != 0;
}
return boolArray;
}
else
{
var array = Array.CreateInstance(node.CSharpType, arraySize);
m_Reader.ReadArray(offset, arraySize * node.Size, array);
return array;
}
}
}
}

View File

@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace UnityFileApi
{
// A TypeTreeNode represents how a property of a serialized object was written to disk.
// See the TextDumper library for an example.
public class TypeTreeNode
{
int m_FirstChildNodeIndex;
int m_NextNodeIndex;
TypeTreeHandle m_Handle;
Lazy<List<TypeTreeNode>> m_Children;
Lazy<Type> m_CSharpType;
Lazy<bool> m_hasConstantSize;
// The type of the property (basic type like int or float or class names for objects)
public readonly string Type;
// The name of the property (e.g. m_IndexBuffer for a Mesh or m_Width for a Texture)
public readonly string Name;
// The size of the property (for basic types only, otherwise -1)
public readonly int Size;
// The offset of the property (mostly useless).
public readonly int Offset;
// Flags used for different things (e.g. field is an array, data alignment, etc.)
public readonly TypeTreeFlags Flags;
public readonly TypeTreeMetaFlags MetaFlags;
// Child nodes container.
public List<TypeTreeNode> Children => m_Children.Value;
// True if the field has no child.
public bool IsLeaf => m_FirstChildNodeIndex == 0;
// True if the field is a basic type. (int, float, char, etc.)
public bool IsBasicType => IsLeaf && Size > 0;
// True if the field is an array.
public bool IsArray => ((int)Flags & (int)TypeTreeFlags.IsArray) != 0;
// True if the field is a ManagedReferenceRegistry
public bool IsManagedReferenceRegistry => ((int)Flags & (int)TypeTreeFlags.IsManagedReferenceRegistry) != 0;
// C# type corresponding to the node type
public Type CSharpType => m_CSharpType.Value;
// True if the node has a constant size (it contains no array or other containers with variable size).
public bool HasConstantSize => m_hasConstantSize.Value;
[ThreadStatic]
static StringBuilder s_NodeType;
[ThreadStatic]
static StringBuilder s_NodeName;
// Properties are required to initialize the ThreadStatic members.
static StringBuilder NodeTypeBuilder
{
get
{
if (s_NodeType == null)
{
s_NodeType = new StringBuilder(512);
}
return s_NodeType;
}
}
static StringBuilder NodeNameBuilder
{
get
{
if (s_NodeName == null)
{
s_NodeName = new StringBuilder(512);
}
return s_NodeName;
}
}
internal TypeTreeNode(TypeTreeHandle typeTreeHandle, int nodeIndex)
{
m_Handle = typeTreeHandle;
var r = DllWrapper.GetTypeTreeNodeInfo(m_Handle, nodeIndex, NodeTypeBuilder, NodeTypeBuilder.Capacity, NodeNameBuilder, NodeNameBuilder.Capacity, out Offset, out Size, out Flags, out MetaFlags, out m_FirstChildNodeIndex, out m_NextNodeIndex);
UnityFileSystem.HandleErrors(r);
Type = NodeTypeBuilder.ToString();
Name = NodeNameBuilder.ToString();
m_Children = new Lazy<List<TypeTreeNode>>(GetChildren);
m_CSharpType = new Lazy<Type>(GetCSharpType);
m_hasConstantSize = new Lazy<bool>(GetHasConstantSize);
}
internal List<TypeTreeNode> GetChildren()
{
var children = new List<TypeTreeNode>();
var current = m_FirstChildNodeIndex;
while (current != 0)
{
var child = new TypeTreeNode(m_Handle, current);
children.Add(child);
current = child.m_NextNodeIndex;
}
return children;
}
bool GetHasConstantSize()
{
if (IsArray || CSharpType == typeof(string))
return false;
foreach (var child in Children)
{
if (!child.HasConstantSize)
return false;
}
return true;
}
Type GetCSharpType()
{
switch (Type)
{
case "int":
case "SInt32":
case "TypePtr":
return typeof(int);
case "unsigned int":
case "UInt32":
return typeof(uint);
case "float":
return typeof(float);
case "double":
return typeof(double);
case "SInt16":
return typeof(short);
case "UInt16":
return typeof(ushort);
case "SInt64":
return typeof(long);
case "FileSize":
case "UInt64":
return typeof(ulong);
case "SInt8":
return typeof(sbyte);
case "UInt8":
case "char":
return typeof(byte);
case "bool":
return typeof(bool);
case "string":
return typeof(string);
default:
{
if (Size == 8)
{
return typeof(long);
}
else if (Size == 4)
{
return typeof(int);
}
else if (Size == 2)
{
return typeof(short);
}
else if (Size == 1)
{
return typeof(sbyte);
}
return typeof(object);
}
}
throw new Exception($"Unknown type {Type}");
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace UnityFileApi
{
// An archive node is a file in an archive.
public struct ArchiveNode
{
public string Path;
public long Size;
public ArchiveNodeFlags Flags;
}
// Class used to open a Unity archive file (such as an AssetBundle).
public class UnityArchive : IDisposable
{
internal UnityArchiveHandle m_Handle;
Lazy<List<ArchiveNode>> m_Nodes;
public IReadOnlyList<ArchiveNode> Nodes => m_Nodes.Value.AsReadOnly();
internal UnityArchive()
{
m_Nodes = new Lazy<List<ArchiveNode>>(() => GetArchiveNodes());
}
List<ArchiveNode> GetArchiveNodes()
{
var r = DllWrapper.GetArchiveNodeCount(m_Handle, out var count);
UnityFileSystem.HandleErrors(r);
if (count == 0)
return null;
var nodes = new List<ArchiveNode>(count);
var path = new StringBuilder(512);
for (var i = 0; i < count; ++i)
{
DllWrapper.GetArchiveNode(m_Handle, i, path, path.Capacity, out var size, out var flags);
UnityFileSystem.HandleErrors(r);
nodes.Add(new ArchiveNode() { Path = path.ToString(), Size = size, Flags = flags });
}
return nodes;
}
public void Dispose()
{
if (m_Handle != null && !m_Handle.IsInvalid)
{
m_Handle.Dispose();
m_Nodes = new Lazy<List<ArchiveNode>>(() => GetArchiveNodes());
}
}
}
}

View File

@ -0,0 +1,46 @@
using System;
namespace UnityFileApi
{
// Use this class to read data from a Unity file.
public class UnityFile : IDisposable
{
internal UnityFile()
{
}
internal UnityFileHandle m_Handle;
public long Read(long size, byte[] buffer)
{
var r = DllWrapper.ReadFile(m_Handle, size, buffer, out var actualSize);
UnityFileSystem.HandleErrors(r);
return actualSize;
}
public long Seek(long offset, SeekOrigin origin = SeekOrigin.Begin)
{
var r = DllWrapper.SeekFile(m_Handle, offset, origin, out var newPosition);
UnityFileSystem.HandleErrors(r);
return newPosition;
}
public long GetSize()
{
// This could be a property but as it may throw an exception, it's probably better as a method.
var r = DllWrapper.GetFileSize(m_Handle, out var size);
UnityFileSystem.HandleErrors(r);
return size;
}
public void Dispose()
{
if (m_Handle != null && !m_Handle.IsInvalid)
{
m_Handle.Dispose();
}
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,139 @@
using System;
using System.IO;
using System.Text;
using Force.Crc32;
namespace UnityFileApi
{
// This class can be used to read typed data from a UnityFile. Is uses a buffer for better performance.
public class UnityFileReader : IDisposable
{
UnityFile m_File;
byte[] m_Buffer;
long m_BufferStartInFile;
long m_BufferEndInFile;
public long Length { get; }
public UnityFileReader(string path, int bufferSize)
{
m_Buffer = new byte[bufferSize];
m_BufferStartInFile = 0;
m_BufferEndInFile = 0;
m_File = UnityFileSystem.OpenFile(path);
Length = m_File.GetSize();
}
int GetBufferOffset(long fileOffset, int count)
{
// Should we update the buffer?
if (fileOffset < m_BufferStartInFile || fileOffset + count > m_BufferEndInFile)
{
if (count > m_Buffer.Length)
throw new IOException("Requested size is larger than cache size");
m_BufferStartInFile = m_File.Seek(fileOffset);
if (m_BufferStartInFile != fileOffset)
throw new IOException("Invalid file offset");
m_BufferEndInFile = m_File.Read(m_Buffer.Length, m_Buffer);
m_BufferEndInFile += m_BufferStartInFile;
}
return (int)(fileOffset - m_BufferStartInFile);
}
public void ReadArray(long fileOffset, int size, Array dest)
{
var offset = GetBufferOffset(fileOffset, size);
Buffer.BlockCopy(m_Buffer, offset, dest, 0, size);
}
public string ReadString(long fileOffset, int size)
{
var offset = GetBufferOffset(fileOffset, size);
return Encoding.Default.GetString(m_Buffer, offset, size);
}
public float ReadFloat(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 4);
return BitConverter.ToSingle(m_Buffer, offset);
}
public double ReadDouble(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 8);
return BitConverter.ToDouble(m_Buffer, offset);
}
public long ReadInt64(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 8);
return BitConverter.ToInt64(m_Buffer, offset);
}
public ulong ReadUInt64(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 8);
return BitConverter.ToUInt64(m_Buffer, offset);
}
public int ReadInt32(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 4);
return BitConverter.ToInt32(m_Buffer, offset);
}
public uint ReadUInt32(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 4);
return BitConverter.ToUInt32(m_Buffer, offset);
}
public short ReadInt16(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 2);
return BitConverter.ToInt16(m_Buffer, offset);
}
public ushort ReadUInt16(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 2);
return BitConverter.ToUInt16(m_Buffer, offset);
}
public sbyte ReadInt8(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 1);
return (sbyte)m_Buffer[offset];
}
public byte ReadUInt8(long fileOffset)
{
var offset = GetBufferOffset(fileOffset, 1);
return m_Buffer[offset];
}
public uint ComputeCRC(long fileOffset, int size, uint crc32 = 0)
{
var readSize = size > m_Buffer.Length ? m_Buffer.Length : size;
var readBytes = 0;
while (readBytes < size)
{
var offset = GetBufferOffset(fileOffset, readSize);
crc32 = Crc32Algorithm.Append(crc32, m_Buffer, offset, readSize);
readBytes += readSize;
}
return crc32;
}
public void Dispose()
{
m_File.Dispose();
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.IO;
namespace UnityFileApi
{
// This is the main entry point. Provides methods to mount archives and open files.
public static class UnityFileSystem
{
public static void Init()
{
// Initialize the native library.
var r = DllWrapper.Init();
if (r != ReturnCode.Success && r != ReturnCode.AlreadyInitialized)
{
HandleErrors(r);
}
}
public static void Cleanup()
{
// Uninitialize the native library.
var r = DllWrapper.Cleanup();
if (r != ReturnCode.Success && r != ReturnCode.NotInitialized)
{
HandleErrors(r);
}
}
public static UnityArchive MountArchive(string path, string mountPoint)
{
var r = DllWrapper.MountArchive(path, mountPoint, out var handle);
HandleErrors(r, path);
return new UnityArchive() { m_Handle = handle };
}
public static UnityFile OpenFile(string path)
{
var r = DllWrapper.OpenFile(path, out var handle);
UnityFileSystem.HandleErrors(r, path);
return new UnityFile() { m_Handle = handle };
}
public static SerializedFile OpenSerializedFile(string path)
{
var r = DllWrapper.OpenSerializedFile(path, out var handle);
UnityFileSystem.HandleErrors(r, path);
return new SerializedFile() { m_Handle = handle };
}
internal static void HandleErrors(ReturnCode returnCode, string filename = "")
{
switch (returnCode)
{
case ReturnCode.AlreadyInitialized:
throw new InvalidOperationException("UnityFileSystem is already initialized.");
case ReturnCode.NotInitialized:
throw new InvalidOperationException("UnityFileSystem is not initialized.");
case ReturnCode.FileNotFound:
throw new FileNotFoundException("File not found.", filename);
case ReturnCode.FileFormatError:
throw new NotSupportedException($"Invalid file format reading {filename}.");
case ReturnCode.InvalidArgument:
throw new ArgumentException();
case ReturnCode.HigherSerializedFileVersion:
throw new NotSupportedException("SerializedFile version not supported.");
case ReturnCode.DestinationBufferTooSmall:
throw new ArgumentException("Destination buffer too small.");
case ReturnCode.InvalidObjectId:
throw new ArgumentException("Invalid object id.");
case ReturnCode.UnknownError:
throw new Exception("Unknown error.");
case ReturnCode.FileError:
throw new IOException("File operation error.");
case ReturnCode.TypeNotFound:
throw new ArgumentException("Type not found.");
}
}
}
}

Binary file not shown.