diff --git a/Analyzer/AnalyzerTool.cs b/Analyzer/AnalyzerTool.cs index a293b41..3245d86 100644 --- a/Analyzer/AnalyzerTool.cs +++ b/Analyzer/AnalyzerTool.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using UnityDataTools.Analyzer.SQLite; using UnityDataTools.FileSystem; @@ -125,7 +126,9 @@ bool ProcessFile(string file, string rootDirectory, SQLiteWriter writer, int fil { var assetBundleName = Path.GetRelativePath(rootDirectory, file); - writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); + long abContentSize = archive.Nodes.Sum(node => (long)node.Size); + + writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length, abContentSize); ReportProgress(assetBundleName, fileIndex, cntFiles); foreach (var node in archive.Nodes) diff --git a/Analyzer/IWriter.cs b/Analyzer/IWriter.cs index 7700c08..8df8463 100644 --- a/Analyzer/IWriter.cs +++ b/Analyzer/IWriter.cs @@ -8,7 +8,7 @@ namespace UnityDataTools.Analyzer; public interface IWriter : IDisposable { void Begin(); - void BeginAssetBundle(string name, long size); + void BeginAssetBundle(string name, long size, long uncompressedSize); void EndAssetBundle(); void WriteSerializedFile(string relativePath, string fullPath, string containingFolder); void End(); diff --git a/Analyzer/Resources/Init.sql b/Analyzer/Resources/Init.sql index a41a44a..87b41a4 100644 --- a/Analyzer/Resources/Init.sql +++ b/Analyzer/Resources/Init.sql @@ -5,11 +5,23 @@ CREATE TABLE types PRIMARY KEY (id) ); +CREATE TABLE typetree_hashes +( + hash TEXT, + count INTEGER, + serialized_size INTEGER, + category TEXT, + type_id INTEGER, + qualified_name TEXT, + PRIMARY KEY (hash) +); + CREATE TABLE asset_bundles ( id INTEGER, name TEXT, file_size INTEGER, + content_size INTEGER, PRIMARY KEY (id) ); @@ -103,5 +115,15 @@ WHERE m.type = "Material"; INSERT INTO types (id, name) VALUES (-1, 'Scene'); +CREATE VIEW view_typetree_summary AS +SELECT + COUNT(*) AS total_typetrees, + SUM(count) AS total_instances, + SUM(count * serialized_size) AS total_memory_with_duplicates, + SUM(serialized_size) AS total_memory_deduplicated, + SUM(CASE WHEN count > 1 THEN count - 1 ELSE 0 END) AS duplicate_count, + SUM(count * serialized_size) - SUM(serialized_size) AS memory_wasted_by_duplicates +FROM typetree_hashes; + PRAGMA synchronous = OFF; PRAGMA journal_mode = MEMORY; diff --git a/Analyzer/SQLite/Handlers/AnimationClipHandler.cs b/Analyzer/SQLite/Handlers/AnimationClipHandler.cs index d00c60f..a96a56c 100644 --- a/Analyzer/SQLite/Handlers/AnimationClipHandler.cs +++ b/Analyzer/SQLite/Handlers/AnimationClipHandler.cs @@ -5,11 +5,11 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class AnimationClipHandler : ISQLiteHandler +public class AnimationClipHandler : SQLiteHandlerBase { SqliteCommand m_InsertCommand; - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { using var command = db.CreateCommand(); command.CommandText = Properties.Resources.AnimationClip; @@ -22,7 +22,7 @@ public void Init(SqliteConnection db) m_InsertCommand.Parameters.Add("@events", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var animationClip = AnimationClip.Read(reader); m_InsertCommand.Transaction = ctx.Transaction; @@ -35,11 +35,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SqliteConnection db) - { - } - - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); } diff --git a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs index 80bbc6b..c228d2c 100644 --- a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs +++ b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs @@ -6,13 +6,13 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class AssetBundleHandler : ISQLiteHandler +public class AssetBundleHandler : SQLiteHandlerBase { SqliteCommand m_InsertCommand; private SqliteCommand m_InsertDepCommand; private Regex m_SceneNameRegex = new Regex(@"([^//]+)\.unity"); - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { using var command = db.CreateCommand(); command.CommandText = Properties.Resources.AssetBundle; @@ -31,7 +31,7 @@ public void Init(SqliteConnection db) m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var assetBundle = AssetBundle.Read(reader); @@ -77,7 +77,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SqliteConnection db) + public override void Finalize(SqliteConnection db) { using var command = new SqliteCommand(); command.Connection = db; @@ -88,7 +88,7 @@ public void Finalize(SqliteConnection db) command.ExecuteNonQuery(); } - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); m_InsertDepCommand?.Dispose(); diff --git a/Analyzer/SQLite/Handlers/AudioClipHandler.cs b/Analyzer/SQLite/Handlers/AudioClipHandler.cs index cfd542e..52d708a 100644 --- a/Analyzer/SQLite/Handlers/AudioClipHandler.cs +++ b/Analyzer/SQLite/Handlers/AudioClipHandler.cs @@ -6,11 +6,11 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class AudioClipHandler : ISQLiteHandler +public class AudioClipHandler : SQLiteHandlerBase { private SqliteCommand m_InsertCommand; - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { using var command = db.CreateCommand(); command.CommandText = Properties.Resources.AudioClip; @@ -26,7 +26,7 @@ public void Init(SqliteConnection db) m_InsertCommand.Parameters.Add("@format", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var audioClip = AudioClip.Read(reader); m_InsertCommand.Transaction = ctx.Transaction; @@ -43,11 +43,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s name = audioClip.Name; } - public void Finalize(SqliteConnection db) - { - } - - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); } diff --git a/Analyzer/SQLite/Handlers/ISQLiteHandler.cs b/Analyzer/SQLite/Handlers/ISQLiteHandler.cs index ba6dfcf..7c9c885 100644 --- a/Analyzer/SQLite/Handlers/ISQLiteHandler.cs +++ b/Analyzer/SQLite/Handlers/ISQLiteHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.Data.Sqlite; +using UnityDataTools.FileSystem; using UnityDataTools.FileSystem.TypeTreeReaders; namespace UnityDataTools.Analyzer.SQLite.Handlers; @@ -16,6 +17,27 @@ public class Context public SqliteTransaction Transaction { get; set; } } +public abstract class SQLiteHandlerBase : IDisposable +{ + public abstract void Init(SqliteConnection db); + + // Override if you want object-level processing + public virtual void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, + out string name, out long streamDataSize) + { + name = string.Empty; + streamDataSize = 0; + } + + // Override if you want SerializedFile-level processing + public virtual void ProcessSerializedFile(SerializedFile sf, SqliteTransaction transaction) { } + + public virtual void Finalize(SqliteConnection db) { } + + public abstract void Dispose(); +} + +// Keep old interface for backward compatibility during migration public interface ISQLiteHandler : IDisposable { void Init(Microsoft.Data.Sqlite.SqliteConnection db); diff --git a/Analyzer/SQLite/Handlers/MeshHandler.cs b/Analyzer/SQLite/Handlers/MeshHandler.cs index 1845b7f..f5e1629 100644 --- a/Analyzer/SQLite/Handlers/MeshHandler.cs +++ b/Analyzer/SQLite/Handlers/MeshHandler.cs @@ -6,11 +6,11 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class MeshHandler : ISQLiteHandler +public class MeshHandler : SQLiteHandlerBase { SqliteCommand m_InsertCommand; - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { using var command = db.CreateCommand(); command.CommandText = Properties.Resources.Mesh; @@ -30,7 +30,7 @@ public void Init(SqliteConnection db) m_InsertCommand.Parameters.Add("@channels", SqliteType.Text); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var mesh = Mesh.Read(reader); m_InsertCommand.Transaction = ctx.Transaction; @@ -63,11 +63,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s name = mesh.Name; } - public void Finalize(SqliteConnection db) - { - } - - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); } diff --git a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs index 75f1420..aa33246 100644 --- a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs +++ b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs @@ -6,11 +6,11 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class PreloadDataHandler : ISQLiteHandler +public class PreloadDataHandler : SQLiteHandlerBase { private SqliteCommand m_InsertDepCommand; - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { m_InsertDepCommand = db.CreateCommand(); m_InsertDepCommand.Connection = db; @@ -19,7 +19,7 @@ public void Init(SqliteConnection db) m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var preloadData = PreloadData.Read(reader); m_InsertDepCommand.Transaction = ctx.Transaction; @@ -38,11 +38,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SqliteConnection db) - { - } - - void IDisposable.Dispose() + public override void Dispose() { m_InsertDepCommand?.Dispose(); } diff --git a/Analyzer/SQLite/Handlers/ShaderHandler.cs b/Analyzer/SQLite/Handlers/ShaderHandler.cs index 6731aad..c87de82 100644 --- a/Analyzer/SQLite/Handlers/ShaderHandler.cs +++ b/Analyzer/SQLite/Handlers/ShaderHandler.cs @@ -5,7 +5,7 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class ShaderHandler : ISQLiteHandler +public class ShaderHandler : SQLiteHandlerBase { private SqliteCommand m_InsertCommand; private SqliteCommand m_InsertSubProgramCommand; @@ -15,7 +15,7 @@ public class ShaderHandler : ISQLiteHandler static long s_SubProgramId = 0; static Dictionary s_GlobalKeywords = new(); - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { s_SubProgramId = 0; s_GlobalKeywords.Clear(); @@ -53,7 +53,7 @@ public void Init(SqliteConnection db) m_InsertSubProgramKeywordsCommand.Parameters.Add("@keyword_id", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var shader = SerializedObjects.Shader.Read(reader); var uniquePrograms = new HashSet(); @@ -143,7 +143,7 @@ private int GetKeywordId(string keyword, SqliteTransaction ctxTransaction) return id; } - public void Finalize(SqliteConnection db) + public override void Finalize(SqliteConnection db) { using var command = new SqliteCommand(); command.Connection = db; @@ -154,7 +154,7 @@ public void Finalize(SqliteConnection db) command.ExecuteNonQuery(); } - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); m_InsertSubProgramCommand?.Dispose(); diff --git a/Analyzer/SQLite/Handlers/Texture2DHandler.cs b/Analyzer/SQLite/Handlers/Texture2DHandler.cs index fe26f1a..33ffaba 100644 --- a/Analyzer/SQLite/Handlers/Texture2DHandler.cs +++ b/Analyzer/SQLite/Handlers/Texture2DHandler.cs @@ -5,11 +5,11 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; -public class Texture2DHandler : ISQLiteHandler +public class Texture2DHandler : SQLiteHandlerBase { SqliteCommand m_InsertCommand = new SqliteCommand(); - public void Init(SqliteConnection db) + public override void Init(SqliteConnection db) { using var command = db.CreateCommand(); command.CommandText = Properties.Resources.Texture2D; @@ -25,7 +25,7 @@ public void Init(SqliteConnection db) m_InsertCommand.Parameters.Add("@mip_count", SqliteType.Integer); } - public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + public override void ProcessObject(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var texture2d = Texture2D.Read(reader); m_InsertCommand.Transaction = ctx.Transaction; @@ -41,11 +41,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = texture2d.StreamDataSize; } - public void Finalize(SqliteConnection db) - { - } - - void IDisposable.Dispose() + public override void Dispose() { m_InsertCommand?.Dispose(); } diff --git a/Analyzer/SQLite/Handlers/TypeTreeHashHandler.cs b/Analyzer/SQLite/Handlers/TypeTreeHashHandler.cs new file mode 100644 index 0000000..96c8ade --- /dev/null +++ b/Analyzer/SQLite/Handlers/TypeTreeHashHandler.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.FileSystem; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SQLite.Handlers; + +internal class TypeTreeHashData +{ + public int Count; + public int SerializedSize; + public string Category; + public int TypeId; + public string QualifiedName; +} + +public class TypeTreeHashHandler : SQLiteHandlerBase +{ + private Dictionary m_TypeTreeHashes = new(); + private SqliteCommand m_InsertCommand; + + public override void Init(SqliteConnection db) + { + // Table is already created in Init.sql + m_InsertCommand = db.CreateCommand(); + m_InsertCommand.CommandText = "INSERT INTO typetree_hashes (hash, count, serialized_size, category, type_id, qualified_name) VALUES (@hash, @count, @serialized_size, @category, @type_id, @qualified_name)"; + m_InsertCommand.Parameters.Add("@hash", SqliteType.Text); + m_InsertCommand.Parameters.Add("@count", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@serialized_size", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@category", SqliteType.Text); + m_InsertCommand.Parameters.Add("@type_id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@qualified_name", SqliteType.Text); + } + + public override void ProcessSerializedFile(SerializedFile sf, SqliteTransaction transaction) + { + int typeTreeCount = sf.GetTypeTreeCount(); + + for (int i = 0; i < typeTreeCount; i++) + { + TypeTreeInfo info = sf.GetTypeTreeInfo(i); + + // Convert hash uint32[4] to hex string + string hashString = $"{info.Hash0:x8}{info.Hash1:x8}{info.Hash2:x8}{info.Hash3:x8}"; + + // Build qualified name + string qualifiedName; + if (info.Category == TypeTreeCategory.RefType) + { + // For RefType: namespace.class (from assembly) + if (!string.IsNullOrEmpty(info.NamespaceName)) + qualifiedName = $"{info.NamespaceName}.{info.ClassName}"; + else + qualifiedName = info.ClassName; + } + else + { + // For ObjectType: use the Unity type name from TypeId + qualifiedName = info.ClassName; + } + + if (m_TypeTreeHashes.TryGetValue(hashString, out var existingData)) + { + // Already seen this hash - increment count + existingData.Count++; + } + else + { + // First time seeing this hash - add to dictionary + m_TypeTreeHashes[hashString] = new TypeTreeHashData + { + Count = 1, + SerializedSize = info.SerializedSize, + Category = info.Category.ToString(), + TypeId = info.TypeId, + QualifiedName = qualifiedName + }; + } + } + } + + public override void Finalize(SqliteConnection db) + { + // Write all TypeTree hash data to database + using (var transaction = db.BeginTransaction()) + { + m_InsertCommand.Transaction = transaction; + foreach (var kvp in m_TypeTreeHashes) + { + m_InsertCommand.Parameters["@hash"].Value = kvp.Key; + m_InsertCommand.Parameters["@count"].Value = kvp.Value.Count; + m_InsertCommand.Parameters["@serialized_size"].Value = kvp.Value.SerializedSize; + m_InsertCommand.Parameters["@category"].Value = kvp.Value.Category; + m_InsertCommand.Parameters["@type_id"].Value = kvp.Value.TypeId; + m_InsertCommand.Parameters["@qualified_name"].Value = kvp.Value.QualifiedName; + m_InsertCommand.ExecuteNonQuery(); + } + transaction.Commit(); + } + } + + public override void Dispose() + { + m_InsertCommand?.Dispose(); + } +} diff --git a/Analyzer/SQLite/SQLiteWriter.cs b/Analyzer/SQLite/SQLiteWriter.cs index a463f3e..40fd676 100644 --- a/Analyzer/SQLite/SQLiteWriter.cs +++ b/Analyzer/SQLite/SQLiteWriter.cs @@ -27,7 +27,7 @@ public class SQLiteWriter : IWriter // Used to map PPtr fileId to its corresponding serialized file id in the database. Dictionary m_LocalToDbFileId = new (); - private Dictionary m_Handlers = new () + private Dictionary m_Handlers = new () { { "Mesh", new MeshHandler() }, { "Texture2D", new Texture2DHandler() }, @@ -36,6 +36,7 @@ public class SQLiteWriter : IWriter { "AnimationClip", new AnimationClipHandler() }, { "AssetBundle", new AssetBundleHandler() }, { "PreloadData", new PreloadDataHandler() }, + { "TypeTreeHash", new TypeTreeHashHandler() }, }; private SqliteConnection m_Database; @@ -108,10 +109,11 @@ public void End() private void CreateSQLiteCommands() { m_AddAssetBundleCommand = m_Database.CreateCommand(); - m_AddAssetBundleCommand.CommandText = "INSERT INTO asset_bundles (id, name, file_size) VALUES (@id, @name, @file_size)"; + m_AddAssetBundleCommand.CommandText = "INSERT INTO asset_bundles (id, name, file_size, content_size) VALUES (@id, @name, @file_size, @content_size)"; m_AddAssetBundleCommand.Parameters.Add("@id", SqliteType.Integer); m_AddAssetBundleCommand.Parameters.Add("@name", SqliteType.Text); m_AddAssetBundleCommand.Parameters.Add("@file_size", SqliteType.Integer); + m_AddAssetBundleCommand.Parameters.Add("@content_size", SqliteType.Integer); m_AddSerializedFileCommand = m_Database.CreateCommand(); m_AddSerializedFileCommand.CommandText = "INSERT INTO serialized_files (id, asset_bundle, name) VALUES (@id, @asset_bundle, @name)"; @@ -148,7 +150,7 @@ private void CreateSQLiteCommands() m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); } - public void BeginAssetBundle(string name, long size) + public void BeginAssetBundle(string name, long size, long uncompressedContentSize) { if (m_CurrentAssetBundleId != -1) { @@ -159,6 +161,7 @@ public void BeginAssetBundle(string name, long size) m_AddAssetBundleCommand.Parameters["@id"].Value = m_CurrentAssetBundleId; m_AddAssetBundleCommand.Parameters["@name"].Value = name; m_AddAssetBundleCommand.Parameters["@file_size"].Value = size; + m_AddAssetBundleCommand.Parameters["@content_size"].Value = uncompressedContentSize; m_AddAssetBundleCommand.ExecuteNonQuery(); } @@ -183,6 +186,12 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con using var transaction = m_Database.BeginTransaction(); m_CurrentTransaction = transaction; + // Call SerializedFile-level handlers + foreach (var handler in m_Handlers.Values) + { + handler.ProcessSerializedFile(sf, transaction); + } + var match = m_RegexSceneFile.Match(relativePath); if (match.Success) @@ -268,7 +277,7 @@ public void WriteSerializedFile(string relativePath, string fullPath, string con if (m_Handlers.TryGetValue(root.Type, out var handler)) { - handler.Process(ctx, currentObjectId, randomAccessReader, + handler.ProcessObject(ctx, currentObjectId, randomAccessReader, out name, out streamDataSize); } else if (randomAccessReader.HasChild("m_Name")) diff --git a/UnityFileSystem/DllWrapper.cs b/UnityFileSystem/DllWrapper.cs index ea16660..a33ef0b 100644 --- a/UnityFileSystem/DllWrapper.cs +++ b/UnityFileSystem/DllWrapper.cs @@ -138,6 +138,33 @@ public enum TypeTreeMetaFlags AnyChildUsesAlignBytes = 1 << 15, } +public enum TypeTreeCategory +{ + ObjectType = 0, + RefType = 1, +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] +public struct TypeTreeInfo +{ + public int TypeId; + public int SerializedSize; + public TypeTreeCategory Category; + public uint Hash0; + public uint Hash1; + public uint Hash2; + public uint Hash3; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string ClassName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string NamespaceName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string AssemblyName; +} + public static class DllWrapper { [DllImport("UnityFileSystemApi", @@ -250,4 +277,19 @@ public static extern ReturnCode GetTypeTreeNodeInfo(TypeTreeHandle handle, int n 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); + + [DllImport("UnityFileSystemApi", + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "UFS_GetTypeTreeCount")] + public static extern ReturnCode GetTypeTreeCount(SerializedFileHandle handle, out int count); + + [DllImport("UnityFileSystemApi", + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "UFS_GetTypeTreeInfo")] + public static extern ReturnCode GetTypeTreeInfo(SerializedFileHandle handle, int index, out TypeTreeInfo info); + + [DllImport("UnityFileSystemApi", + CallingConvention = CallingConvention.Cdecl, + EntryPoint = "UFS_GetTypeTreeByIndex")] + public static extern ReturnCode GetTypeTreeByIndex(SerializedFileHandle handle, int index, out TypeTreeHandle typeTree); } \ No newline at end of file diff --git a/UnityFileSystem/SerializedFile.cs b/UnityFileSystem/SerializedFile.cs index 829ea37..4e80644 100644 --- a/UnityFileSystem/SerializedFile.cs +++ b/UnityFileSystem/SerializedFile.cs @@ -96,6 +96,36 @@ private ObjectInfo[] GetObjects() return objs; } + public int GetTypeTreeCount() + { + var r = DllWrapper.GetTypeTreeCount(m_Handle, out var count); + UnityFileSystem.HandleErrors(r); + return count; + } + + public TypeTreeInfo GetTypeTreeInfo(int index) + { + var r = DllWrapper.GetTypeTreeInfo(m_Handle, index, out var info); + UnityFileSystem.HandleErrors(r); + return info; + } + + public TypeTreeNode GetTypeTreeByIndex(int index) + { + var r = DllWrapper.GetTypeTreeByIndex(m_Handle, index, 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 void Dispose() { if (m_Handle != null && !m_Handle.IsInvalid) diff --git a/UnityFileSystem/UnityFileSystemApi.dll b/UnityFileSystem/UnityFileSystemApi.dll index 8f1a1d1..ec02516 100644 Binary files a/UnityFileSystem/UnityFileSystemApi.dll and b/UnityFileSystem/UnityFileSystemApi.dll differ