├── 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