├── AddressablesTools ├── icon.png ├── CatalogFileType.cs ├── Classes │ ├── TypeReference.cs │ ├── Hash128.cs │ └── AssetBundleRequestOptions.cs ├── AddressablesTools.csproj ├── JSON │ ├── SerializedTypeJson.cs │ ├── ObjectInitializationDataJson.cs │ └── ContentCatalogDataJson.cs ├── Catalog │ ├── WrappedSerializedObject.cs │ ├── ClassJsonObject.cs │ ├── SerializedTypeAsmContainer.cs │ ├── ObjectInitializationData.cs │ ├── SerializedType.cs │ ├── ResourceLocation.cs │ ├── SerializedObjectDecoder.cs │ └── ContentCatalogData.cs ├── AssetsTools.NET.Addressables.nuspec ├── Binary │ ├── ContentCatalogDataBinaryHeader.cs │ ├── CatalogBinaryReader.cs │ └── CatalogBinaryWriter.cs └── AddressablesCatalogFileParser.cs ├── Example ├── Example.csproj └── Program.cs ├── .github └── workflows │ ├── build-ubuntu.yml │ └── build-windows.yml ├── AddressablesTools.sln ├── readme.txt └── .gitignore /AddressablesTools/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesrak1/AddressablesTools/HEAD/AddressablesTools/icon.png -------------------------------------------------------------------------------- /AddressablesTools/CatalogFileType.cs: -------------------------------------------------------------------------------- 1 | namespace AddressablesTools 2 | { 3 | public enum CatalogFileType 4 | { 5 | None, 6 | Json, 7 | Binary 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AddressablesTools/Classes/TypeReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AddressablesTools.Classes 4 | { 5 | public class TypeReference 6 | { 7 | public string Clsid { get; set; } 8 | 9 | public TypeReference(string clsid) 10 | { 11 | Clsid = clsid; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AddressablesTools/AddressablesTools.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /AddressablesTools/JSON/SerializedTypeJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AddressablesTools.JSON 6 | { 7 | #pragma warning disable IDE1006 8 | internal class SerializedTypeJson 9 | { 10 | public string m_AssemblyName { get; set; } 11 | public string m_ClassName { get; set; } 12 | } 13 | #pragma warning restore IDE1006 14 | } 15 | -------------------------------------------------------------------------------- /Example/Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/WrappedSerializedObject.cs: -------------------------------------------------------------------------------- 1 | namespace AddressablesTools.Catalog 2 | { 3 | public class WrappedSerializedObject 4 | { 5 | public SerializedType Type { get; set; } 6 | public object Object { get; set; } 7 | 8 | public WrappedSerializedObject(SerializedType type, object obj) 9 | { 10 | Type = type; 11 | Object = obj; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AddressablesTools/JSON/ObjectInitializationDataJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AddressablesTools.JSON 6 | { 7 | #pragma warning disable IDE1006 8 | internal class ObjectInitializationDataJson 9 | { 10 | public string m_Id { get; set; } 11 | public SerializedTypeJson m_ObjectType { get; set; } 12 | public string m_Data { get; set; } 13 | } 14 | #pragma warning restore IDE1006 15 | } 16 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/ClassJsonObject.cs: -------------------------------------------------------------------------------- 1 | namespace AddressablesTools.Catalog 2 | { 3 | public class ClassJsonObject 4 | { 5 | public SerializedType Type { get; set; } 6 | public string JsonText { get; set; } 7 | 8 | public ClassJsonObject(string assemblyName, string className, string jsonText) 9 | { 10 | Type = new SerializedType 11 | { 12 | AssemblyName = assemblyName, 13 | ClassName = className, 14 | }; 15 | JsonText = jsonText; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/SerializedTypeAsmContainer.cs: -------------------------------------------------------------------------------- 1 | namespace AddressablesTools.Catalog; 2 | public class SerializedTypeAsmContainer 3 | { 4 | public string StandardLibAsm { get; set; } 5 | public string Hash128Asm { get; set; } 6 | public string AbroAsm { get; set; } 7 | 8 | public static SerializedTypeAsmContainer ForNet40() 9 | { 10 | return new SerializedTypeAsmContainer() 11 | { 12 | StandardLibAsm = "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", 13 | Hash128Asm = "UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", 14 | AbroAsm = "Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build-ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Build AddressablesTools Example Ubuntu 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install .NET Core 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 8.0.x 23 | 24 | - name: Restore 25 | run: dotnet restore 26 | 27 | - name: Build 28 | run: dotnet build --configuration Release --no-restore 29 | 30 | - name: Upload 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: addrtool-example-ubuntu 34 | path: Example/bin/Release/net8.0 -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | name: Build AddressablesTools Example Windows 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-2022 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install .NET Core 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 8.0.x 23 | 24 | - name: Setup MSBuild.exe 25 | uses: microsoft/setup-msbuild@v1.1 26 | 27 | - name: Build 28 | run: msbuild AddressablesTools.sln /restore /p:Configuration=Release 29 | 30 | - name: Upload 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: addrtool-example-windows 34 | path: Example/bin/Release/net8.0 -------------------------------------------------------------------------------- /AddressablesTools/AssetsTools.NET.Addressables.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AssetsTools.NET.Addressables 5 | 3.0.2 6 | nesrak1 7 | nesrak1 8 | false 9 | MIT 10 | https://licenses.nuget.org/MIT 11 | icon.png 12 | Read and write addressables 13 | Read and write addressables from outside of the editor 14 | 3.0.2 - improved binary reading/writing, fixes with internal prefixes 15 | https://github.com/nesrak1/AddressablesTools 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AddressablesTools/JSON/ContentCatalogDataJson.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace AddressablesTools.JSON 4 | { 5 | #pragma warning disable IDE1006 6 | internal class ContentCatalogDataJson 7 | { 8 | public string m_LocatorId { get; set; } 9 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 10 | public string m_BuildResultHash { get; set; } // ??? 11 | public ObjectInitializationDataJson m_InstanceProviderData { get; set; } 12 | public ObjectInitializationDataJson m_SceneProviderData { get; set; } 13 | public ObjectInitializationDataJson[] m_ResourceProviderData { get; set; } 14 | public string[] m_ProviderIds { get; set; } 15 | public string[] m_InternalIds { get; set; } 16 | public string m_KeyDataString { get; set; } 17 | public string m_BucketDataString { get; set; } 18 | public string m_EntryDataString { get; set; } 19 | public string m_ExtraDataString { get; set; } 20 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 21 | public string[] m_Keys { get; set; } // 1.1.3 - 1.16.10 22 | public SerializedTypeJson[] m_resourceTypes { get; set; } 23 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 24 | public string[] m_InternalIdPrefixes { get; set; } // 1.16.10 - 1.21.3 (if compact) 25 | } 26 | #pragma warning restore IDE1006 27 | } 28 | -------------------------------------------------------------------------------- /AddressablesTools/Classes/Hash128.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using System; 3 | using System.Buffers.Binary; 4 | 5 | namespace AddressablesTools.Classes 6 | { 7 | public class Hash128 8 | { 9 | public string Value { get; set; } 10 | 11 | public Hash128(string value) 12 | { 13 | Value = value; 14 | } 15 | 16 | public Hash128(uint v0, uint v1, uint v2, uint v3) 17 | { 18 | byte[] data = new byte[16]; 19 | Span dataSpan = data.AsSpan(); 20 | // we already read with the correct endianness 21 | BinaryPrimitives.WriteUInt32BigEndian(dataSpan[0..], v0); 22 | BinaryPrimitives.WriteUInt32BigEndian(dataSpan[4..], v1); 23 | BinaryPrimitives.WriteUInt32BigEndian(dataSpan[8..], v2); 24 | BinaryPrimitives.WriteUInt32BigEndian(dataSpan[12..], v3); 25 | Value = Convert.ToHexString(data).ToLowerInvariant(); 26 | } 27 | 28 | internal uint Write(CatalogBinaryWriter writer) 29 | { 30 | byte[] data = new byte[16]; 31 | Span dataSpan = data.AsSpan(); 32 | BinaryPrimitives.WriteUInt32LittleEndian(dataSpan[0..], Convert.ToUInt32(Value[0..8], 16)); 33 | BinaryPrimitives.WriteUInt32LittleEndian(dataSpan[4..], Convert.ToUInt32(Value[8..16], 16)); 34 | BinaryPrimitives.WriteUInt32LittleEndian(dataSpan[8..], Convert.ToUInt32(Value[16..24], 16)); 35 | BinaryPrimitives.WriteUInt32LittleEndian(dataSpan[12..], Convert.ToUInt32(Value[24..32], 16)); 36 | return writer.WriteWithCache(data); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AddressablesTools.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AddressablesTools", "AddressablesTools\AddressablesTools.csproj", "{C622872F-67B4-42D5-8035-02F3F1953A3F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example\Example.csproj", "{B021C089-1B6A-4A5A-BB9D-2BA866445128}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C622872F-67B4-42D5-8035-02F3F1953A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C622872F-67B4-42D5-8035-02F3F1953A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C622872F-67B4-42D5-8035-02F3F1953A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C622872F-67B4-42D5-8035-02F3F1953A3F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B021C089-1B6A-4A5A-BB9D-2BA866445128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B021C089-1B6A-4A5A-BB9D-2BA866445128}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B021C089-1B6A-4A5A-BB9D-2BA866445128}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B021C089-1B6A-4A5A-BB9D-2BA866445128}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {AE108FCE-D30A-4056-8D56-9F96A04D71D5} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/ObjectInitializationData.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using AddressablesTools.JSON; 3 | using System; 4 | using System.Buffers.Binary; 5 | 6 | namespace AddressablesTools.Catalog 7 | { 8 | public class ObjectInitializationData 9 | { 10 | public string Id { get; set; } 11 | public SerializedType ObjectType { get; set; } 12 | public string Data { get; set; } 13 | 14 | internal void Read(ObjectInitializationDataJson obj) 15 | { 16 | Id = obj.m_Id; 17 | ObjectType = new SerializedType(); 18 | ObjectType.Read(obj.m_ObjectType); 19 | Data = obj.m_Data; 20 | } 21 | 22 | internal void Read(CatalogBinaryReader reader, uint offset) 23 | { 24 | reader.BaseStream.Position = offset; 25 | 26 | uint idOffset = reader.ReadUInt32(); 27 | uint objectTypeOffset = reader.ReadUInt32(); 28 | uint dataOffset = reader.ReadUInt32(); 29 | 30 | Id = reader.ReadEncodedString(idOffset); 31 | ObjectType = new SerializedType(); 32 | ObjectType.Read(reader, objectTypeOffset); 33 | Data = reader.ReadEncodedString(dataOffset); 34 | } 35 | 36 | internal void Write(ObjectInitializationDataJson obj) 37 | { 38 | obj.m_Id = Id; 39 | obj.m_ObjectType = new SerializedTypeJson(); 40 | ObjectType.Write(obj.m_ObjectType); 41 | obj.m_Data = Data; 42 | } 43 | 44 | internal uint Write(CatalogBinaryWriter writer) 45 | { 46 | Span bytes = stackalloc byte[12]; 47 | BinaryPrimitives.WriteUInt32LittleEndian(bytes, writer.WriteEncodedString(Id)); 48 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], ObjectType.Write(writer)); 49 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[8..], writer.WriteEncodedString(Data)); 50 | return writer.WriteWithCache(bytes); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /AddressablesTools/Binary/ContentCatalogDataBinaryHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AddressablesTools.Binary 4 | { 5 | internal class ContentCatalogDataBinaryHeader 6 | { 7 | public int Magic { get; set; } 8 | public int Version { get; set; } 9 | public uint KeysOffset { get; set; } 10 | public uint IdOffset { get; set; } 11 | public uint InstanceProviderOffset { get; set; } 12 | public uint SceneProviderOffset { get; set; } 13 | public uint InitObjectsArrayOffset { get; set; } 14 | public uint BuildResultHashOffset { get; set; } 15 | 16 | internal void Read(CatalogBinaryReader reader) 17 | { 18 | Magic = reader.ReadInt32(); 19 | Version = reader.ReadInt32(); 20 | if (Version is not (1 or 2)) 21 | { 22 | throw new NotSupportedException("Only versions 1 and 2 are supported"); 23 | } 24 | reader.Version = Version; 25 | 26 | KeysOffset = reader.ReadUInt32(); 27 | IdOffset = reader.ReadUInt32(); 28 | InstanceProviderOffset = reader.ReadUInt32(); 29 | SceneProviderOffset = reader.ReadUInt32(); 30 | InitObjectsArrayOffset = reader.ReadUInt32(); 31 | 32 | // version 1 has at least two sub versions: 33 | // 1.21.18 does not have this member, so we ignore it 34 | if (Version == 1 && KeysOffset == 0x20) 35 | BuildResultHashOffset = uint.MaxValue; 36 | else 37 | BuildResultHashOffset = reader.ReadUInt32(); 38 | } 39 | 40 | internal void Write(CatalogBinaryWriter writer) 41 | { 42 | writer.Write(Magic); 43 | writer.Write(Version); 44 | writer.Write(KeysOffset); 45 | writer.Write(IdOffset); 46 | writer.Write(InstanceProviderOffset); 47 | writer.Write(SceneProviderOffset); 48 | writer.Write(InitObjectsArrayOffset); 49 | if (BuildResultHashOffset != uint.MaxValue) 50 | writer.Write(BuildResultHashOffset); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/SerializedType.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using AddressablesTools.JSON; 3 | using System; 4 | using System.Buffers.Binary; 5 | using System.IO; 6 | 7 | namespace AddressablesTools.Catalog 8 | { 9 | public class SerializedType 10 | { 11 | public string AssemblyName { get; set; } 12 | public string ClassName { get; set; } 13 | 14 | public override bool Equals(object obj) 15 | { 16 | return obj is SerializedType type && 17 | AssemblyName == type.AssemblyName && 18 | ClassName == type.ClassName; 19 | } 20 | 21 | public override int GetHashCode() 22 | { 23 | return HashCode.Combine(AssemblyName, ClassName); 24 | } 25 | 26 | internal void Read(SerializedTypeJson type) 27 | { 28 | AssemblyName = type.m_AssemblyName; 29 | ClassName = type.m_ClassName; 30 | } 31 | 32 | internal void Read(CatalogBinaryReader reader, uint offset) 33 | { 34 | reader.BaseStream.Position = offset; 35 | 36 | uint assemblyNameOffset = reader.ReadUInt32(); 37 | uint classNameOffset = reader.ReadUInt32(); 38 | 39 | AssemblyName = reader.ReadEncodedString(assemblyNameOffset, '.'); 40 | ClassName = reader.ReadEncodedString(classNameOffset, '.'); 41 | } 42 | 43 | internal void Write(SerializedTypeJson type) 44 | { 45 | type.m_AssemblyName = AssemblyName; 46 | type.m_ClassName = ClassName; 47 | } 48 | 49 | internal uint Write(CatalogBinaryWriter writer) 50 | { 51 | Span bytes = stackalloc byte[8]; 52 | BinaryPrimitives.WriteUInt32LittleEndian(bytes, writer.WriteEncodedString(AssemblyName, '.')); 53 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], writer.WriteEncodedString(ClassName, '.')); 54 | return writer.WriteWithCache(bytes); 55 | } 56 | 57 | internal string GetMatchName() 58 | { 59 | return GetAssemblyShortName() + "; " + ClassName; 60 | } 61 | 62 | internal string GetAssemblyShortName() 63 | { 64 | if (!AssemblyName.Contains(',')) 65 | { 66 | throw new InvalidDataException("Assembly name must have commas"); 67 | } 68 | 69 | return AssemblyName.Split(',')[0]; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | A Unityless way to read and write addressables. 2 | 3 | Work in progress. 4 | 5 | Nightly links: 6 | 7 | - https://nightly.link/nesrak1/AddressablesTools/workflows/build-windows/master/addrtool-example-windows.zip 8 | - https://nightly.link/nesrak1/AddressablesTools/workflows/build-ubuntu/master/addrtool-example-ubuntu.zip 9 | 10 | Nuget link: https://www.nuget.org/packages/AssetsTools.NET.Addressables 11 | 12 | Supported versions: 13 | 14 | .---------------------------------------------. 15 | | Version | Read support | Write support | 16 | |--------------|--------------|---------------| 17 | | Json "v1" | Yes | Yes | 18 | | Json "v2" | Yes | Yes | 19 | | Json "v3" | Yes | Yes | 20 | | Binv1 | Yes | Yes* | 21 | | Binv1 "v1.1" | Yes | Yes* | 22 | | Binv2 | Yes | Yes | 23 | `---------------------------------------------` 24 | 25 | * - This support has not been thoroughly tested 26 | 27 | --- 28 | 29 | To use the "Example" command line app to... 30 | 31 | - patch catalog CRCs, run `Example patchcrc path/to/catalog.json` (replace .json with .bin if your game uses .bin) 32 | - search for assets, run `Example searchasset path/to/catalog.json` and then type the key to search for 33 | 34 | --- 35 | 36 | Using AddressablesTools is simple. Use either 37 | AddressablesJsonParser.FromBundle("path/to/file.bundle"); 38 | or 39 | AddressablesJsonParser.FromString(File.ReadAllText("path/to/file.json")); 40 | 41 | From there, you can access the `Resources` dictionary which contains a mapping from an object (usually a string or number) to a list of resource locations. 42 | 43 | In the `searchasset` example below, we can look up an asset string and find all of the bundles that are needed to load it. To do so, we find all keys that contain the substring we're searching for and that have resource locations with a `ProviderId` of `BundledAssetProvider`. After that, we can look up the `Dependency` id back into the `Resources` dictionary to find all of the necessary bundles. In this list, the first item is always the bundle that contains the asset, and all other bundles are dependencies needed by the first bundle. 44 | 45 | This can be useful if you want to know what bundles you need to load in order to load an asset in a tool without having to load _every_ bundle in the game. 46 | 47 | --- 48 | 49 | If you're still confused, maybe check out https://github.com/nesrak1/SeaOfStarsSpriteExtractor for a more "real-life" example. 50 | 51 | --- 52 | 53 | The "Example" program contains two tools: searchasset and patchcrc. 54 | 55 | The searchasset command takes an argument to the catalog.json or catalog.bundle file. It will then ask you for a string to search for and will display any results that it finds. 56 | 57 | The patchcrc command also takes an argument to catalog.json or catalog.bundle. It sets the m_Crc of all entries to 0, effectively disabling all CRC checks. 58 | 59 | --- 60 | 61 | This software is not sponsored by or affiliated with Unity Technologies or its affiliates. "Unity" is a registered trademark of Unity Technologies or its affiliates in the U.S. and elsewhere. -------------------------------------------------------------------------------- /AddressablesTools/Binary/CatalogBinaryReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace AddressablesTools.Binary 8 | { 9 | internal class CatalogBinaryReader : BinaryReader 10 | { 11 | public int Version { get; set; } = 1; 12 | 13 | private readonly Dictionary _objCache = []; 14 | 15 | public CatalogBinaryReader(Stream input) : base(input) { } 16 | 17 | public T CacheAndReturn(uint offset, T obj) 18 | { 19 | _objCache[offset] = obj; 20 | return obj; 21 | } 22 | 23 | public bool TryGetCachedObject(uint offset, out T typedObj) 24 | { 25 | if (_objCache.TryGetValue(offset, out object obj)) 26 | { 27 | typedObj = (T)obj; 28 | return true; 29 | } 30 | 31 | typedObj = default; 32 | return false; 33 | } 34 | 35 | private string ReadBasicString(long offset, bool unicode) 36 | { 37 | BaseStream.Position = offset - 4; 38 | int length = ReadInt32(); 39 | byte[] data = ReadBytes(length); 40 | if (unicode) 41 | { 42 | return Encoding.Unicode.GetString(data); 43 | } 44 | else 45 | { 46 | return Encoding.ASCII.GetString(data); 47 | } 48 | } 49 | 50 | private string ReadDynamicString(long offset, bool unicode, char sep) 51 | { 52 | BaseStream.Position = offset; 53 | 54 | List partStrs = new List(); 55 | while (true) 56 | { 57 | long partStringOffset = ReadUInt32(); 58 | long nextPartOffset = ReadUInt32(); 59 | 60 | partStrs.Add(ReadEncodedString((uint)partStringOffset)); // which seperator? 61 | 62 | if (nextPartOffset == uint.MaxValue) 63 | { 64 | break; 65 | } 66 | 67 | BaseStream.Position = nextPartOffset; 68 | } 69 | 70 | if (partStrs.Count == 1) 71 | return partStrs[0]; 72 | 73 | if (Version > 1) 74 | return string.Join(sep, partStrs.AsEnumerable().Reverse()); 75 | else 76 | return string.Join(sep, partStrs); 77 | } 78 | 79 | public string ReadEncodedString(uint encodedOffset, char dynstrSep = '\0') 80 | { 81 | if (encodedOffset == uint.MaxValue) 82 | { 83 | return null; 84 | } 85 | 86 | if (TryGetCachedObject(encodedOffset, out string cachedStr)) 87 | { 88 | return cachedStr; 89 | } 90 | 91 | bool unicode = (encodedOffset & 0x80000000) != 0; 92 | bool dynamicString = (encodedOffset & 0x40000000) != 0 && dynstrSep != '\0'; 93 | long offset = encodedOffset & 0x3fffffff; 94 | 95 | if (!dynamicString) 96 | { 97 | return CacheAndReturn((uint)offset, ReadBasicString(offset, unicode)); 98 | } 99 | else 100 | { 101 | return CacheAndReturn((uint)offset, ReadDynamicString(offset, unicode, dynstrSep)); 102 | } 103 | } 104 | 105 | public uint[] ReadOffsetArray(uint encodedOffset) 106 | { 107 | if (encodedOffset == uint.MaxValue) 108 | { 109 | return []; 110 | } 111 | 112 | if (TryGetCachedObject(encodedOffset, out uint[] cachedArr)) 113 | { 114 | return cachedArr; 115 | } 116 | 117 | BaseStream.Position = encodedOffset - 4; 118 | int byteSize = ReadInt32(); 119 | if (byteSize % 4 != 0) 120 | { 121 | throw new InvalidDataException("Array size must be a multiple of 4"); 122 | } 123 | 124 | int elemCount = byteSize / 4; 125 | uint[] result = new uint[elemCount]; 126 | for (int i = 0; i < elemCount; i++) 127 | { 128 | result[i] = ReadUInt32(); 129 | } 130 | 131 | return CacheAndReturn(encodedOffset, result); 132 | } 133 | 134 | public T ReadCustom(uint offset, Func fetchFunc) 135 | { 136 | if (!TryGetCachedObject(offset, out T v)) 137 | { 138 | v = fetchFunc(); 139 | _objCache[offset] = v; 140 | } 141 | 142 | return v; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/ResourceLocation.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using System; 3 | using System.Buffers.Binary; 4 | using System.Collections.Generic; 5 | 6 | namespace AddressablesTools.Catalog 7 | { 8 | public class ResourceLocation 9 | { 10 | public string InternalId { get; set; } 11 | public string ProviderId { get; set; } 12 | public object DependencyKey { get; set; } 13 | public List Dependencies { get; set; } 14 | public object Data { get; set; } 15 | public int HashCode { get; set; } 16 | public int DependencyHashCode { get; set; } 17 | public string PrimaryKey { get; set; } 18 | public SerializedType Type { get; set; } 19 | 20 | internal void Read( 21 | string internalId, string providerId, object dependencyKey, object data, 22 | int depHashCode, object primaryKey, SerializedType resourceType 23 | ) 24 | { 25 | InternalId = internalId; 26 | ProviderId = providerId; 27 | DependencyKey = dependencyKey; 28 | Dependencies = null; 29 | Data = data; 30 | HashCode = internalId.GetHashCode() * 31 + providerId.GetHashCode(); 31 | DependencyHashCode = depHashCode; 32 | PrimaryKey = primaryKey.ToString(); 33 | Type = resourceType; 34 | } 35 | 36 | internal void Read(CatalogBinaryReader reader, uint offset) 37 | { 38 | reader.BaseStream.Position = offset; 39 | uint primaryKeyOffset = reader.ReadUInt32(); 40 | uint internalIdOffset = reader.ReadUInt32(); 41 | uint providerIdOffset = reader.ReadUInt32(); 42 | uint dependenciesOffset = reader.ReadUInt32(); 43 | int dependencyHashCode = reader.ReadInt32(); 44 | uint dataOffset = reader.ReadUInt32(); 45 | uint typeOffset = reader.ReadUInt32(); 46 | 47 | PrimaryKey = reader.ReadEncodedString(primaryKeyOffset, '/'); 48 | InternalId = reader.ReadEncodedString(internalIdOffset, '/'); 49 | ProviderId = reader.ReadEncodedString(providerIdOffset, '.'); 50 | 51 | uint[] dependencyOffsets = reader.ReadOffsetArray(dependenciesOffset); 52 | List dependencies = new List(dependencyOffsets.Length); 53 | for (int i = 0; i < dependencyOffsets.Length; i++) 54 | { 55 | //reader.BaseStream.Position = dependencyOffsets[i]; 56 | uint objectOffset = dependencyOffsets[i]; 57 | var dependencyLocation = reader.ReadCustom(objectOffset, () => 58 | { 59 | var newDepLoc = new ResourceLocation(); 60 | newDepLoc.Read(reader, objectOffset); 61 | return newDepLoc; 62 | }); 63 | dependencies.Add(dependencyLocation); 64 | } 65 | 66 | DependencyKey = null; 67 | Dependencies = dependencies; 68 | 69 | // officially, dependenciesOffset is used here. lol. we can't do 70 | // that since writing the file would permenantly lose that value. 71 | DependencyHashCode = dependencyHashCode; 72 | Data = SerializedObjectDecoder.DecodeV2(reader, dataOffset); 73 | Type = new SerializedType(); 74 | Type.Read(reader, typeOffset); 75 | } 76 | 77 | internal uint Write(CatalogBinaryWriter writer, SerializedTypeAsmContainer staCont) 78 | { 79 | uint dependenciesOffset; 80 | if (Dependencies.Count > 0) 81 | { 82 | uint[] dependenciesList = new uint[Dependencies.Count]; 83 | for (int i = 0; i < Dependencies.Count; i++) 84 | { 85 | dependenciesList[i] = Dependencies[i].Write(writer, staCont); 86 | } 87 | 88 | dependenciesOffset = writer.WriteOffsetArray(dependenciesList); 89 | } 90 | else 91 | { 92 | dependenciesOffset = uint.MaxValue; 93 | } 94 | 95 | uint primaryKeyOffset = writer.WriteEncodedString(PrimaryKey, '/'); 96 | uint internalIdOffset = writer.WriteEncodedString(InternalId, '/'); 97 | uint providerIdOffset = writer.WriteEncodedString(ProviderId, '.'); 98 | 99 | int dependencyHashCode = DependencyHashCode; 100 | uint dataOffset = SerializedObjectDecoder.EncodeV2(writer, staCont, Data); 101 | uint typeOffset = Type.Write(writer); 102 | 103 | Span bytes = stackalloc byte[28]; 104 | BinaryPrimitives.WriteUInt32LittleEndian(bytes, primaryKeyOffset); 105 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], internalIdOffset); 106 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[8..], providerIdOffset); 107 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[12..], dependenciesOffset); 108 | BinaryPrimitives.WriteInt32LittleEndian(bytes[16..], dependencyHashCode); 109 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[20..], dataOffset); 110 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[24..], typeOffset); 111 | return writer.WriteWithCache(bytes); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /AddressablesTools/Binary/CatalogBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Hashing; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace AddressablesTools.Binary 10 | { 11 | internal class CatalogBinaryWriter : BinaryWriter 12 | { 13 | public int Version { get; set; } = 1; 14 | 15 | private readonly Dictionary _dataCache = []; 16 | private readonly Dictionary _quickStrCache = []; 17 | 18 | public CatalogBinaryWriter(Stream input) : base(input) { } 19 | 20 | public void Reserve(int space) 21 | { 22 | const int BLOCK_SIZE = 512; 23 | 24 | Span zeros = stackalloc byte[BLOCK_SIZE]; 25 | while (space > BLOCK_SIZE) 26 | { 27 | BaseStream.Write(zeros); 28 | space -= BLOCK_SIZE; 29 | } 30 | if (space > 0) 31 | { 32 | BaseStream.Write(zeros[..space]); 33 | } 34 | } 35 | 36 | public uint WriteWithCache(ReadOnlySpan data) 37 | { 38 | UInt128 key = XxHash128.HashToUInt128(data); 39 | if (_dataCache.TryGetValue(key, out uint value)) 40 | { 41 | return value; 42 | } 43 | 44 | uint pos = (uint)BaseStream.Position; 45 | Write(data); 46 | _dataCache[key] = pos; 47 | return pos; 48 | } 49 | 50 | private uint WriteBasicString(string data, bool unicode) 51 | { 52 | // an extra cache so we don't run Encoding.XXX.GetBytes unnecessarily 53 | if (_quickStrCache.TryGetValue(data, out uint cachedOff)) 54 | { 55 | return cachedOff; 56 | } 57 | 58 | byte[] lengthlessBytes; 59 | if (unicode) 60 | { 61 | lengthlessBytes = Encoding.Unicode.GetBytes(data); 62 | } 63 | else 64 | { 65 | lengthlessBytes = Encoding.ASCII.GetBytes(data); 66 | } 67 | 68 | byte[] bytes = new byte[lengthlessBytes.Length + 4]; 69 | Span bytesSpan = bytes.AsSpan(); 70 | BinaryPrimitives.WriteInt32LittleEndian(bytesSpan, lengthlessBytes.Length); 71 | lengthlessBytes.CopyTo(bytesSpan[4..]); 72 | 73 | uint pos = WriteWithCache(bytes) + 4; 74 | _quickStrCache[data] = pos; 75 | return pos; 76 | } 77 | 78 | private uint WriteDynamicString(string data, bool unicode, char sep) 79 | { 80 | const int MIN_SPLIT_SIZE = 8; 81 | 82 | string[] dataSplits = data.Split(sep); 83 | List joinedSplits = new List(dataSplits.Length); 84 | 85 | // would regular string concat be faster? 86 | int currentSplitLength = -1; // remove one for first seperator char 87 | int totalSplitLength = 0; 88 | List currentSplit = new List(); 89 | for (int i = dataSplits.Length - 1; i >= 0; i--) 90 | { 91 | string dataSplit = dataSplits[i]; 92 | currentSplit.Add(dataSplit); 93 | 94 | // add one for seperator 95 | if (unicode) 96 | currentSplitLength += Encoding.Unicode.GetByteCount(dataSplit) + 1; 97 | else 98 | currentSplitLength += Encoding.ASCII.GetByteCount(dataSplit) + 1; 99 | 100 | if (currentSplitLength >= MIN_SPLIT_SIZE || i == 0) 101 | { 102 | if (currentSplit.Count == 1) 103 | { 104 | joinedSplits.Add(currentSplit[0]); 105 | } 106 | else 107 | { 108 | joinedSplits.Add(string.Join(sep, currentSplit.AsEnumerable().Reverse())); 109 | } 110 | 111 | // only sum split contents, no seperators 112 | totalSplitLength += Math.Max(currentSplitLength, 0) - (currentSplit.Count - 1); 113 | 114 | currentSplitLength = -1; 115 | currentSplit.Clear(); 116 | } 117 | } 118 | 119 | // to keep with how unity does it, we'll check their way (even though this is slower) 120 | if (dataSplits.Length < 2 || (dataSplits.Length == 2 && totalSplitLength < MIN_SPLIT_SIZE)) 121 | { 122 | return WriteBasicString(data, unicode); 123 | } 124 | 125 | List splitOffsets = new List(joinedSplits.Count); 126 | foreach (string split in joinedSplits) 127 | { 128 | uint offset = WriteBasicString(split, unicode); 129 | splitOffsets.Add(offset); 130 | } 131 | 132 | uint lastLlOffset = uint.MaxValue; 133 | Span pieceBytes = stackalloc byte[8]; 134 | for (int i = splitOffsets.Count - 1; i >= 0; i--) 135 | { 136 | BinaryPrimitives.WriteUInt32LittleEndian(pieceBytes, splitOffsets[i]); 137 | BinaryPrimitives.WriteUInt32LittleEndian(pieceBytes[4..], lastLlOffset); 138 | uint thisLlOffset = WriteWithCache(pieceBytes); 139 | 140 | lastLlOffset = thisLlOffset; 141 | } 142 | 143 | return lastLlOffset; 144 | } 145 | 146 | private static bool IsStringAscii(string str) 147 | { 148 | int strLen = str.Length; 149 | for (int i = 0; i < strLen; i++) 150 | { 151 | if (str[i] > 255) 152 | return false; 153 | } 154 | 155 | return true; 156 | } 157 | 158 | public uint WriteEncodedString(string data, char dynstrSep = '\0') 159 | { 160 | if (data == null) 161 | { 162 | return uint.MaxValue; 163 | } 164 | 165 | bool unicode = data.Length > 0 && !IsStringAscii(data); 166 | bool dynamicString = dynstrSep != '\0' && data.Contains(dynstrSep); 167 | 168 | uint result; 169 | if (dynamicString) 170 | { 171 | result = WriteDynamicString(data, unicode, dynstrSep); 172 | result |= 0x40000000; 173 | } 174 | else 175 | { 176 | result = WriteBasicString(data, unicode); 177 | } 178 | 179 | if (unicode) 180 | { 181 | result |= 0x80000000; 182 | } 183 | 184 | return result; 185 | } 186 | 187 | public uint WriteOffsetArray(uint[] data, bool withCache = true) 188 | { 189 | byte[] bytes = new byte[data.Length * 4 + 4]; 190 | Span bytesSpan = bytes.AsSpan(); 191 | 192 | BinaryPrimitives.WriteInt32LittleEndian(bytesSpan, data.Length * 4); 193 | int dataIdx = 0; 194 | for (int i = 4; i < bytes.Length; i += 4) 195 | { 196 | BinaryPrimitives.WriteUInt32LittleEndian(bytesSpan[i..], data[dataIdx++]); 197 | } 198 | 199 | if (withCache) 200 | { 201 | uint pos = WriteWithCache(bytes); 202 | return pos + 4; 203 | } 204 | else 205 | { 206 | uint pos = (uint)BaseStream.Position; 207 | Write(bytes); 208 | return pos + 4; 209 | } 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /Example/Program.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools; 2 | using AddressablesTools.Catalog; 3 | using AddressablesTools.Classes; 4 | using AssetsTools.NET; 5 | 6 | static void SearchExample(string[] args) 7 | { 8 | if (args.Length < 2) 9 | { 10 | Console.WriteLine("need path to catalog.json"); 11 | return; 12 | } 13 | 14 | bool fromBundle = IsUnityFS(args[1]); 15 | 16 | ContentCatalogData ccd; 17 | if (fromBundle) 18 | { 19 | ccd = AddressablesCatalogFileParser.FromBundle(args[1]); 20 | } 21 | else 22 | { 23 | CatalogFileType fileType; 24 | using (FileStream fs = File.OpenRead(args[1])) 25 | { 26 | fileType = AddressablesCatalogFileParser.GetCatalogFileType(fs); 27 | } 28 | 29 | if (fileType == CatalogFileType.Json) 30 | { 31 | ccd = AddressablesCatalogFileParser.FromJsonString(File.ReadAllText(args[1])); 32 | } 33 | else if (fileType == CatalogFileType.Binary) 34 | { 35 | ccd = AddressablesCatalogFileParser.FromBinaryData(File.ReadAllBytes(args[1])); 36 | } 37 | else 38 | { 39 | Console.WriteLine("not a valid catalog file"); 40 | return; 41 | } 42 | } 43 | 44 | Console.Write("search key to find bundles of: "); 45 | string? search = Console.ReadLine(); 46 | 47 | if (search == null) 48 | { 49 | return; 50 | } 51 | 52 | search = search.ToLower(); 53 | foreach (object k in ccd.Resources.Keys) 54 | { 55 | if (k is string s && s.ToLower().Contains(search)) 56 | { 57 | Console.Write(s); 58 | var rsrcs = ccd.Resources[s]; 59 | foreach (var rsrc in rsrcs) 60 | { 61 | Console.WriteLine($" (id: {rsrc.InternalId}, prov: {rsrc.ProviderId})"); 62 | if (rsrc.ProviderId == "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider") 63 | { 64 | var data = rsrc.Data; 65 | if (data is WrappedSerializedObject { Object: AssetBundleRequestOptions abro }) 66 | { 67 | uint crc = abro.Crc; 68 | Console.WriteLine($" crc = {crc:x8}"); 69 | } 70 | } 71 | else if (rsrc.ProviderId == "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider") 72 | { 73 | List locs; 74 | if (rsrc.Dependencies != null) 75 | { 76 | // new version 77 | locs = rsrc.Dependencies; 78 | } 79 | else if (rsrc.DependencyKey != null) 80 | { 81 | // old version 82 | locs = ccd.Resources[rsrc.DependencyKey]; 83 | } 84 | else 85 | { 86 | continue; 87 | } 88 | 89 | Console.WriteLine($" {locs[0].InternalId}"); 90 | if (locs.Count > 1) 91 | { 92 | for (int i = 1; i < locs.Count; i++) 93 | { 94 | Console.WriteLine($" {locs[i].InternalId}"); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | static bool IsUnityFS(string path) 104 | { 105 | const string unityFs = "UnityFS"; 106 | using AssetsFileReader reader = new AssetsFileReader(path); 107 | if (reader.BaseStream.Length < unityFs.Length) 108 | { 109 | return false; 110 | } 111 | 112 | return reader.ReadStringLength(unityFs.Length) == unityFs; 113 | } 114 | 115 | static void PatchCrcRecursive(ResourceLocation thisRsrc, HashSet seenRsrcs) 116 | { 117 | // I think this can't happen right now, resources are duplicated every time 118 | if (seenRsrcs.Contains(thisRsrc)) 119 | return; 120 | 121 | var data = thisRsrc.Data; 122 | if (data is WrappedSerializedObject { Object: AssetBundleRequestOptions abro }) 123 | { 124 | abro.Crc = 0; 125 | } 126 | 127 | seenRsrcs.Add(thisRsrc); 128 | foreach (var childRsrc in thisRsrc.Dependencies) 129 | { 130 | PatchCrcRecursive(childRsrc, seenRsrcs); 131 | } 132 | } 133 | 134 | static void PatchCrcExample(string[] args) 135 | { 136 | if (args.Length < 2) 137 | { 138 | Console.WriteLine("need path to catalog.json"); 139 | return; 140 | } 141 | 142 | bool fromBundle = IsUnityFS(args[1]); 143 | 144 | ContentCatalogData ccd; 145 | CatalogFileType fileType = CatalogFileType.None; 146 | if (fromBundle) 147 | { 148 | ccd = AddressablesCatalogFileParser.FromBundle(args[1]); 149 | } 150 | else 151 | { 152 | using (FileStream fs = File.OpenRead(args[1])) 153 | { 154 | fileType = AddressablesCatalogFileParser.GetCatalogFileType(fs); 155 | } 156 | 157 | switch (fileType) 158 | { 159 | case CatalogFileType.Json: 160 | ccd = AddressablesCatalogFileParser.FromJsonString(File.ReadAllText(args[1])); 161 | break; 162 | case CatalogFileType.Binary: 163 | ccd = AddressablesCatalogFileParser.FromBinaryData(File.ReadAllBytes(args[1])); 164 | break; 165 | default: 166 | Console.WriteLine("not a valid catalog file"); 167 | return; 168 | } 169 | } 170 | 171 | Console.WriteLine("patching..."); 172 | 173 | var seenRsrcs = new HashSet(); 174 | foreach (var resourceList in ccd.Resources.Values) 175 | { 176 | foreach (var rsrc in resourceList) 177 | { 178 | if (rsrc.Dependencies != null) 179 | { 180 | // we just spotted a new version entry, switch to new entry parsing 181 | PatchCrcRecursive(rsrc, seenRsrcs); 182 | continue; 183 | } 184 | 185 | if (rsrc.ProviderId == "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider") 186 | { 187 | // old version 188 | var data = rsrc.Data; 189 | if (data is WrappedSerializedObject { Object: AssetBundleRequestOptions abro }) 190 | { 191 | abro.Crc = 0; 192 | } 193 | } 194 | } 195 | } 196 | 197 | if (fromBundle) 198 | { 199 | AddressablesCatalogFileParser.ToBundle(ccd, args[1], args[1] + ".patched"); 200 | } 201 | else 202 | { 203 | switch (fileType) 204 | { 205 | case CatalogFileType.Json: 206 | File.WriteAllText(args[1] + ".patched", AddressablesCatalogFileParser.ToJsonString(ccd)); 207 | break; 208 | case CatalogFileType.Binary: 209 | File.WriteAllBytes(args[1] + ".patched", AddressablesCatalogFileParser.ToBinaryData(ccd)); 210 | break; 211 | default: 212 | return; 213 | } 214 | } 215 | 216 | File.Move(args[1], args[1] + ".old", true); 217 | File.Move(args[1] + ".patched", args[1], true); 218 | } 219 | 220 | if (args.Length < 1) 221 | { 222 | Console.WriteLine("need args: "); 223 | Console.WriteLine("modes: searchasset, patchcrc"); 224 | } 225 | else if (args[0] == "searchasset") 226 | { 227 | SearchExample(args); 228 | } 229 | else if (args[0] == "patchcrc") 230 | { 231 | PatchCrcExample(args); 232 | } 233 | else 234 | { 235 | Console.WriteLine("mode not supported"); 236 | } -------------------------------------------------------------------------------- /AddressablesTools/AddressablesCatalogFileParser.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using AddressablesTools.Catalog; 3 | using AddressablesTools.JSON; 4 | using AssetsTools.NET; 5 | using AssetsTools.NET.Extra; 6 | using System; 7 | using System.Buffers.Binary; 8 | using System.IO; 9 | using System.Text; 10 | using System.Text.Encodings.Web; 11 | using System.Text.Json; 12 | 13 | namespace AddressablesTools 14 | { 15 | public static class AddressablesCatalogFileParser 16 | { 17 | internal static ContentCatalogDataJson CCDJsonFromString(string data) 18 | { 19 | return JsonSerializer.Deserialize(data); 20 | } 21 | 22 | public static ContentCatalogData FromBinaryData(byte[] data) 23 | { 24 | using MemoryStream ms = new MemoryStream(data); 25 | using CatalogBinaryReader reader = new CatalogBinaryReader(ms); 26 | 27 | ContentCatalogData catalogData = new ContentCatalogData(); 28 | catalogData.Read(reader); 29 | 30 | return catalogData; 31 | } 32 | 33 | public static ContentCatalogData FromJsonString(string data) 34 | { 35 | ContentCatalogDataJson ccdJson = CCDJsonFromString(data); 36 | 37 | ContentCatalogData catalogData = new ContentCatalogData(); 38 | catalogData.Read(ccdJson); 39 | 40 | return catalogData; 41 | } 42 | 43 | public static CatalogFileType GetCatalogFileType(Stream stream) 44 | { 45 | byte[] data = new byte[4]; 46 | int readBytes = stream.Read(data, 0, 4); 47 | if (readBytes != 4) 48 | { 49 | return CatalogFileType.None; 50 | } 51 | 52 | int possibleMagic; 53 | possibleMagic = BinaryPrimitives.ReadInt32LittleEndian(data); 54 | if (possibleMagic == 0x0de38942) 55 | { 56 | return CatalogFileType.Binary; 57 | } 58 | else if (possibleMagic == 0x4289e30d) 59 | { 60 | return CatalogFileType.Binary; 61 | } 62 | else 63 | { 64 | if (data[0] == '{') 65 | { 66 | return CatalogFileType.Json; 67 | } 68 | 69 | // double check there isn't whitespace before the { 70 | stream.Position = 0; 71 | while (true) 72 | { 73 | int v = stream.ReadByte(); 74 | if (v == -1) 75 | { 76 | return CatalogFileType.None; 77 | } 78 | else if (v == '\t' || v == ' ') 79 | { 80 | continue; 81 | } 82 | else if (v == '{') 83 | { 84 | return CatalogFileType.Json; 85 | } 86 | } 87 | } 88 | } 89 | 90 | internal static byte[] GetBundleTextAssetData(AssetsManager manager, BundleFileInstance bundleInst) 91 | { 92 | // there should only be one file in this bundle, so 0 is fine 93 | AssetsFileInstance assetsInst = manager.LoadAssetsFileFromBundle(bundleInst, 0); 94 | AssetsFile assetsFile = assetsInst.file; 95 | 96 | // there should also be only one text asset 97 | AssetFileInfo catalogAssetInfo = assetsFile.GetAssetsOfType(AssetClassID.TextAsset)[0]; 98 | 99 | // faster to manually read 100 | AssetsFileReader reader = assetsFile.Reader; 101 | reader.Position = catalogAssetInfo.GetAbsoluteByteOffset(assetsFile); 102 | 103 | reader.ReadCountStringInt32(); // ignore name 104 | reader.Align(); 105 | int dataSize = reader.ReadInt32(); 106 | byte[] data = reader.ReadBytes(dataSize); 107 | 108 | manager.UnloadAll(); 109 | 110 | return data; 111 | } 112 | 113 | internal static ContentCatalogData FromBundle(AssetsManager manager, BundleFileInstance bundleInst) 114 | { 115 | byte[] data = GetBundleTextAssetData(manager, bundleInst); 116 | if (data.Length < 4) 117 | { 118 | throw new InvalidDataException("Catalog data too small"); 119 | } 120 | 121 | int possibleMagic; 122 | possibleMagic = BinaryPrimitives.ReadInt32LittleEndian(data); 123 | if (possibleMagic == 0x0de38942) 124 | { 125 | return FromBinaryData(data); 126 | } 127 | else if (possibleMagic == 0x4289e30d) 128 | { 129 | // different hash code on big endian maybe? 130 | throw new NotSupportedException("Big endian catalogs are not supported"); 131 | } 132 | else 133 | { 134 | return FromJsonString(Encoding.UTF8.GetString(data)); 135 | } 136 | } 137 | 138 | public static ContentCatalogData FromBundle(Stream stream) 139 | { 140 | AssetsManager manager = new AssetsManager(); 141 | // name doesn't matter since we don't have dependencies 142 | BundleFileInstance bundleInst = manager.LoadBundleFile(stream, "catalog.bundle"); 143 | return FromBundle(manager, bundleInst); 144 | } 145 | 146 | public static ContentCatalogData FromBundle(string path) 147 | { 148 | AssetsManager manager = new AssetsManager(); 149 | BundleFileInstance bundleInst = manager.LoadBundleFile(path); 150 | return FromBundle(manager, bundleInst); 151 | } 152 | 153 | public static byte[] ToBinaryData(ContentCatalogData ccd) 154 | { 155 | using MemoryStream ms = new MemoryStream(); 156 | using CatalogBinaryWriter writer = new CatalogBinaryWriter(ms); 157 | 158 | ccd.Write(writer, SerializedTypeAsmContainer.ForNet40()); 159 | 160 | return ms.ToArray(); 161 | } 162 | 163 | public static string ToJsonString(ContentCatalogData ccd) 164 | { 165 | ContentCatalogDataJson ccdJson = new ContentCatalogDataJson(); 166 | 167 | ccd.Write(ccdJson); 168 | 169 | JsonSerializerOptions options = new JsonSerializerOptions() 170 | { 171 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 172 | }; 173 | return JsonSerializer.Serialize(ccdJson, options); 174 | } 175 | 176 | internal static void ToBundle(ContentCatalogData ccd, AssetsManager manager, BundleFileInstance bundleInst, Stream stream) 177 | { 178 | string json = ToJsonString(ccd); 179 | 180 | // there should only be one file in this bundle, so 0 is fine 181 | AssetsFileInstance assetsInst = manager.LoadAssetsFileFromBundle(bundleInst, 0); 182 | AssetsFile assetsFile = assetsInst.file; 183 | 184 | // there should also be only one text asset 185 | AssetFileInfo catalogAssetInfo = assetsFile.GetAssetsOfType(AssetClassID.TextAsset)[0]; 186 | 187 | MemoryStream newTextAssetMem = new MemoryStream(); 188 | AssetsFileWriter newTextAssetWriter = new AssetsFileWriter(newTextAssetMem); 189 | newTextAssetWriter.WriteCountStringInt32("catalog"); // doesn't really matter 190 | newTextAssetWriter.Align(); 191 | newTextAssetWriter.WriteCountStringInt32(json); 192 | newTextAssetWriter.Align(); 193 | 194 | catalogAssetInfo.SetNewData(newTextAssetMem.ToArray()); 195 | 196 | bundleInst.file.BlockAndDirInfo.DirectoryInfos[0].SetNewData(assetsFile); 197 | 198 | AssetsFileWriter bundleWriter = new AssetsFileWriter(stream); 199 | bundleInst.file.Write(bundleWriter); 200 | 201 | manager.UnloadAll(); 202 | } 203 | 204 | public static void ToBundle(ContentCatalogData ccd, Stream inStream, Stream outStream) 205 | { 206 | AssetsManager manager = new AssetsManager(); 207 | BundleFileInstance bundleInst = manager.LoadBundleFile(inStream, "catalog.bundle"); 208 | ToBundle(ccd, manager, bundleInst, outStream); 209 | } 210 | 211 | public static void ToBundle(ContentCatalogData ccd, string inPath, string outPath) 212 | { 213 | AssetsManager manager = new AssetsManager(); 214 | BundleFileInstance bundleInst = manager.LoadBundleFile(inPath); 215 | using FileStream fs = File.OpenWrite(outPath); 216 | ToBundle(ccd, manager, bundleInst, fs); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | launchSettings.json -------------------------------------------------------------------------------- /AddressablesTools/Classes/AssetBundleRequestOptions.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using System; 3 | using System.Buffers.Binary; 4 | using System.Text.Encodings.Web; 5 | using System.Text.Json; 6 | using System.Text.Json.Nodes; 7 | 8 | namespace AddressablesTools.Classes 9 | { 10 | public enum AssetLoadMode 11 | { 12 | RequestedAssetAndDependencies, 13 | AllPackedAssetsAndDependencies 14 | } 15 | 16 | public class AssetBundleRequestOptions 17 | { 18 | public string Hash { get; set; } 19 | public uint Crc { get; set; } 20 | public CommonInfo ComInfo { get; set; } 21 | public string BundleName { get; set; } 22 | public long BundleSize { get; set; } 23 | 24 | internal void Read(string jsonText) 25 | { 26 | JsonObject jsonObj = JsonSerializer.Deserialize(jsonText); 27 | if (jsonObj == null) 28 | { 29 | return; 30 | } 31 | 32 | Hash = (string)jsonObj["m_Hash"]; 33 | Crc = (uint)jsonObj["m_Crc"]; 34 | BundleName = (string)jsonObj["m_BundleName"]; 35 | BundleSize = (long)jsonObj["m_BundleSize"]; 36 | 37 | // this is only for writing back 38 | int commonInfoVersion; 39 | if (jsonObj["m_ChunkedTransfer"] == null) 40 | { 41 | commonInfoVersion = 1; 42 | } 43 | else if (jsonObj["m_AssetLoadMode"] == null && 44 | jsonObj["m_UseCrcForCachedBundles"] == null && 45 | jsonObj["m_UseUWRForLocalBundles"] == null && 46 | jsonObj["m_ClearOtherCachedVersionsWhenLoaded"] == null) 47 | { 48 | commonInfoVersion = 2; 49 | } 50 | else 51 | { 52 | commonInfoVersion = 3; 53 | } 54 | 55 | ComInfo = new CommonInfo() 56 | { 57 | Version = commonInfoVersion, 58 | Timeout = (short)(int)jsonObj["m_Timeout"], 59 | ChunkedTransfer = (bool)(jsonObj["m_ChunkedTransfer"] ?? false), 60 | RedirectLimit = (byte)(int)jsonObj["m_RedirectLimit"], 61 | RetryCount = (byte)(int)jsonObj["m_RetryCount"], 62 | AssetLoadMode = (AssetLoadMode)(int)(jsonObj["m_AssetLoadMode"] ?? (int)AssetLoadMode.RequestedAssetAndDependencies), 63 | UseCrcForCachedBundle = (bool)(jsonObj["m_UseCrcForCachedBundles"] ?? false), 64 | UseUnityWebRequestForLocalBundles = (bool)(jsonObj["m_UseUWRForLocalBundles"] ?? false), 65 | ClearOtherCachedVersionsWhenLoaded = (bool)(jsonObj["m_ClearOtherCachedVersionsWhenLoaded"] ?? false), 66 | }; 67 | } 68 | 69 | internal void Read(CatalogBinaryReader reader, uint offset) 70 | { 71 | reader.BaseStream.Position = offset; 72 | 73 | uint hashOffset = reader.ReadUInt32(); 74 | uint bundleNameOffset = reader.ReadUInt32(); 75 | uint crc = reader.ReadUInt32(); 76 | uint bundleSize = reader.ReadUInt32(); 77 | uint commonInfoOffset = reader.ReadUInt32(); 78 | 79 | reader.BaseStream.Position = hashOffset; 80 | uint hashV0 = reader.ReadUInt32(); 81 | uint hashV1 = reader.ReadUInt32(); 82 | uint hashV2 = reader.ReadUInt32(); 83 | uint hashV3 = reader.ReadUInt32(); 84 | Hash = new Hash128(hashV0, hashV1, hashV2, hashV3).Value; 85 | 86 | BundleName = reader.ReadEncodedString(bundleNameOffset, '_'); 87 | Crc = crc; 88 | BundleSize = bundleSize; 89 | 90 | // split in another class in case we need to do writing with duplicates later 91 | ComInfo = new CommonInfo() 92 | { 93 | Version = 3 94 | }; 95 | ComInfo.Read(reader, commonInfoOffset); 96 | } 97 | 98 | internal string WriteJson() 99 | { 100 | JsonSerializerOptions options = new JsonSerializerOptions() 101 | { 102 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 103 | }; 104 | 105 | JsonObject jsonObj = new JsonObject(); 106 | 107 | jsonObj["m_Hash"] = Hash; 108 | jsonObj["m_Crc"] = Crc; 109 | jsonObj["m_Timeout"] = ComInfo.Timeout; 110 | jsonObj["m_RedirectLimit"] = ComInfo.RedirectLimit; 111 | jsonObj["m_RetryCount"] = ComInfo.RetryCount; 112 | jsonObj["m_BundleName"] = BundleName; 113 | jsonObj["m_BundleSize"] = BundleSize; 114 | if (ComInfo.Version > 1) 115 | { 116 | jsonObj["m_ChunkedTransfer"] = ComInfo.ChunkedTransfer; 117 | } 118 | if (ComInfo.Version > 2) 119 | { 120 | jsonObj["m_AssetLoadMode"] = (int)ComInfo.AssetLoadMode; 121 | jsonObj["m_UseCrcForCachedBundles"] = ComInfo.UseCrcForCachedBundle; // not a typo 122 | jsonObj["m_UseUWRForLocalBundles"] = ComInfo.UseUnityWebRequestForLocalBundles; 123 | jsonObj["m_ClearOtherCachedVersionsWhenLoaded"] = ComInfo.ClearOtherCachedVersionsWhenLoaded; 124 | } 125 | 126 | return JsonSerializer.Serialize(jsonObj, options); 127 | } 128 | 129 | internal uint WriteBinary(CatalogBinaryWriter writer) 130 | { 131 | uint hashOffset = new Hash128(Hash).Write(writer); 132 | uint bundleNameOffset = writer.WriteEncodedString(BundleName, '_'); 133 | uint crc = Crc; 134 | uint bundleSize = (uint)BundleSize; 135 | uint commonInfoOffset = ComInfo.Write(writer); 136 | 137 | Span bytes = stackalloc byte[20]; 138 | BinaryPrimitives.WriteUInt32LittleEndian(bytes, hashOffset); 139 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], bundleNameOffset); 140 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[8..], crc); 141 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[12..], bundleSize); 142 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[16..], commonInfoOffset); 143 | return writer.WriteWithCache(bytes); 144 | } 145 | 146 | public class CommonInfo 147 | { 148 | public short Timeout { get; set; } 149 | public byte RedirectLimit { get; set; } 150 | public byte RetryCount { get; set; } 151 | public AssetLoadMode AssetLoadMode { get; set; } 152 | public bool ChunkedTransfer { get; set; } 153 | public bool UseCrcForCachedBundle { get; set; } 154 | public bool UseUnityWebRequestForLocalBundles { get; set; } 155 | public bool ClearOtherCachedVersionsWhenLoaded { get; set; } 156 | 157 | // this is not a real field, but this helps us know which fields to write back 158 | // version 1 (json) = don't write AssetLoadMode, UseCrcForCachedBundle, UseUnityWebRequestForLocalBundles, 159 | // ClearOtherCachedVersionsWhenLoaded, ChunkedTransfer 160 | // version 2 (json) = don't write AssetLoadMode, UseCrcForCachedBundle, UseUnityWebRequestForLocalBundles, 161 | // ClearOtherCachedVersionsWhenLoaded 162 | // version 3 (json + binary) = write all fields 163 | public int Version { get; init; } 164 | 165 | internal void Read(CatalogBinaryReader reader, uint offset) 166 | { 167 | reader.BaseStream.Position = offset; 168 | 169 | short timeout = reader.ReadInt16(); 170 | byte redirectLimit = reader.ReadByte(); 171 | byte retryCount = reader.ReadByte(); 172 | int flags = reader.ReadInt32(); 173 | 174 | Timeout = timeout; 175 | RedirectLimit = redirectLimit; 176 | RetryCount = retryCount; 177 | 178 | if ((flags & 1) != 0) 179 | { 180 | AssetLoadMode = AssetLoadMode.AllPackedAssetsAndDependencies; 181 | } 182 | else 183 | { 184 | AssetLoadMode = AssetLoadMode.RequestedAssetAndDependencies; 185 | } 186 | 187 | ChunkedTransfer = (flags & 2) != 0; 188 | UseCrcForCachedBundle = (flags & 4) != 0; 189 | UseUnityWebRequestForLocalBundles = (flags & 8) != 0; 190 | ClearOtherCachedVersionsWhenLoaded = (flags & 16) != 0; 191 | } 192 | 193 | internal uint Write(CatalogBinaryWriter writer) 194 | { 195 | int flags = 0; 196 | flags |= ((int)AssetLoadMode) & 1; 197 | flags |= (ChunkedTransfer ? 1 : 0) << 1; 198 | flags |= (UseCrcForCachedBundle ? 1 : 0) << 2; 199 | flags |= (UseUnityWebRequestForLocalBundles ? 1 : 0) << 3; 200 | flags |= (ClearOtherCachedVersionsWhenLoaded ? 1 : 0) << 4; 201 | 202 | Span data = stackalloc byte[8]; 203 | BinaryPrimitives.WriteInt16LittleEndian(data, Timeout); 204 | data[2] = RedirectLimit; 205 | data[3] = RetryCount; 206 | BinaryPrimitives.WriteInt32LittleEndian(data[4..], flags); 207 | return writer.WriteWithCache(data); 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/SerializedObjectDecoder.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using AddressablesTools.Classes; 3 | using System; 4 | using System.Buffers.Binary; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace AddressablesTools.Catalog 9 | { 10 | internal static class SerializedObjectDecoder 11 | { 12 | private const string INT_TYPENAME = "System.Int32"; 13 | private const string LONG_TYPENAME = "System.Int64"; 14 | private const string BOOL_TYPENAME = "System.Boolean"; 15 | private const string STRING_TYPENAME = "System.String"; 16 | private const string HASH128_TYPENAME = "UnityEngine.Hash128"; 17 | private const string ABRO_TYPENAME = "UnityEngine.ResourceManagement.ResourceProviders.AssetBundleRequestOptions"; 18 | 19 | private const string INT_MATCHNAME = "mscorlib; " + INT_TYPENAME; 20 | private const string LONG_MATCHNAME = "mscorlib; " + LONG_TYPENAME; 21 | private const string BOOL_MATCHNAME = "mscorlib; " + BOOL_TYPENAME; 22 | private const string STRING_MATCHNAME = "mscorlib; " + STRING_TYPENAME; 23 | private const string HASH128_MATCHNAME = "UnityEngine.CoreModule; " + HASH128_TYPENAME; 24 | private const string ABRO_MATCHNAME = "Unity.ResourceManager; " + ABRO_TYPENAME; 25 | 26 | internal enum ObjectType 27 | { 28 | AsciiString, 29 | UnicodeString, 30 | UInt16, 31 | UInt32, 32 | Int32, 33 | Hash128, 34 | Type, 35 | JsonObject 36 | } 37 | 38 | internal static object DecodeV1(BinaryReader br) 39 | { 40 | ObjectType type = (ObjectType)br.ReadByte(); 41 | 42 | switch (type) 43 | { 44 | case ObjectType.AsciiString: 45 | { 46 | string str = ReadString4(br); 47 | return str; 48 | } 49 | 50 | case ObjectType.UnicodeString: 51 | { 52 | string str = ReadString4Unicode(br); 53 | return str; 54 | } 55 | 56 | case ObjectType.UInt16: 57 | { 58 | return br.ReadUInt16(); 59 | } 60 | 61 | case ObjectType.UInt32: 62 | { 63 | return br.ReadUInt32(); 64 | } 65 | 66 | case ObjectType.Int32: 67 | { 68 | return br.ReadInt32(); 69 | } 70 | 71 | case ObjectType.Hash128: 72 | { 73 | string str = ReadString1(br); 74 | Hash128 hash = new Hash128(str); 75 | return hash; 76 | } 77 | 78 | case ObjectType.Type: 79 | { 80 | string str = ReadString1(br); 81 | TypeReference typeReference = new TypeReference(str); 82 | return typeReference; 83 | } 84 | 85 | case ObjectType.JsonObject: 86 | { 87 | string assemblyName = ReadString1(br); 88 | string className = ReadString1(br); 89 | string jsonText = ReadString4Unicode(br); 90 | 91 | ClassJsonObject jsonObj = new ClassJsonObject(assemblyName, className, jsonText); 92 | string matchName = jsonObj.Type.GetMatchName(); 93 | switch (matchName) 94 | { 95 | case ABRO_MATCHNAME: 96 | { 97 | AssetBundleRequestOptions obj = new AssetBundleRequestOptions(); 98 | obj.Read(jsonText); 99 | return new WrappedSerializedObject(jsonObj.Type, obj); 100 | } 101 | } 102 | 103 | // fallback to ClassJsonObject 104 | return jsonObj; 105 | } 106 | 107 | default: 108 | { 109 | return null; 110 | } 111 | } 112 | } 113 | 114 | internal static object DecodeV2(CatalogBinaryReader reader, uint offset) 115 | { 116 | if (offset == uint.MaxValue) 117 | { 118 | return null; 119 | } 120 | 121 | reader.BaseStream.Position = offset; 122 | uint typeNameOffset = reader.ReadUInt32(); 123 | uint objectOffset = reader.ReadUInt32(); 124 | 125 | bool isDefaultObject = objectOffset == uint.MaxValue; 126 | 127 | SerializedType serializedType = new SerializedType(); 128 | serializedType.Read(reader, typeNameOffset); 129 | string matchName = serializedType.GetMatchName(); 130 | switch (matchName) 131 | { 132 | case INT_MATCHNAME: 133 | { 134 | if (isDefaultObject) 135 | { 136 | return default(int); 137 | } 138 | 139 | reader.BaseStream.Position = objectOffset; 140 | return reader.ReadInt32(); 141 | } 142 | 143 | case LONG_MATCHNAME: 144 | { 145 | if (isDefaultObject) 146 | { 147 | return default(long); 148 | } 149 | 150 | reader.BaseStream.Position = objectOffset; 151 | return reader.ReadInt64(); 152 | } 153 | 154 | case BOOL_MATCHNAME: 155 | { 156 | if (isDefaultObject) 157 | { 158 | return default(bool); 159 | } 160 | 161 | reader.BaseStream.Position = objectOffset; 162 | return reader.ReadBoolean(); 163 | } 164 | 165 | case STRING_MATCHNAME: 166 | { 167 | if (isDefaultObject) 168 | { 169 | return default(string); 170 | } 171 | 172 | reader.BaseStream.Position = objectOffset; 173 | uint stringOffset = reader.ReadUInt32(); 174 | char separator = reader.ReadChar(); 175 | return reader.ReadEncodedString(stringOffset, separator); 176 | } 177 | 178 | case HASH128_MATCHNAME: 179 | { 180 | if (isDefaultObject) 181 | { 182 | return default(Hash128); 183 | } 184 | 185 | reader.BaseStream.Position = objectOffset; 186 | uint v0 = reader.ReadUInt32(); 187 | uint v1 = reader.ReadUInt32(); 188 | uint v2 = reader.ReadUInt32(); 189 | uint v3 = reader.ReadUInt32(); 190 | return new Hash128(v0, v1, v2, v3); 191 | } 192 | 193 | case ABRO_MATCHNAME: 194 | { 195 | if (isDefaultObject) 196 | { 197 | // loses type info, but we can't really do anything about it 198 | return null; 199 | } 200 | 201 | var obj = reader.ReadCustom(objectOffset, () => 202 | { 203 | var newobj = new AssetBundleRequestOptions(); 204 | newobj.Read(reader, objectOffset); 205 | return newobj; 206 | }); 207 | 208 | return new WrappedSerializedObject(serializedType, obj); 209 | } 210 | 211 | default: 212 | { 213 | throw new NotImplementedException("Unsupported type for deserialization " + matchName); 214 | } 215 | } 216 | } 217 | 218 | internal static void EncodeV1(BinaryWriter bw, object ob) 219 | { 220 | switch (ob) 221 | { 222 | case string str: 223 | { 224 | byte[] asciiEncoding = Encoding.ASCII.GetBytes(str); 225 | string asciiText = Encoding.ASCII.GetString(asciiEncoding); 226 | if (str != asciiText) 227 | { 228 | bw.Write((byte)ObjectType.UnicodeString); 229 | WriteString4Unicode(bw, str); 230 | } 231 | else 232 | { 233 | bw.Write((byte)ObjectType.AsciiString); 234 | WriteString4(bw, str); 235 | } 236 | break; 237 | } 238 | 239 | case ushort ush: 240 | { 241 | bw.Write((byte)ObjectType.UInt16); 242 | bw.Write(ush); 243 | break; 244 | } 245 | 246 | case uint uin: 247 | { 248 | bw.Write((byte)ObjectType.UInt32); 249 | bw.Write(uin); 250 | break; 251 | } 252 | 253 | case int i: 254 | { 255 | bw.Write((byte)ObjectType.Int32); 256 | bw.Write(i); 257 | break; 258 | } 259 | 260 | case Hash128 hash: 261 | { 262 | bw.Write((byte)ObjectType.Hash128); 263 | bw.Write(hash.Value); 264 | break; 265 | } 266 | 267 | case TypeReference type: 268 | { 269 | bw.Write((byte)ObjectType.Type); 270 | WriteString1(bw, type.Clsid); 271 | break; 272 | } 273 | 274 | case ClassJsonObject jsonObject: 275 | { 276 | // fallback class, shouldn't be used but here just in case 277 | // use WrappedSerializedObject if possible 278 | bw.Write((byte)ObjectType.JsonObject); 279 | WriteString1(bw, jsonObject.Type.AssemblyName); 280 | WriteString1(bw, jsonObject.Type.ClassName); 281 | WriteString4Unicode(bw, jsonObject.JsonText); 282 | break; 283 | } 284 | 285 | case WrappedSerializedObject wso: 286 | { 287 | string matchName = wso.Type.GetMatchName(); 288 | string jsonText; 289 | switch (matchName) 290 | { 291 | case ABRO_MATCHNAME: 292 | { 293 | AssetBundleRequestOptions abro = (AssetBundleRequestOptions)wso.Object; 294 | jsonText = abro.WriteJson(); 295 | break; 296 | } 297 | default: 298 | { 299 | throw new Exception($"Serialized type {wso.Type.AssemblyName}; {wso.Type.ClassName} not supported"); 300 | } 301 | } 302 | 303 | bw.Write((byte)ObjectType.JsonObject); 304 | WriteString1(bw, wso.Type.AssemblyName); 305 | WriteString1(bw, wso.Type.ClassName); 306 | WriteString4Unicode(bw, jsonText); 307 | break; 308 | } 309 | 310 | default: 311 | { 312 | throw new Exception($"Type {ob.GetType().FullName} not supported"); 313 | } 314 | } 315 | } 316 | 317 | private static char GetSeparatorWithMostOccurrences(string str, char[] options) 318 | { 319 | // no unicode separators pls :) 320 | Span mapping = stackalloc byte[256]; 321 | for (int i = 0; i < options.Length; i++) 322 | { 323 | mapping[options[i]] = (byte)(i + 1); 324 | } 325 | 326 | int[] occurrences = new int[options.Length]; 327 | int maxOccurrenceCount = int.MinValue; 328 | char maxOccurrenceChar = '\0'; 329 | foreach (char c in str) 330 | { 331 | if (c > 255) 332 | continue; 333 | 334 | int charIdx = mapping[c]; 335 | if (charIdx != 0) 336 | { 337 | int newOccurrenceCount = occurrences[charIdx - 1] + 1; 338 | if (newOccurrenceCount > maxOccurrenceCount) 339 | { 340 | maxOccurrenceCount = newOccurrenceCount; 341 | maxOccurrenceChar = c; 342 | } 343 | occurrences[charIdx - 1] = newOccurrenceCount; 344 | } 345 | } 346 | 347 | string[] splits = str.Split(maxOccurrenceChar); 348 | int largeSplits = 0; 349 | foreach (string split in splits) 350 | { 351 | if (split.Length >= 5) 352 | { 353 | largeSplits++; 354 | if (largeSplits >= 2) 355 | { 356 | return maxOccurrenceChar; 357 | } 358 | } 359 | } 360 | 361 | return '\0'; 362 | } 363 | 364 | internal static uint EncodeV2(CatalogBinaryWriter writer, SerializedTypeAsmContainer staCont, object ob) 365 | { 366 | if (ob == null) 367 | { 368 | return uint.MaxValue; 369 | } 370 | 371 | uint objectOffset = uint.MaxValue; 372 | SerializedType serializedType; 373 | switch (ob) 374 | { 375 | case int i: 376 | { 377 | if (i != default) 378 | { 379 | Span valBytes = stackalloc byte[4]; 380 | BinaryPrimitives.WriteInt32LittleEndian(valBytes, i); 381 | writer.WriteWithCache(valBytes); 382 | } 383 | 384 | serializedType = new SerializedType() 385 | { 386 | AssemblyName = staCont.StandardLibAsm, 387 | ClassName = INT_TYPENAME, 388 | }; 389 | break; 390 | } 391 | 392 | case long lon: 393 | { 394 | if (lon != default) 395 | { 396 | Span valBytes = stackalloc byte[8]; 397 | BinaryPrimitives.WriteInt64LittleEndian(valBytes, lon); 398 | writer.WriteWithCache(valBytes); 399 | } 400 | 401 | serializedType = new SerializedType() 402 | { 403 | AssemblyName = staCont.StandardLibAsm, 404 | ClassName = LONG_TYPENAME, 405 | }; 406 | break; 407 | } 408 | 409 | case bool boo: 410 | { 411 | if (boo != default) 412 | { 413 | Span valBytes = [boo ? (byte)1 : (byte)0]; 414 | writer.WriteWithCache(valBytes); 415 | } 416 | 417 | serializedType = new SerializedType() 418 | { 419 | AssemblyName = staCont.StandardLibAsm, 420 | ClassName = BOOL_TYPENAME, 421 | }; 422 | break; 423 | } 424 | 425 | case string str: 426 | { 427 | if (str != string.Empty) 428 | { 429 | char dynstrSep = GetSeparatorWithMostOccurrences(str, ['/', '\\', '.', '-', '_', ',']); 430 | uint stringOffset = writer.WriteEncodedString(str, dynstrSep); 431 | 432 | Span bytes = stackalloc byte[8]; 433 | BinaryPrimitives.WriteUInt32LittleEndian(bytes, stringOffset); 434 | BinaryPrimitives.WriteUInt32LittleEndian(bytes[4..], dynstrSep); 435 | objectOffset = writer.WriteWithCache(bytes); 436 | } 437 | 438 | serializedType = new SerializedType() 439 | { 440 | AssemblyName = staCont.StandardLibAsm, 441 | ClassName = STRING_TYPENAME, 442 | }; 443 | break; 444 | } 445 | 446 | case Hash128 hash: 447 | { 448 | if (hash != default) 449 | { 450 | hash.Write(writer); 451 | } 452 | 453 | serializedType = new SerializedType() 454 | { 455 | AssemblyName = staCont.Hash128Asm, 456 | ClassName = HASH128_TYPENAME, 457 | }; 458 | break; 459 | } 460 | 461 | case WrappedSerializedObject wso: 462 | { 463 | string matchName = wso.Type.GetMatchName(); 464 | switch (matchName) 465 | { 466 | case ABRO_MATCHNAME: 467 | { 468 | AssetBundleRequestOptions abro = (AssetBundleRequestOptions)wso.Object; 469 | objectOffset = abro.WriteBinary(writer); 470 | break; 471 | } 472 | default: 473 | { 474 | throw new Exception($"Serialized type {wso.Type.AssemblyName}; {wso.Type.ClassName} not supported"); 475 | } 476 | } 477 | 478 | serializedType = wso.Type; 479 | break; 480 | } 481 | 482 | default: 483 | { 484 | throw new Exception($"Type {ob.GetType().FullName} not supported"); 485 | } 486 | } 487 | 488 | Span finalBytes = stackalloc byte[8]; 489 | BinaryPrimitives.WriteUInt32LittleEndian(finalBytes, serializedType.Write(writer)); 490 | BinaryPrimitives.WriteUInt32LittleEndian(finalBytes[4..], objectOffset); 491 | 492 | return writer.WriteWithCache(finalBytes); 493 | } 494 | 495 | private static string ReadString1(BinaryReader br) 496 | { 497 | int length = br.ReadByte(); 498 | string str = Encoding.ASCII.GetString(br.ReadBytes(length)); 499 | return str; 500 | } 501 | 502 | private static string ReadString4(BinaryReader br) 503 | { 504 | int length = br.ReadInt32(); 505 | string str = Encoding.ASCII.GetString(br.ReadBytes(length)); 506 | return str; 507 | } 508 | 509 | private static string ReadString4Unicode(BinaryReader br) 510 | { 511 | int length = br.ReadInt32(); 512 | string str = Encoding.Unicode.GetString(br.ReadBytes(length)); 513 | return str; 514 | } 515 | 516 | private static void WriteString1(BinaryWriter bw, string str) 517 | { 518 | if (str.Length > 255) 519 | throw new ArgumentException("String length cannot be greater than 255"); 520 | 521 | byte[] bytes = Encoding.ASCII.GetBytes(str); 522 | bw.Write((byte)bytes.Length); 523 | bw.Write(bytes); 524 | } 525 | 526 | private static void WriteString4(BinaryWriter bw, string str) 527 | { 528 | byte[] bytes = Encoding.ASCII.GetBytes(str); 529 | bw.Write(bytes.Length); 530 | bw.Write(bytes); 531 | } 532 | 533 | private static void WriteString4Unicode(BinaryWriter bw, string str) 534 | { 535 | byte[] bytes = Encoding.Unicode.GetBytes(str); 536 | bw.Write(bytes.Length); 537 | bw.Write(bytes); 538 | } 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /AddressablesTools/Catalog/ContentCatalogData.cs: -------------------------------------------------------------------------------- 1 | using AddressablesTools.Binary; 2 | using AddressablesTools.JSON; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace AddressablesTools.Catalog 9 | { 10 | public class ContentCatalogData 11 | { 12 | // only used for binary format 13 | public int Version { get; set; } 14 | 15 | public string LocatorId { get; set; } 16 | public string BuildResultHash { get; set; } 17 | public ObjectInitializationData InstanceProviderData { get; set; } 18 | public ObjectInitializationData SceneProviderData { get; set; } 19 | public ObjectInitializationData[] ResourceProviderData { get; set; } 20 | public bool WriteCompact { get; set; } // use prefixes when writing? 21 | 22 | // used for resources for the json format, shouldn't be edited directly 23 | private string[] ProviderIds { get; set; } 24 | private string[] InternalIds { get; set; } 25 | private string[] Keys { get; set; } // for old versions 26 | private SerializedType[] ResourceTypes { get; set; } 27 | private string[] InternalIdPrefixes { get; set; } 28 | 29 | public Dictionary> Resources { get; set; } 30 | 31 | internal void Read(ContentCatalogDataJson data) 32 | { 33 | LocatorId = data.m_LocatorId; 34 | BuildResultHash = data.m_BuildResultHash; 35 | 36 | InstanceProviderData = new ObjectInitializationData(); 37 | InstanceProviderData.Read(data.m_InstanceProviderData); 38 | 39 | SceneProviderData = new ObjectInitializationData(); 40 | SceneProviderData.Read(data.m_SceneProviderData); 41 | 42 | ResourceProviderData = new ObjectInitializationData[data.m_ResourceProviderData.Length]; 43 | for (int i = 0; i < ResourceProviderData.Length; i++) 44 | { 45 | ResourceProviderData[i] = new ObjectInitializationData(); 46 | ResourceProviderData[i].Read(data.m_ResourceProviderData[i]); 47 | } 48 | 49 | ProviderIds = new string[data.m_ProviderIds.Length]; 50 | for (int i = 0; i < ProviderIds.Length; i++) 51 | { 52 | ProviderIds[i] = data.m_ProviderIds[i]; 53 | } 54 | 55 | InternalIds = new string[data.m_InternalIds.Length]; 56 | for (int i = 0; i < InternalIds.Length; i++) 57 | { 58 | InternalIds[i] = data.m_InternalIds[i]; 59 | } 60 | 61 | if (data.m_Keys != null) 62 | { 63 | Keys = new string[data.m_Keys.Length]; 64 | for (int i = 0; i < Keys.Length; i++) 65 | { 66 | Keys[i] = data.m_Keys[i]; 67 | } 68 | } 69 | else 70 | { 71 | Keys = null; 72 | } 73 | 74 | ResourceTypes = new SerializedType[data.m_resourceTypes.Length]; 75 | for (int i = 0; i < ResourceTypes.Length; i++) 76 | { 77 | ResourceTypes[i] = new SerializedType(); 78 | ResourceTypes[i].Read(data.m_resourceTypes[i]); 79 | } 80 | 81 | if (data.m_InternalIdPrefixes != null) 82 | { 83 | InternalIdPrefixes = new string[data.m_InternalIdPrefixes.Length]; 84 | for (int i = 0; i < InternalIdPrefixes.Length; i++) 85 | { 86 | InternalIdPrefixes[i] = data.m_InternalIdPrefixes[i]; 87 | } 88 | 89 | WriteCompact = InternalIdPrefixes.Length > 0; 90 | } 91 | else 92 | { 93 | InternalIdPrefixes = null; 94 | WriteCompact = false; 95 | } 96 | 97 | ReadResources(data); 98 | } 99 | 100 | internal void Read(CatalogBinaryReader reader) 101 | { 102 | ContentCatalogDataBinaryHeader header = new ContentCatalogDataBinaryHeader(); 103 | header.Read(reader); 104 | 105 | Version = reader.Version; 106 | 107 | LocatorId = reader.ReadEncodedString(header.IdOffset); 108 | BuildResultHash = reader.ReadEncodedString(header.BuildResultHashOffset); 109 | 110 | InstanceProviderData = new ObjectInitializationData(); 111 | InstanceProviderData.Read(reader, header.InstanceProviderOffset); 112 | 113 | SceneProviderData = new ObjectInitializationData(); 114 | SceneProviderData.Read(reader, header.SceneProviderOffset); 115 | 116 | uint[] resourceProviderDataOffsets = reader.ReadOffsetArray(header.InitObjectsArrayOffset); 117 | ResourceProviderData = new ObjectInitializationData[resourceProviderDataOffsets.Length]; 118 | for (int i = 0; i < ResourceProviderData.Length; i++) 119 | { 120 | ResourceProviderData[i] = new ObjectInitializationData(); 121 | ResourceProviderData[i].Read(reader, resourceProviderDataOffsets[i]); 122 | } 123 | 124 | ReadResources(reader, header); 125 | } 126 | 127 | private void ReadResources(ContentCatalogDataJson data) 128 | { 129 | List buckets; 130 | 131 | MemoryStream bucketStream = new MemoryStream(Convert.FromBase64String(data.m_BucketDataString)); 132 | using (BinaryReader bucketReader = new BinaryReader(bucketStream)) 133 | { 134 | int bucketCount = bucketReader.ReadInt32(); 135 | buckets = new List(bucketCount); 136 | 137 | for (int i = 0; i < bucketCount; i++) 138 | { 139 | int offset = bucketReader.ReadInt32(); 140 | 141 | int entryCount = bucketReader.ReadInt32(); 142 | int[] entries = new int[entryCount]; 143 | for (int j = 0; j < entryCount; j++) 144 | { 145 | entries[j] = bucketReader.ReadInt32(); 146 | } 147 | 148 | buckets.Add(new Bucket(offset, entries)); 149 | } 150 | } 151 | 152 | List keys; 153 | 154 | MemoryStream keyDataStream = new MemoryStream(Convert.FromBase64String(data.m_KeyDataString)); 155 | using (BinaryReader keyReader = new BinaryReader(keyDataStream)) 156 | { 157 | int keyCount = keyReader.ReadInt32(); 158 | keys = new List(keyCount); 159 | 160 | for (int i = 0; i < keyCount; i++) 161 | { 162 | keyDataStream.Position = buckets[i].offset; 163 | keys.Add(SerializedObjectDecoder.DecodeV1(keyReader)); 164 | } 165 | } 166 | 167 | List locations; 168 | 169 | MemoryStream entryDataStream = new MemoryStream(Convert.FromBase64String(data.m_EntryDataString)); 170 | MemoryStream extraDataStream = new MemoryStream(Convert.FromBase64String(data.m_ExtraDataString)); 171 | using (BinaryReader entryReader = new BinaryReader(entryDataStream)) 172 | using (BinaryReader extraReader = new BinaryReader(extraDataStream)) 173 | { 174 | int entryCount = entryReader.ReadInt32(); 175 | locations = new List(entryCount); 176 | 177 | for (int i = 0; i < entryCount; i++) 178 | { 179 | int internalIdIndex = entryReader.ReadInt32(); 180 | int providerIndex = entryReader.ReadInt32(); 181 | int dependencyKeyIndex = entryReader.ReadInt32(); 182 | int depHash = entryReader.ReadInt32(); 183 | int dataIndex = entryReader.ReadInt32(); 184 | int primaryKeyIndex = entryReader.ReadInt32(); 185 | int resourceTypeIndex = entryReader.ReadInt32(); 186 | 187 | string internalId = InternalIds[internalIdIndex]; 188 | if (InternalIdPrefixes != null && InternalIdPrefixes.Length > 0) 189 | { 190 | // the real code uses LastIndexOf, but this completely breaks if 191 | // a string has a # in it already (e.g., #12/some/path/thathas#init.prefab) 192 | // this does not technically meet the reference behavior, 193 | // but as this is the _expected_ behavior, we'll use that instead. 194 | int splitIndex = internalId.IndexOf('#'); 195 | if (splitIndex != -1 && int.TryParse(internalId[..splitIndex], out int prefixIndex)) 196 | { 197 | internalId = string.Concat(InternalIdPrefixes[prefixIndex], internalId.AsSpan(splitIndex + 1)); 198 | } 199 | } 200 | 201 | string providerId = ProviderIds[providerIndex]; 202 | 203 | object dependencyKey = null; 204 | if (dependencyKeyIndex >= 0) 205 | { 206 | dependencyKey = keys[dependencyKeyIndex]; 207 | } 208 | 209 | object objData = null; 210 | if (dataIndex >= 0) 211 | { 212 | extraDataStream.Position = dataIndex; 213 | objData = SerializedObjectDecoder.DecodeV1(extraReader); 214 | } 215 | 216 | object primaryKey; 217 | if (Keys == null) 218 | { 219 | primaryKey = keys[primaryKeyIndex]; 220 | } 221 | else 222 | { 223 | // unity moment 224 | primaryKey = Keys[primaryKeyIndex]; 225 | } 226 | 227 | SerializedType resourceType = ResourceTypes[resourceTypeIndex]; 228 | 229 | var loc = new ResourceLocation(); 230 | loc.Read(internalId, providerId, dependencyKey, objData, depHash, primaryKey, resourceType); 231 | locations.Add(loc); 232 | } 233 | } 234 | 235 | Resources = new Dictionary>(buckets.Count); 236 | for (int i = 0; i < buckets.Count; i++) 237 | { 238 | int[] bucketEntries = buckets[i].entries; 239 | List locs = new List(bucketEntries.Length); 240 | for (int j = 0; j < bucketEntries.Length; j++) 241 | { 242 | locs.Add(locations[bucketEntries[j]]); 243 | } 244 | Resources[keys[i]] = locs; 245 | } 246 | } 247 | 248 | private void ReadResources(CatalogBinaryReader reader, ContentCatalogDataBinaryHeader header) 249 | { 250 | uint[] keyLocationOffsets = reader.ReadOffsetArray(header.KeysOffset); 251 | Resources = new Dictionary>(keyLocationOffsets.Length / 2); 252 | for (int i = 0; i < keyLocationOffsets.Length; i += 2) 253 | { 254 | uint keyOffset = keyLocationOffsets[i]; 255 | uint locationListOffset = keyLocationOffsets[i + 1]; 256 | object key = SerializedObjectDecoder.DecodeV2(reader, keyOffset); 257 | 258 | uint[] locationOffsets = reader.ReadOffsetArray(locationListOffset); 259 | List locations = new List(locationOffsets.Length); 260 | for (int j = 0; j < locationOffsets.Length; j++) 261 | { 262 | ResourceLocation location = new ResourceLocation(); 263 | location.Read(reader, locationOffsets[j]); 264 | locations.Add(location); 265 | } 266 | 267 | Resources[key] = locations; 268 | } 269 | } 270 | 271 | internal void Write(ContentCatalogDataJson data) 272 | { 273 | data.m_LocatorId = LocatorId; 274 | data.m_BuildResultHash = BuildResultHash; 275 | 276 | data.m_InstanceProviderData = new ObjectInitializationDataJson(); 277 | InstanceProviderData.Write(data.m_InstanceProviderData); 278 | 279 | data.m_SceneProviderData = new ObjectInitializationDataJson(); 280 | SceneProviderData.Write(data.m_SceneProviderData); 281 | 282 | data.m_ResourceProviderData = new ObjectInitializationDataJson[ResourceProviderData.Length]; 283 | for (int i = 0; i < data.m_ResourceProviderData.Length; i++) 284 | { 285 | data.m_ResourceProviderData[i] = new ObjectInitializationDataJson(); 286 | ResourceProviderData[i].Write(data.m_ResourceProviderData[i]); 287 | } 288 | 289 | WriteResources(data); 290 | 291 | data.m_ProviderIds = new string[ProviderIds.Length]; 292 | for (int i = 0; i < data.m_ProviderIds.Length; i++) 293 | { 294 | data.m_ProviderIds[i] = ProviderIds[i]; 295 | } 296 | 297 | data.m_InternalIds = new string[InternalIds.Length]; 298 | if (InternalIdPrefixes != null && InternalIdPrefixes.Length > 0 && WriteCompact) 299 | { 300 | Dictionary newPrefixesToIndex = MakeDictionaryList(InternalIdPrefixes.ToList()); 301 | for (int i = 0; i < data.m_InternalIds.Length; i++) 302 | { 303 | string internalId = InternalIds[i]; 304 | int splitIndex = internalId.LastIndexOf('/'); 305 | // skip if # in string since this seems broken in addressables' implementation 306 | if (splitIndex != -1 && !internalId.Contains('#')) 307 | { 308 | int prefixIndex = newPrefixesToIndex[internalId[..splitIndex]]; 309 | data.m_InternalIds[i] = $"{prefixIndex}#{internalId[splitIndex..]}"; 310 | } 311 | else 312 | { 313 | data.m_InternalIds[i] = InternalIds[i]; 314 | } 315 | } 316 | } 317 | else 318 | { 319 | for (int i = 0; i < data.m_InternalIds.Length; i++) 320 | { 321 | data.m_InternalIds[i] = InternalIds[i]; 322 | } 323 | } 324 | 325 | if (Keys != null) 326 | { 327 | data.m_Keys = new string[Keys.Length]; 328 | for (int i = 0; i < data.m_Keys.Length; i++) 329 | { 330 | data.m_Keys[i] = Keys[i]; 331 | } 332 | } 333 | else 334 | { 335 | data.m_Keys = null; 336 | } 337 | 338 | data.m_resourceTypes = new SerializedTypeJson[ResourceTypes.Length]; 339 | for (int i = 0; i < data.m_resourceTypes.Length; i++) 340 | { 341 | data.m_resourceTypes[i] = new SerializedTypeJson(); 342 | ResourceTypes[i].Write(data.m_resourceTypes[i]); 343 | } 344 | 345 | if (InternalIdPrefixes != null) 346 | { 347 | data.m_InternalIdPrefixes = new string[InternalIdPrefixes.Length]; 348 | for (int i = 0; i < data.m_InternalIdPrefixes.Length; i++) 349 | { 350 | data.m_InternalIdPrefixes[i] = InternalIdPrefixes[i]; 351 | } 352 | } 353 | else 354 | { 355 | data.m_InternalIdPrefixes = null; 356 | } 357 | } 358 | 359 | internal void Write(CatalogBinaryWriter writer, SerializedTypeAsmContainer staCont) 360 | { 361 | writer.Version = Version; 362 | 363 | ContentCatalogDataBinaryHeader header = new ContentCatalogDataBinaryHeader(); 364 | header.Write(writer); // empty header 365 | 366 | header.Magic = 0x0de38942; 367 | header.Version = 2; 368 | header.KeysOffset = (uint)writer.BaseStream.Position + 4; 369 | writer.Reserve(4 + Resources.Count * 4 * 2); // empty key list + length 370 | 371 | header.IdOffset = writer.WriteEncodedString(LocatorId); 372 | header.InstanceProviderOffset = InstanceProviderData.Write(writer); 373 | header.SceneProviderOffset = SceneProviderData.Write(writer); 374 | 375 | uint[] initObjectsOffsets = new uint[ResourceProviderData.Length]; 376 | for (int i = 0; i < ResourceProviderData.Length; i++) 377 | { 378 | initObjectsOffsets[i] = ResourceProviderData[i].Write(writer); 379 | } 380 | 381 | header.InitObjectsArrayOffset = writer.WriteOffsetArray(initObjectsOffsets); 382 | header.BuildResultHashOffset = writer.WriteEncodedString(BuildResultHash); 383 | 384 | WriteResources(writer, header, staCont); 385 | 386 | writer.BaseStream.Position = 0; 387 | header.Write(writer); 388 | } 389 | 390 | private void WriteResources(CatalogBinaryWriter writer, ContentCatalogDataBinaryHeader header, SerializedTypeAsmContainer staCont) 391 | { 392 | List tmpLocationOffsetArray = new List(); 393 | uint[] keyLocationOffsets = new uint[Resources.Count * 2]; 394 | foreach (var kvp in Resources) 395 | { 396 | // resource locations are written first 397 | uint[] locationOffsets = new uint[kvp.Value.Count]; 398 | for (int j = 0; j < kvp.Value.Count; j++) 399 | { 400 | ResourceLocation location = kvp.Value[j]; 401 | locationOffsets[j] = location.Write(writer, staCont); 402 | } 403 | 404 | tmpLocationOffsetArray.Add(locationOffsets); 405 | } 406 | 407 | int i = 0; 408 | int i2 = 0; 409 | foreach (var kvp in Resources) 410 | { 411 | uint keyOffset = SerializedObjectDecoder.EncodeV2(writer, staCont, kvp.Key); 412 | keyLocationOffsets[i++] = keyOffset; 413 | 414 | uint locationListOffset = writer.WriteOffsetArray(tmpLocationOffsetArray[i2++]); 415 | keyLocationOffsets[i++] = locationListOffset; 416 | } 417 | 418 | // don't write with cache since we already reserved space for it 419 | writer.BaseStream.Position = header.KeysOffset - 4; 420 | header.KeysOffset = writer.WriteOffsetArray(keyLocationOffsets, false); 421 | } 422 | 423 | private void WriteResources(ContentCatalogDataJson data) 424 | { 425 | HashSet newInternalIdHs = new HashSet(); 426 | HashSet newProviderIdHs = new HashSet(); 427 | HashSet newResourceTypeHs = new HashSet(); 428 | HashSet newInternalIdPrefixes = new HashSet(); 429 | 430 | HashSet newLocationHs = new HashSet(); 431 | 432 | List newKeys = Resources.Keys.ToList(); 433 | 434 | foreach (var value in Resources.Values) 435 | { 436 | foreach (var location in value) 437 | { 438 | newLocationHs.Add(location); 439 | 440 | if (location.InternalId == null) 441 | throw new Exception("Location's internal ID cannot be null"); 442 | 443 | if (location.ProviderId == null) 444 | throw new Exception("Location's provider ID cannot be null"); 445 | 446 | if (InternalIdPrefixes != null && WriteCompact) 447 | { 448 | int splitIndex = location.InternalId.LastIndexOf('/'); 449 | if (splitIndex != -1) 450 | { 451 | newInternalIdPrefixes.Add(location.InternalId[..splitIndex]); 452 | } 453 | } 454 | 455 | newInternalIdHs.Add(location.InternalId); 456 | newProviderIdHs.Add(location.ProviderId); 457 | 458 | if (location.Type != null) 459 | { 460 | newResourceTypeHs.Add(location.Type); 461 | } 462 | } 463 | } 464 | 465 | List newInternalIds = newInternalIdHs.ToList(); 466 | List newProviderIds = newProviderIdHs.ToList(); 467 | List newResourceTypes = newResourceTypeHs.ToList(); 468 | List newLocations = newLocationHs.ToList(); 469 | 470 | Dictionary newKeyToIndex = MakeDictionaryList(newKeys); 471 | Dictionary newInternalIdsToIndex = MakeDictionaryList(newInternalIds); 472 | Dictionary newProviderIdsToIndex = MakeDictionaryList(newProviderIds); 473 | Dictionary newResourceTypesToIndex = MakeDictionaryList(newResourceTypes); 474 | Dictionary newLocationsToIndex = MakeDictionaryList(newLocations); 475 | 476 | MemoryStream entryDataStream = new MemoryStream(); 477 | MemoryStream extraDataStream = new MemoryStream(); 478 | using (BinaryWriter entryWriter = new BinaryWriter(entryDataStream)) 479 | using (BinaryWriter extraWriter = new BinaryWriter(extraDataStream)) 480 | { 481 | entryWriter.Write(newLocationHs.Count); 482 | 483 | foreach (var location in newLocationHs) 484 | { 485 | int internalIdIndex = newInternalIdsToIndex[location.InternalId]; 486 | int providerIndex = newProviderIdsToIndex[location.ProviderId]; 487 | int dependencyKeyIndex = (location.DependencyKey == null) ? -1 : newKeyToIndex[location.DependencyKey]; 488 | int depHash = location.DependencyHashCode; // todo calculate this 489 | int dataIndex = -1; 490 | if (location.Data != null) 491 | { 492 | dataIndex = (int)extraDataStream.Position; 493 | SerializedObjectDecoder.EncodeV1(extraWriter, location.Data); 494 | } 495 | int primaryKeyIndex = newKeyToIndex[location.PrimaryKey]; 496 | int resourceTypeIndex = newResourceTypesToIndex[location.Type]; 497 | 498 | entryWriter.Write(internalIdIndex); 499 | entryWriter.Write(providerIndex); 500 | entryWriter.Write(dependencyKeyIndex); 501 | entryWriter.Write(depHash); 502 | entryWriter.Write(dataIndex); 503 | entryWriter.Write(primaryKeyIndex); 504 | entryWriter.Write(resourceTypeIndex); 505 | } 506 | } 507 | 508 | MemoryStream keyDataStream = new MemoryStream(); 509 | MemoryStream bucketStream = new MemoryStream(); 510 | using (BinaryWriter keyWriter = new BinaryWriter(keyDataStream)) 511 | using (BinaryWriter bucketWriter = new BinaryWriter(bucketStream)) 512 | { 513 | keyWriter.Write(newKeys.Count); // same as Resources.Count 514 | bucketWriter.Write(newKeys.Count); 515 | 516 | foreach (var resourceKvp in Resources) 517 | { 518 | object resourceKey = resourceKvp.Key; 519 | List resourceValue = resourceKvp.Value; 520 | 521 | Bucket bucket = new Bucket 522 | { 523 | offset = (int)keyDataStream.Position, 524 | entries = new int[resourceValue.Count] 525 | }; 526 | 527 | // write key 528 | SerializedObjectDecoder.EncodeV1(keyWriter, resourceKey); 529 | 530 | for (int i = 0; i < resourceValue.Count; i++) 531 | { 532 | bucket.entries[i] = newLocationsToIndex[resourceValue[i]]; 533 | } 534 | 535 | // write bucket 536 | bucketWriter.Write(bucket.offset); 537 | bucketWriter.Write(bucket.entries.Length); 538 | for (int i = 0; i < bucket.entries.Length; i++) 539 | { 540 | bucketWriter.Write(bucket.entries[i]); 541 | } 542 | } 543 | } 544 | 545 | ProviderIds = newProviderIds.ToArray(); 546 | InternalIds = newInternalIds.ToArray(); 547 | if (InternalIdPrefixes != null) 548 | { 549 | if (WriteCompact) 550 | InternalIdPrefixes = newInternalIdPrefixes.ToArray(); 551 | else 552 | InternalIdPrefixes = Array.Empty(); 553 | } 554 | ResourceTypes = newResourceTypes.ToArray(); 555 | 556 | data.m_BucketDataString = Convert.ToBase64String(bucketStream.ToArray()); 557 | data.m_KeyDataString = Convert.ToBase64String(keyDataStream.ToArray()); 558 | data.m_EntryDataString = Convert.ToBase64String(entryDataStream.ToArray()); 559 | data.m_ExtraDataString = Convert.ToBase64String(extraDataStream.ToArray()); 560 | } 561 | 562 | private static Dictionary MakeDictionaryList(List list) 563 | { 564 | return list 565 | .Select((item, index) => new { Item = item, Index = index }) 566 | .ToDictionary(x => x.Item, x => x.Index); 567 | } 568 | 569 | private struct Bucket 570 | { 571 | public int offset; 572 | public int[] entries; 573 | 574 | public Bucket(int offset, int[] entries) 575 | { 576 | this.offset = offset; 577 | this.entries = entries; 578 | } 579 | } 580 | } 581 | } 582 | --------------------------------------------------------------------------------