first commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using AddressablesTools.Catalog;
|
||||
using AddressablesTools.JSON;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AddressablesTools
|
||||
{
|
||||
public static class AddressablesJsonParser
|
||||
{
|
||||
internal static ContentCatalogDataJson CCDJsonFromString(string data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<ContentCatalogDataJson>(data);
|
||||
}
|
||||
|
||||
public static ContentCatalogData FromString(string data)
|
||||
{
|
||||
ContentCatalogDataJson ccdJson = CCDJsonFromString(data);
|
||||
|
||||
ContentCatalogData catalogData = new ContentCatalogData();
|
||||
catalogData.Read(ccdJson);
|
||||
|
||||
return catalogData;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
internal class ClassJsonObject
|
||||
{
|
||||
public string AssemblyName { get; }
|
||||
public string ClassName { get; }
|
||||
public string JsonText { get; }
|
||||
|
||||
public ClassJsonObject(string assemblyName, string className, string jsonText)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
ClassName = className;
|
||||
JsonText = jsonText;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,177 @@
|
||||
using AddressablesTools.JSON;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
public class ContentCatalogData
|
||||
{
|
||||
public string LocatorId { get; set; }
|
||||
public ObjectInitializationData InstanceProviderData { get; set; }
|
||||
public ObjectInitializationData SceneProviderData { get; set; }
|
||||
public ObjectInitializationData[] ResourceProviderData { get; set; }
|
||||
public string[] ProviderIds { get; set; }
|
||||
public string[] InternalIds { get; set; }
|
||||
public SerializedType[] ResourceTypes { get; set; }
|
||||
public string[] InternalIdPrefixes { get; set; }
|
||||
|
||||
public Dictionary<object, List<ResourceLocation>> Resources { get; set; }
|
||||
|
||||
internal void Read(ContentCatalogDataJson data)
|
||||
{
|
||||
LocatorId = data.m_LocatorId;
|
||||
|
||||
InstanceProviderData = new ObjectInitializationData();
|
||||
InstanceProviderData.Read(data.m_InstanceProviderData);
|
||||
|
||||
SceneProviderData = new ObjectInitializationData();
|
||||
SceneProviderData.Read(data.m_SceneProviderData);
|
||||
|
||||
ResourceProviderData = new ObjectInitializationData[data.m_ResourceProviderData.Length];
|
||||
for (int i = 0; i < ResourceProviderData.Length; i++)
|
||||
{
|
||||
ResourceProviderData[i] = new ObjectInitializationData();
|
||||
ResourceProviderData[i].Read(data.m_ResourceProviderData[i]);
|
||||
}
|
||||
|
||||
ProviderIds = new string[data.m_ProviderIds.Length];
|
||||
for (int i = 0; i < ProviderIds.Length; i++)
|
||||
{
|
||||
ProviderIds[i] = data.m_ProviderIds[i];
|
||||
}
|
||||
|
||||
InternalIds = new string[data.m_InternalIds.Length];
|
||||
for (int i = 0; i < InternalIds.Length; i++)
|
||||
{
|
||||
InternalIds[i] = data.m_InternalIds[i];
|
||||
}
|
||||
|
||||
ResourceTypes = new SerializedType[data.m_resourceTypes.Length];
|
||||
for (int i = 0; i < ResourceTypes.Length; i++)
|
||||
{
|
||||
ResourceTypes[i] = new SerializedType();
|
||||
ResourceTypes[i].Read(data.m_resourceTypes[i]);
|
||||
}
|
||||
|
||||
InternalIdPrefixes = new string[data.m_InternalIdPrefixes.Length];
|
||||
for (int i = 0; i < InternalIdPrefixes.Length; i++)
|
||||
{
|
||||
InternalIdPrefixes[i] = data.m_InternalIdPrefixes[i];
|
||||
}
|
||||
|
||||
ReadResources(data);
|
||||
}
|
||||
|
||||
private void ReadResources(ContentCatalogDataJson data)
|
||||
{
|
||||
List<Bucket> buckets;
|
||||
|
||||
MemoryStream bucketStream = new MemoryStream(Convert.FromBase64String(data.m_BucketDataString));
|
||||
using (BinaryReader bucketReader = new BinaryReader(bucketStream))
|
||||
{
|
||||
int bucketCount = bucketReader.ReadInt32();
|
||||
buckets = new List<Bucket>(bucketCount);
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
int offset = bucketReader.ReadInt32();
|
||||
|
||||
int entryCount = bucketReader.ReadInt32();
|
||||
int[] entries = new int[entryCount];
|
||||
for (int j = 0; j < entryCount; j++)
|
||||
{
|
||||
entries[j] = bucketReader.ReadInt32();
|
||||
}
|
||||
|
||||
buckets.Add(new Bucket(offset, entries));
|
||||
}
|
||||
}
|
||||
|
||||
List<object> keys;
|
||||
|
||||
MemoryStream keyDataStream = new MemoryStream(Convert.FromBase64String(data.m_KeyDataString));
|
||||
using (BinaryReader keyReader = new BinaryReader(keyDataStream))
|
||||
{
|
||||
int keyCount = keyReader.ReadInt32();
|
||||
keys = new List<object>(keyCount);
|
||||
|
||||
for (int i = 0; i < keyCount; i++)
|
||||
{
|
||||
keyDataStream.Position = buckets[i].offset;
|
||||
keys.Add(SerializedObjectDecoder.Decode(keyReader));
|
||||
}
|
||||
}
|
||||
|
||||
List<ResourceLocation> locations;
|
||||
|
||||
MemoryStream entryDataStream = new MemoryStream(Convert.FromBase64String(data.m_EntryDataString));
|
||||
MemoryStream extraDataStream = new MemoryStream(Convert.FromBase64String(data.m_ExtraDataString));
|
||||
using (BinaryReader entryReader = new BinaryReader(entryDataStream))
|
||||
using (BinaryReader extraReader = new BinaryReader(extraDataStream))
|
||||
{
|
||||
int entryCount = entryReader.ReadInt32();
|
||||
locations = new List<ResourceLocation>(entryCount);
|
||||
|
||||
for (int i = 0; i < entryCount; i++)
|
||||
{
|
||||
int internalIdIndex = entryReader.ReadInt32();
|
||||
int providerIndex = entryReader.ReadInt32();
|
||||
int dependencyKeyIndex = entryReader.ReadInt32();
|
||||
int depHash = entryReader.ReadInt32();
|
||||
int dataIndex = entryReader.ReadInt32();
|
||||
int primaryKeyIndex = entryReader.ReadInt32();
|
||||
int resourceTypeIndex = entryReader.ReadInt32();
|
||||
|
||||
string internalId = InternalIds[internalIdIndex];
|
||||
|
||||
string providerId = ProviderIds[providerIndex];
|
||||
|
||||
object dependencyKey = null;
|
||||
if (dependencyKeyIndex >= 0)
|
||||
{
|
||||
dependencyKey = keys[dependencyKeyIndex];
|
||||
}
|
||||
|
||||
object objData = null;
|
||||
if (dataIndex >= 0)
|
||||
{
|
||||
extraDataStream.Position = dataIndex;
|
||||
objData = SerializedObjectDecoder.Decode(extraReader);
|
||||
}
|
||||
|
||||
object primaryKey = keys[primaryKeyIndex];
|
||||
SerializedType resourceType = ResourceTypes[resourceTypeIndex];
|
||||
|
||||
var loc = new ResourceLocation();
|
||||
loc.ReadCompact(internalId, providerId, dependencyKey, objData, depHash, primaryKey, resourceType);
|
||||
locations.Add(loc);
|
||||
}
|
||||
}
|
||||
|
||||
Resources = new Dictionary<object, List<ResourceLocation>>(buckets.Count);
|
||||
for (int i = 0; i < buckets.Count; i++)
|
||||
{
|
||||
int[] bucketEntries = buckets[i].entries;
|
||||
List<ResourceLocation> locs = new List<ResourceLocation>(bucketEntries.Length);
|
||||
for (int j = 0; j < bucketEntries.Length; j++)
|
||||
{
|
||||
locs.Add(locations[bucketEntries[j]]);
|
||||
}
|
||||
Resources[keys[i]] = locs;
|
||||
}
|
||||
}
|
||||
|
||||
private struct Bucket
|
||||
{
|
||||
public int offset;
|
||||
public int[] entries;
|
||||
|
||||
public Bucket(int offset, int[] entries)
|
||||
{
|
||||
this.offset = offset;
|
||||
this.entries = entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using AddressablesTools.JSON;
|
||||
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
public class ObjectInitializationData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public SerializedType ObjectType { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
internal void Read(ObjectInitializationDataJson obj)
|
||||
{
|
||||
Id = obj.m_Id;
|
||||
ObjectType = new SerializedType();
|
||||
ObjectType.Read(obj.m_ObjectType);
|
||||
Data = obj.m_Data;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
public class ResourceLocation
|
||||
{
|
||||
public string InternalId { get; set; }
|
||||
public string ProviderId { get; set; }
|
||||
public object Dependency { get; set; }
|
||||
public object Data { get; set; }
|
||||
public int HashCode { get; set; }
|
||||
public int DependencyHashCode { get; set; }
|
||||
public string PrimaryKey { get; set; }
|
||||
public SerializedType Type { get; set; }
|
||||
|
||||
internal void ReadCompact(
|
||||
string internalId, string providerId, object dependencyKey, object data,
|
||||
int depHashCode, object primaryKey, SerializedType resourceType
|
||||
)
|
||||
{
|
||||
InternalId = internalId;
|
||||
ProviderId = providerId;
|
||||
Dependency = dependencyKey;
|
||||
Data = data;
|
||||
HashCode = internalId.GetHashCode() * 31 + providerId.GetHashCode();
|
||||
DependencyHashCode = depHashCode;
|
||||
PrimaryKey = primaryKey.ToString();
|
||||
Type = resourceType;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
internal static class SerializedObjectDecoder
|
||||
{
|
||||
internal enum ObjectType
|
||||
{
|
||||
AsciiString,
|
||||
UnicodeString,
|
||||
UInt16,
|
||||
UInt32,
|
||||
Int32,
|
||||
Hash128,
|
||||
Type,
|
||||
JsonObject
|
||||
}
|
||||
|
||||
internal static object Decode(BinaryReader br)
|
||||
{
|
||||
ObjectType type = (ObjectType)br.ReadByte();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ObjectType.AsciiString:
|
||||
{
|
||||
string str = ReadString4(br);
|
||||
return str;
|
||||
}
|
||||
|
||||
case ObjectType.UnicodeString:
|
||||
{
|
||||
string str = ReadString4Unicode(br);
|
||||
return str;
|
||||
}
|
||||
|
||||
case ObjectType.UInt16:
|
||||
{
|
||||
return br.ReadUInt16();
|
||||
}
|
||||
|
||||
case ObjectType.UInt32:
|
||||
{
|
||||
return br.ReadUInt32();
|
||||
}
|
||||
|
||||
case ObjectType.Int32:
|
||||
{
|
||||
return br.ReadInt32();
|
||||
}
|
||||
|
||||
case ObjectType.Hash128:
|
||||
{
|
||||
// read as string for now
|
||||
string str = ReadString1(br);
|
||||
return str;
|
||||
}
|
||||
|
||||
case ObjectType.Type:
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(ObjectType)}.{nameof(ObjectType.Type)} is only supported on windows because it uses {nameof(Type.GetTypeFromCLSID)}");
|
||||
}
|
||||
string str = ReadString1(br);
|
||||
return Type.GetTypeFromCLSID(new Guid(str));
|
||||
}
|
||||
|
||||
case ObjectType.JsonObject:
|
||||
{
|
||||
string assemblyName = ReadString1(br);
|
||||
string className = ReadString1(br);
|
||||
string jsonText = ReadString4Unicode(br);
|
||||
ClassJsonObject jsonObj = new ClassJsonObject(assemblyName, className, jsonText);
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadString1(BinaryReader br)
|
||||
{
|
||||
int length = br.ReadByte();
|
||||
string str = Encoding.ASCII.GetString(br.ReadBytes(length));
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string ReadString4(BinaryReader br)
|
||||
{
|
||||
int length = br.ReadInt32();
|
||||
string str = Encoding.ASCII.GetString(br.ReadBytes(length));
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string ReadString4Unicode(BinaryReader br)
|
||||
{
|
||||
int length = br.ReadInt32();
|
||||
string str = Encoding.Unicode.GetString(br.ReadBytes(length));
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
using AddressablesTools.JSON;
|
||||
|
||||
namespace AddressablesTools.Catalog
|
||||
{
|
||||
public class SerializedType
|
||||
{
|
||||
public string AssemblyName { get; set; }
|
||||
public string ClassName { get; set; }
|
||||
|
||||
internal void Read(SerializedTypeJson type)
|
||||
{
|
||||
AssemblyName = type.m_AssemblyName;
|
||||
ClassName = type.m_ClassName;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
namespace AddressablesTools.JSON
|
||||
{
|
||||
#pragma warning disable IDE1006
|
||||
internal class ContentCatalogDataJson
|
||||
{
|
||||
public string m_LocatorId { get; set; }
|
||||
public ObjectInitializationDataJson m_InstanceProviderData { get; set; }
|
||||
public ObjectInitializationDataJson m_SceneProviderData { get; set; }
|
||||
public ObjectInitializationDataJson[] m_ResourceProviderData { get; set; }
|
||||
public string[] m_ProviderIds { get; set; }
|
||||
public string[] m_InternalIds { get; set; }
|
||||
public string m_KeyDataString { get; set; }
|
||||
public string m_BucketDataString { get; set; }
|
||||
public string m_EntryDataString { get; set; }
|
||||
public string m_ExtraDataString { get; set; }
|
||||
public SerializedTypeJson[] m_resourceTypes { get; set; }
|
||||
public string[] m_InternalIdPrefixes { get; set; }
|
||||
}
|
||||
#pragma warning restore IDE1006
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
namespace AddressablesTools.JSON
|
||||
{
|
||||
#pragma warning disable IDE1006
|
||||
internal class ObjectInitializationDataJson
|
||||
{
|
||||
public string m_Id { get; set; }
|
||||
public SerializedTypeJson m_ObjectType { get; set; }
|
||||
public string m_Data { get; set; }
|
||||
}
|
||||
#pragma warning restore IDE1006
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
namespace AddressablesTools.JSON
|
||||
{
|
||||
#pragma warning disable IDE1006
|
||||
internal class SerializedTypeJson
|
||||
{
|
||||
public string m_AssemblyName { get; set; }
|
||||
public string m_ClassName { get; set; }
|
||||
}
|
||||
#pragma warning restore IDE1006
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using AssetsTools.NET.Extra;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
public abstract class AssetParser
|
||||
{
|
||||
protected static readonly string rootPath;
|
||||
protected static readonly AssetsManager assetsManager;
|
||||
|
||||
private static readonly ThreadSafeMonoCecilTempGenerator monoGen;
|
||||
|
||||
static AssetParser()
|
||||
{
|
||||
rootPath = ResourceAssetsParser.FindDirectoryContainingResourceAssets();
|
||||
assetsManager = new AssetsManager();
|
||||
assetsManager.LoadClassPackage(Path.Combine(NitroxUser.AssetsPath, "Resources", "classdata.tpk"));
|
||||
assetsManager.LoadClassDatabaseFromPackage("2019.4.36f1");
|
||||
assetsManager.SetMonoTempGenerator(monoGen = new ThreadSafeMonoCecilTempGenerator(Path.Combine(rootPath, "Managed")));
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
assetsManager.UnloadAll(true);
|
||||
monoGen.Dispose();
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Abstract;
|
||||
|
||||
public abstract class BundleFileParser<T> : AssetParser
|
||||
{
|
||||
protected static AssetsFileInstance assetFileInst;
|
||||
protected static AssetsFile bundleFile;
|
||||
|
||||
protected BundleFileParser(string bundleName, int index)
|
||||
{
|
||||
string standaloneFolderName = "StandaloneWindows64";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
standaloneFolderName = "StandaloneOSX";
|
||||
}
|
||||
string bundlePath = Path.Combine(ResourceAssetsParser.FindDirectoryContainingResourceAssets(), "StreamingAssets", "aa", standaloneFolderName, bundleName);
|
||||
BundleFileInstance bundleFileInst = assetsManager.LoadBundleFile(bundlePath);
|
||||
assetFileInst = assetsManager.LoadAssetsFileFromBundle(bundleFileInst, index, true);
|
||||
bundleFile = assetFileInst.file;
|
||||
}
|
||||
|
||||
public abstract T ParseFile();
|
||||
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
using System.IO;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Abstract;
|
||||
|
||||
public abstract class ResourceFileParser<T> : AssetParser
|
||||
{
|
||||
protected static readonly AssetsFileInstance resourceInst;
|
||||
protected static readonly AssetsFile resourceFile;
|
||||
|
||||
static ResourceFileParser()
|
||||
{
|
||||
resourceInst = assetsManager.LoadAssetsFile(Path.Combine(rootPath, "resources.assets"), true);
|
||||
resourceFile = resourceInst.file;
|
||||
}
|
||||
public abstract T ParseFile();
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
public class EntityDistributionsParser : ResourceFileParser<string>
|
||||
{
|
||||
public override string ParseFile()
|
||||
{
|
||||
AssetFileInfo assetFileInfo = resourceFile.GetAssetInfo(assetsManager, "EntityDistributions", AssetClassID.TextAsset);
|
||||
AssetTypeValueField assetValue = assetsManager.GetBaseField(resourceInst, assetFileInfo);
|
||||
string json = assetValue["m_Script"].AsString;
|
||||
|
||||
assetsManager.UnloadAll();
|
||||
return json;
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
using AssetsTools.NET;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
public static class AssetTypeValueFieldExtension
|
||||
{
|
||||
public static Vector3 ToVector3(this AssetTypeValueField valueField)
|
||||
{
|
||||
return new Vector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat);
|
||||
}
|
||||
|
||||
public static NitroxVector3 ToNitroxVector3(this AssetTypeValueField valueField)
|
||||
{
|
||||
return new NitroxVector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat);
|
||||
}
|
||||
|
||||
public static NitroxQuaternion ToNitroxQuaternion(this AssetTypeValueField valueField)
|
||||
{
|
||||
return new NitroxQuaternion(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat, valueField["w"].AsFloat);
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
public class AssetsBundleManager : AssetsManager
|
||||
{
|
||||
private readonly string aaRootPath;
|
||||
private readonly Dictionary<AssetsFileInstance, string[]> dependenciesByAssetFileInst = new();
|
||||
private ThreadSafeMonoCecilTempGenerator monoTempGenerator;
|
||||
|
||||
public AssetsBundleManager(string aaRootPath)
|
||||
{
|
||||
this.aaRootPath = aaRootPath;
|
||||
}
|
||||
|
||||
public string CleanBundlePath(string bundlePath)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
bundlePath = bundlePath.Replace('\\', '/');
|
||||
}
|
||||
|
||||
return aaRootPath + bundlePath.Substring(bundlePath.IndexOf('}') + 1);
|
||||
}
|
||||
|
||||
public AssetsFileInstance LoadBundleWithDependencies(string[] bundlePaths)
|
||||
{
|
||||
BundleFileInstance bundleFile = LoadBundleFile(CleanBundlePath(bundlePaths[0]));
|
||||
AssetsFileInstance assetFileInstance = LoadAssetsFileFromBundle(bundleFile, 0);
|
||||
|
||||
dependenciesByAssetFileInst[assetFileInstance] = bundlePaths;
|
||||
return assetFileInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copied from https://github.com/nesrak1/AssetsTools.NET#full-monobehaviour-writing-example
|
||||
/// </summary>
|
||||
/// <param name="inst"><see cref="AssetsFileInstance" /> instance currently used</param>
|
||||
/// <param name="targetGameObjectValue"><see cref="AssetFileInfo" /> of the target GameObject</param>
|
||||
/// <param name="targetClassName">Class name of the target MonoBehaviour</param>
|
||||
public AssetFileInfo GetMonoBehaviourFromGameObject(AssetsFileInstance inst, AssetFileInfo targetGameObjectValue, string targetClassName)
|
||||
{
|
||||
//example for finding a specific script and modifying the script on a GameObject
|
||||
AssetTypeValueField playerBf = GetBaseField(inst, targetGameObjectValue);
|
||||
AssetTypeValueField playerComponentArr = playerBf["m_Component"]["Array"];
|
||||
|
||||
AssetFileInfo monoBehaviourInf = null;
|
||||
//first let's search for the MonoBehaviour we want in a GameObject
|
||||
foreach (AssetTypeValueField child in playerComponentArr.Children)
|
||||
{
|
||||
//get component info (but don't deserialize yet, loading assets we don't need is wasteful)
|
||||
AssetTypeValueField childPtr = child["component"];
|
||||
AssetExternal childExt = GetExtAsset(inst, childPtr, true);
|
||||
AssetFileInfo childInf = childExt.info;
|
||||
|
||||
//skip if not MonoBehaviour
|
||||
if (childInf.GetTypeId(inst.file) != (int)AssetClassID.MonoBehaviour)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//actually deserialize the MonoBehaviour asset now
|
||||
AssetTypeValueField childBf = GetExtAssetSafe(inst, childPtr).baseField;
|
||||
AssetTypeValueField monoScriptPtr = childBf["m_Script"];
|
||||
|
||||
//get MonoScript from MonoBehaviour
|
||||
AssetExternal monoScriptExt = GetExtAsset(childExt.file, monoScriptPtr);
|
||||
AssetTypeValueField monoScriptBf = monoScriptExt.baseField;
|
||||
|
||||
string className = monoScriptBf["m_ClassName"].AsString;
|
||||
if (className == targetClassName)
|
||||
{
|
||||
monoBehaviourInf = childInf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return monoBehaviourInf;
|
||||
}
|
||||
|
||||
public NitroxTransform GetTransformFromGameObject(AssetsFileInstance assetFileInst, AssetTypeValueField rootGameObject)
|
||||
{
|
||||
AssetTypeValueField componentArray = rootGameObject["m_Component"]["Array"];
|
||||
|
||||
AssetTypeValueField transformRef = componentArray[0]["component"];
|
||||
AssetTypeValueField transformField = GetExtAsset(assetFileInst, transformRef).baseField;
|
||||
|
||||
return new(transformField["m_LocalPosition"].ToNitroxVector3(), transformField["m_LocalRotation"].ToNitroxQuaternion(), transformField["m_LocalScale"].ToNitroxVector3());
|
||||
}
|
||||
|
||||
public new void SetMonoTempGenerator(IMonoBehaviourTemplateGenerator generator)
|
||||
{
|
||||
monoTempGenerator = (ThreadSafeMonoCecilTempGenerator)generator;
|
||||
base.SetMonoTempGenerator(generator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a ready to use <see cref="AssetsManager" /> with loaded <see cref="AssetsManager.classDatabase" />, <see cref="AssetsManager.classPackage" /> and
|
||||
/// <see cref="IMonoBehaviourTemplateGenerator" />.
|
||||
/// </summary>
|
||||
public AssetsBundleManager Clone()
|
||||
{
|
||||
AssetsBundleManager bundleManagerInst = new(aaRootPath) { classDatabase = classDatabase, classPackage = classPackage };
|
||||
bundleManagerInst.SetMonoTempGenerator(monoTempGenerator);
|
||||
return bundleManagerInst;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AssetsManager.UnloadAll" />
|
||||
public new void UnloadAll(bool unloadClassData = false)
|
||||
{
|
||||
if (unloadClassData)
|
||||
{
|
||||
monoTempGenerator.Dispose();
|
||||
}
|
||||
dependenciesByAssetFileInst.Clear();
|
||||
base.UnloadAll(unloadClassData);
|
||||
}
|
||||
|
||||
private AssetExternal GetExtAssetSafe(AssetsFileInstance relativeTo, AssetTypeValueField valueField)
|
||||
{
|
||||
string[] bundlePaths = dependenciesByAssetFileInst[relativeTo];
|
||||
for (int i = 0; i < bundlePaths.Length; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
BundleFileInstance dependenciesBundleFile = LoadBundleFile(CleanBundlePath(bundlePaths[i]));
|
||||
LoadAssetsFileFromBundle(dependenciesBundleFile, 0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GetExtAsset(relativeTo, valueField);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could find AssetTypeValueField in given dependencies");
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
public static class AssetsFileMetadataExtension
|
||||
{
|
||||
public static AssetFileInfo GetAssetInfo(this AssetsFile assetsFile, AssetsManager assetsManager, string assetName, AssetClassID classID)
|
||||
{
|
||||
foreach (AssetFileInfo assetInfo in assetsFile.GetAssetsOfType(classID))
|
||||
{
|
||||
if (AssetHelper.GetAssetNameFast(assetsFile, assetsManager.classDatabase, assetInfo).Equals(assetName))
|
||||
{
|
||||
return assetInfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
|
||||
public class ThreadSafeMonoCecilTempGenerator : IMonoBehaviourTemplateGenerator, IDisposable
|
||||
{
|
||||
private readonly MonoCecilTempGenerator generator;
|
||||
private readonly Lock locker = new();
|
||||
|
||||
public ThreadSafeMonoCecilTempGenerator(string managedPath)
|
||||
{
|
||||
generator = new MonoCecilTempGenerator(managedPath);
|
||||
}
|
||||
|
||||
public AssetTypeTemplateField GetTemplateField(
|
||||
AssetTypeTemplateField baseField,
|
||||
string assemblyName,
|
||||
string nameSpace,
|
||||
string className,
|
||||
UnityVersion unityVersion)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return generator.GetTemplateField(baseField, assemblyName, nameSpace, className, unityVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (KeyValuePair<string, AssemblyDefinition> pair in generator.loadedAssemblies)
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
generator.loadedAssemblies.Clear();
|
||||
}
|
||||
}
|
@@ -0,0 +1,436 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AddressablesTools;
|
||||
using AddressablesTools.Catalog;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using Newtonsoft.Json;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
using NitroxServer.GameLogic.Entities;
|
||||
using NitroxServer.Resources;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
public class PrefabPlaceholderGroupsParser : IDisposable
|
||||
{
|
||||
private readonly string prefabDatabasePath;
|
||||
private readonly string aaRootPath;
|
||||
private readonly AssetsBundleManager am;
|
||||
private readonly ThreadSafeMonoCecilTempGenerator monoGen;
|
||||
private readonly JsonSerializer serializer;
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> classIdByRuntimeKey = new();
|
||||
private readonly ConcurrentDictionary<string, string[]> addressableCatalog = new();
|
||||
private readonly ConcurrentDictionary<string, PrefabPlaceholderAsset> placeholdersByClassId = new();
|
||||
private readonly ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> groupsByClassId = new();
|
||||
public ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId = [];
|
||||
|
||||
public PrefabPlaceholderGroupsParser()
|
||||
{
|
||||
string resourcePath = ResourceAssetsParser.FindDirectoryContainingResourceAssets();
|
||||
string managedPath = Path.Combine(resourcePath, "Managed");
|
||||
|
||||
string streamingAssetsPath = Path.Combine(resourcePath, "StreamingAssets");
|
||||
prefabDatabasePath = Path.Combine(streamingAssetsPath, "SNUnmanagedData", "prefabs.db");
|
||||
aaRootPath = Path.Combine(streamingAssetsPath, "aa");
|
||||
|
||||
am = new AssetsBundleManager(aaRootPath);
|
||||
|
||||
// ReSharper disable once StringLiteralTypo)
|
||||
am.LoadClassPackage(Path.Combine(NitroxUser.AssetsPath, "Resources", "classdata.tpk"));
|
||||
am.LoadClassDatabaseFromPackage("2019.4.36f1");
|
||||
am.SetMonoTempGenerator(monoGen = new(managedPath));
|
||||
|
||||
serializer = new()
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.Auto
|
||||
};
|
||||
}
|
||||
|
||||
public Dictionary<string, PrefabPlaceholdersGroupAsset> ParseFile()
|
||||
{
|
||||
// Get all prefab-classIds linked to the (partial) bundle path
|
||||
Dictionary<string, string> prefabDatabase = LoadPrefabDatabase(prefabDatabasePath);
|
||||
|
||||
// Loading all prefabs by their classId and file paths (first the path to the prefab then the dependencies)
|
||||
LoadAddressableCatalog(prefabDatabase);
|
||||
|
||||
string nitroxCachePath = Path.Combine(NitroxUser.AppDataPath, "Cache");
|
||||
Directory.CreateDirectory(nitroxCachePath);
|
||||
|
||||
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupPaths = null;
|
||||
string prefabPlaceholdersGroupAssetCachePath = Path.Combine(nitroxCachePath, "PrefabPlaceholdersGroupAssetsCache.json");
|
||||
if (File.Exists(prefabPlaceholdersGroupAssetCachePath))
|
||||
{
|
||||
Cache? cache = DeserializeCache(prefabPlaceholdersGroupAssetCachePath);
|
||||
if (cache.HasValue)
|
||||
{
|
||||
prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths;
|
||||
RandomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId;
|
||||
Log.Info($"Successfully loaded cache with {prefabPlaceholdersGroupPaths.Count} prefab placeholder groups and {RandomPossibilitiesByClassId.Count} random spawn behaviours.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback solution
|
||||
if (prefabPlaceholdersGroupPaths == null)
|
||||
{
|
||||
prefabPlaceholdersGroupPaths = MakeAndSerializeCache(prefabPlaceholdersGroupAssetCachePath);
|
||||
Log.Info($"Successfully built cache with {prefabPlaceholdersGroupPaths.Count} prefab placeholder groups and {RandomPossibilitiesByClassId.Count} random spawn behaviours. Future server starts will take less time.");
|
||||
}
|
||||
|
||||
// Select only prefabs with a PrefabPlaceholdersGroups component in the root ans link them with their dependencyPaths
|
||||
// Do not remove: the internal cache list is slowing down the process more than loading a few assets again. There maybe is a better way in the new AssetToolsNetVersion but we need a byte to texture library bc ATNs sub-package is only for netstandard.
|
||||
am.UnloadAll();
|
||||
|
||||
// Get all needed data for the filtered PrefabPlaceholdersGroups to construct PrefabPlaceholdersGroupAssets and add them to the dictionary by classId
|
||||
return prefabPlaceholdersGroupPaths;
|
||||
}
|
||||
|
||||
private Dictionary<string, PrefabPlaceholdersGroupAsset> MakeAndSerializeCache(string filePath)
|
||||
{
|
||||
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = GetAllPrefabPlaceholdersGroupsFast();
|
||||
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupAssets = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths));
|
||||
using StreamWriter stream = File.CreateText(filePath);
|
||||
serializer.Serialize(stream, new Cache(prefabPlaceholdersGroupAssets, RandomPossibilitiesByClassId));
|
||||
|
||||
return prefabPlaceholdersGroupAssets;
|
||||
}
|
||||
|
||||
private Cache? DeserializeCache(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using StreamReader reader = File.OpenText(filePath);
|
||||
|
||||
return (Cache)serializer.Deserialize(reader, typeof(Cache));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception, "An error occurred while deserializing the game Cache. Re-creating it.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> LoadPrefabDatabase(string fullFilename)
|
||||
{
|
||||
Dictionary<string, string> prefabFiles = new();
|
||||
if (!File.Exists(fullFilename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using FileStream input = File.OpenRead(fullFilename);
|
||||
using BinaryReader binaryReader = new(input);
|
||||
int num = binaryReader.ReadInt32();
|
||||
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
string key = binaryReader.ReadString();
|
||||
string value = binaryReader.ReadString();
|
||||
prefabFiles[key] = value;
|
||||
}
|
||||
|
||||
return prefabFiles;
|
||||
}
|
||||
|
||||
private void LoadAddressableCatalog(Dictionary<string, string> prefabDatabase)
|
||||
{
|
||||
ContentCatalogData ccd = AddressablesJsonParser.FromString(File.ReadAllText(Path.Combine(aaRootPath, "catalog.json")));
|
||||
Dictionary<string, string> classIdByPath = prefabDatabase.ToDictionary(m => m.Value, m => m.Key);
|
||||
|
||||
foreach (KeyValuePair<object, List<ResourceLocation>> entry in ccd.Resources)
|
||||
{
|
||||
if (entry.Key is string primaryKey && primaryKey.Length == 32 &&
|
||||
classIdByPath.TryGetValue(entry.Value[0].PrimaryKey, out string classId))
|
||||
{
|
||||
classIdByRuntimeKey.TryAdd(primaryKey, classId);
|
||||
}
|
||||
}
|
||||
foreach (KeyValuePair<string, string> prefabAddressable in prefabDatabase)
|
||||
{
|
||||
foreach (ResourceLocation resourceLocation in ccd.Resources[prefabAddressable.Value])
|
||||
{
|
||||
if (resourceLocation.ProviderId != "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<ResourceLocation> resourceLocations = ccd.Resources[resourceLocation.Dependency];
|
||||
|
||||
if (!addressableCatalog.TryAdd(prefabAddressable.Key, resourceLocations.Select(x => x.InternalId).ToArray()))
|
||||
{
|
||||
throw new InvalidOperationException($"Couldn't add item to {nameof(addressableCatalog)}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers bundle paths by class id for prefab placeholder groups.
|
||||
/// Also fills <see cref="RandomPossibilitiesByClassId"/>
|
||||
/// </summary>
|
||||
private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFast()
|
||||
{
|
||||
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = new();
|
||||
|
||||
// First step is to find out about the hash of the types PrefabPlaceholdersGroup and SpawnRandom
|
||||
// to be able to recognize them easily later on
|
||||
byte[] prefabPlaceholdersGroupHash = [];
|
||||
byte[] spawnRandomHash = [];
|
||||
|
||||
for (int aaIndex = 0; aaIndex < addressableCatalog.Count; aaIndex++)
|
||||
{
|
||||
KeyValuePair<string, string[]> keyValuePair = addressableCatalog.ElementAt(aaIndex);
|
||||
BundleFileInstance bundleFile = am.LoadBundleFile(am.CleanBundlePath(keyValuePair.Value[0]));
|
||||
AssetsFileInstance assetFileInstance = am.LoadAssetsFileFromBundle(bundleFile, 0);
|
||||
|
||||
foreach (AssetFileInfo monoScriptInfo in assetFileInstance.file.GetAssetsOfType(AssetClassID.MonoScript))
|
||||
{
|
||||
AssetTypeValueField monoScript = am.GetBaseField(assetFileInstance, monoScriptInfo);
|
||||
|
||||
switch (monoScript["m_Name"].AsString)
|
||||
{
|
||||
case "SpawnRandom":
|
||||
spawnRandomHash = new byte[16];
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
spawnRandomHash[i] = monoScript["m_PropertiesHash"][i].AsByte;
|
||||
}
|
||||
break;
|
||||
case "PrefabPlaceholdersGroup":
|
||||
prefabPlaceholdersGroupHash = new byte[16];
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
prefabPlaceholdersGroupHash[i] = monoScript["m_PropertiesHash"][i].AsByte;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefabPlaceholdersGroupHash.Length > 0 && spawnRandomHash.Length > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now use the bundle paths and the hashes to find out which items from the catalog are important
|
||||
// We fill prefabPlaceholdersGroupPaths and RandomPossibilitiesByClassId when we find objects with a SpawnRandom
|
||||
Parallel.ForEach(addressableCatalog, (keyValuePair) =>
|
||||
{
|
||||
string[] assetPaths = keyValuePair.Value;
|
||||
|
||||
AssetsBundleManager bundleManagerInst = am.Clone();
|
||||
AssetsFileInstance assetFileInstance = bundleManagerInst.LoadBundleWithDependencies(assetPaths);
|
||||
|
||||
foreach (TypeTreeType typeTreeType in assetFileInstance.file.Metadata.TypeTreeTypes)
|
||||
{
|
||||
if (typeTreeType.TypeId != (int)AssetClassID.MonoBehaviour)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeTreeType.TypeHash.data.SequenceEqual(prefabPlaceholdersGroupHash))
|
||||
{
|
||||
prefabPlaceholdersGroupPaths.TryAdd(keyValuePair.Key, keyValuePair.Value);
|
||||
break;
|
||||
}
|
||||
else if (typeTreeType.TypeHash.data.SequenceEqual(spawnRandomHash))
|
||||
{
|
||||
AssetsFileInstance assetFileInst = bundleManagerInst.LoadBundleWithDependencies(assetPaths);
|
||||
|
||||
GetPrefabGameObjectInfoFromBundle(bundleManagerInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
|
||||
|
||||
AssetFileInfo spawnRandomInfo = bundleManagerInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom");
|
||||
// See SpawnRandom.Start
|
||||
AssetTypeValueField spawnRandom = bundleManagerInst.GetBaseField(assetFileInst, spawnRandomInfo);
|
||||
List<string> classIds = [];
|
||||
foreach (AssetTypeValueField assetReference in spawnRandom["assetReferences"])
|
||||
{
|
||||
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
|
||||
}
|
||||
|
||||
RandomPossibilitiesByClassId.TryAdd(keyValuePair.Key, [.. classIds]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bundleManagerInst.UnloadAll();
|
||||
});
|
||||
|
||||
return prefabPlaceholdersGroupPaths;
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlaceholderGroupAssetsByGroupClassId(ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths)
|
||||
{
|
||||
ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholderGroupsByGroupClassId = new();
|
||||
|
||||
Parallel.ForEach(prefabPlaceholdersGroupPaths, (keyValuePair) =>
|
||||
{
|
||||
AssetsBundleManager bundleManagerInst = am.Clone();
|
||||
AssetsFileInstance assetFileInst = bundleManagerInst.LoadBundleWithDependencies(keyValuePair.Value);
|
||||
|
||||
PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(bundleManagerInst, assetFileInst, keyValuePair.Key);
|
||||
bundleManagerInst.UnloadAll();
|
||||
|
||||
if (!prefabPlaceholderGroupsByGroupClassId.TryAdd(keyValuePair.Key, prefabPlaceholderGroup))
|
||||
{
|
||||
throw new InvalidOperationException($"Couldn't add item to {nameof(prefabPlaceholderGroupsByGroupClassId)}");
|
||||
}
|
||||
});
|
||||
return prefabPlaceholderGroupsByGroupClassId;
|
||||
}
|
||||
|
||||
private static void GetPrefabGameObjectInfoFromBundle(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, out AssetFileInfo prefabGameObjectInfo)
|
||||
{
|
||||
//Get the main asset with "m_Container" of the "AssetBundle-asset" inside the bundle
|
||||
AssetFileInfo assetBundleInfo = assetFileInst.file.Metadata.GetAssetInfo(1);
|
||||
AssetTypeValueField assetBundleValue = amInst.GetBaseField(assetFileInst, assetBundleInfo);
|
||||
AssetTypeValueField assetBundleContainer = assetBundleValue["m_Container.Array"];
|
||||
long rootAssetPathId = assetBundleContainer.Children[0][1]["asset.m_PathID"].AsLong;
|
||||
|
||||
prefabGameObjectInfo = assetFileInst.file.Metadata.GetAssetInfo(rootAssetPathId);
|
||||
}
|
||||
|
||||
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, string classId)
|
||||
{
|
||||
GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
|
||||
return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId);
|
||||
}
|
||||
|
||||
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(classId) && groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup))
|
||||
{
|
||||
return cachedGroup;
|
||||
}
|
||||
|
||||
AssetFileInfo prefabPlaceholdersGroupInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, rootGameObjectInfo, "PrefabPlaceholdersGroup");
|
||||
if (prefabPlaceholdersGroupInfo == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
AssetTypeValueField prefabPlaceholdersGroupScript = amInst.GetBaseField(assetFileInst, prefabPlaceholdersGroupInfo);
|
||||
List<AssetTypeValueField> prefabPlaceholdersOnGroup = prefabPlaceholdersGroupScript["prefabPlaceholders"].Children;
|
||||
|
||||
IPrefabAsset[] prefabPlaceholders = new IPrefabAsset[prefabPlaceholdersOnGroup.Count];
|
||||
|
||||
for (int index = 0; index < prefabPlaceholdersOnGroup.Count; index++)
|
||||
{
|
||||
AssetTypeValueField prefabPlaceholderPtr = prefabPlaceholdersOnGroup[index];
|
||||
AssetTypeValueField prefabPlaceholder = amInst.GetExtAsset(assetFileInst, prefabPlaceholderPtr).baseField;
|
||||
|
||||
AssetTypeValueField gameObjectPtr = prefabPlaceholder["m_GameObject"];
|
||||
AssetTypeValueField gameObjectField = amInst.GetExtAsset(assetFileInst, gameObjectPtr).baseField;
|
||||
NitroxTransform transform = amInst.GetTransformFromGameObject(assetFileInst, gameObjectField);
|
||||
IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString);
|
||||
asset.Transform = transform;
|
||||
prefabPlaceholders[index] = asset;
|
||||
}
|
||||
|
||||
PrefabPlaceholdersGroupAsset prefabPlaceholdersGroup = new(classId, prefabPlaceholders);
|
||||
AssetTypeValueField rootGameObjectField = amInst.GetBaseField(assetFileInst, rootGameObjectInfo);
|
||||
NitroxTransform groupTransform = amInst.GetTransformFromGameObject(assetFileInst, rootGameObjectField);
|
||||
prefabPlaceholdersGroup.Transform = groupTransform;
|
||||
|
||||
groupsByClassId[classId] = prefabPlaceholdersGroup;
|
||||
return prefabPlaceholdersGroup;
|
||||
}
|
||||
|
||||
private IPrefabAsset GetAndCacheAsset(AssetsBundleManager amInst, string classId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(classId))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
if (groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup))
|
||||
{
|
||||
return cachedGroup;
|
||||
}
|
||||
else if (placeholdersByClassId.TryGetValue(classId, out PrefabPlaceholderAsset cachedPlaceholder))
|
||||
{
|
||||
return cachedPlaceholder;
|
||||
}
|
||||
if (!addressableCatalog.TryGetValue(classId, out string[] assetPaths))
|
||||
{
|
||||
Log.Error($"Couldn't get PrefabPlaceholder with classId: {classId}");
|
||||
return default;
|
||||
}
|
||||
|
||||
AssetsFileInstance assetFileInst = amInst.LoadBundleWithDependencies(assetPaths);
|
||||
|
||||
GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
|
||||
|
||||
AssetFileInfo placeholdersGroupInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "PrefabPlaceholdersGroup");
|
||||
if (placeholdersGroupInfo != null)
|
||||
{
|
||||
PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(amInst, assetFileInst, classId);
|
||||
groupsByClassId[classId] = groupAsset;
|
||||
return groupAsset;
|
||||
}
|
||||
|
||||
AssetFileInfo spawnRandomInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom");
|
||||
if (spawnRandomInfo != null)
|
||||
{
|
||||
// See SpawnRandom.Start
|
||||
AssetTypeValueField spawnRandom = amInst.GetBaseField(assetFileInst, spawnRandomInfo);
|
||||
List<string> classIds = new();
|
||||
foreach (AssetTypeValueField assetReference in spawnRandom["assetReferences"])
|
||||
{
|
||||
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
|
||||
}
|
||||
|
||||
return new PrefabPlaceholderRandomAsset(classIds);
|
||||
}
|
||||
|
||||
AssetFileInfo databoxSpawnerInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "DataboxSpawner");
|
||||
if (databoxSpawnerInfo != null)
|
||||
{
|
||||
// NB: This spawning should be cancelled if the techType is from a known tech
|
||||
// But it doesn't matter if we still spawn it so we do so.
|
||||
// See DataboxSpawner.Start
|
||||
AssetTypeValueField databoxSpawner = amInst.GetBaseField(assetFileInst, databoxSpawnerInfo);
|
||||
string runtimeKey = databoxSpawner["databoxPrefabReference"]["m_AssetGUID"].AsString;
|
||||
|
||||
PrefabPlaceholderAsset databoxAsset = new(classIdByRuntimeKey[runtimeKey]);
|
||||
placeholdersByClassId[classId] = databoxAsset;
|
||||
return databoxAsset;
|
||||
}
|
||||
|
||||
AssetFileInfo entitySlotInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "EntitySlot");
|
||||
NitroxEntitySlot? nitroxEntitySlot = null;
|
||||
if (entitySlotInfo != null)
|
||||
{
|
||||
AssetTypeValueField entitySlot = amInst.GetBaseField(assetFileInst, entitySlotInfo);
|
||||
string biomeType = ((BiomeType)entitySlot["biomeType"].AsInt).ToString();
|
||||
|
||||
List<string> allowedTypes = [];
|
||||
foreach (AssetTypeValueField allowedType in entitySlot["allowedTypes"])
|
||||
{
|
||||
allowedTypes.Add(((EntitySlot.Type)allowedType.AsInt).ToString());
|
||||
}
|
||||
|
||||
nitroxEntitySlot = new NitroxEntitySlot(biomeType, allowedTypes);
|
||||
}
|
||||
|
||||
PrefabPlaceholderAsset prefabPlaceholderAsset = new(classId, nitroxEntitySlot);
|
||||
placeholdersByClassId[classId] = prefabPlaceholderAsset;
|
||||
return prefabPlaceholderAsset;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
monoGen.Dispose();
|
||||
am.UnloadAll(true);
|
||||
}
|
||||
|
||||
record struct Cache(Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupPaths, ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId);
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
public class RandomStartParser : BundleFileParser<RandomStartGenerator>
|
||||
{
|
||||
public RandomStartParser() : base("essentials.unity_0ee8dd89ed55f05bc38a09cc77137d4e.bundle", 0) { }
|
||||
|
||||
public override RandomStartGenerator ParseFile()
|
||||
{
|
||||
AssetFileInfo assetFile = bundleFile.GetAssetInfo(assetsManager, "RandomStart", AssetClassID.Texture2D);
|
||||
AssetTypeValueField textureValueField = assetsManager.GetBaseField(assetFileInst, assetFile);
|
||||
TextureFile textureFile = TextureFile.ReadTextureFile(textureValueField);
|
||||
byte[] texDat = textureFile.GetTextureData(assetFileInst);
|
||||
assetsManager.UnloadAll();
|
||||
|
||||
if (texDat is not { Length: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Image<Bgra32> texture = Image.LoadPixelData<Bgra32>(texDat, textureFile.m_Width, textureFile.m_Height);
|
||||
texture.Mutate(x => x.Flip(FlipMode.Vertical));
|
||||
return new RandomStartGenerator(new PixelProvider(texture));
|
||||
}
|
||||
|
||||
private class PixelProvider : RandomStartGenerator.IPixelProvider
|
||||
{
|
||||
private readonly Image<Bgra32> texture;
|
||||
|
||||
public PixelProvider(Image<Bgra32> texture)
|
||||
{
|
||||
Validate.NotNull(texture);
|
||||
this.texture = texture;
|
||||
}
|
||||
|
||||
public byte GetRed(int x, int y) => texture[x, y].R;
|
||||
|
||||
public byte GetGreen(int x, int y) => texture[x, y].G;
|
||||
|
||||
public byte GetBlue(int x, int y) => texture[x, y].B;
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using AssetsTools.NET;
|
||||
using AssetsTools.NET.Extra;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
|
||||
using NitroxServer_Subnautica.Resources.Parsers.Helper;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
public class WorldEntityInfoParser : ResourceFileParser<Dictionary<string, WorldEntityInfo>>
|
||||
{
|
||||
public override Dictionary<string, WorldEntityInfo> ParseFile()
|
||||
{
|
||||
Dictionary<string, WorldEntityInfo> worldEntitiesByClassId = new();
|
||||
|
||||
AssetFileInfo assetFileInfo = resourceFile.GetAssetInfo(assetsManager, "WorldEntityData", AssetClassID.MonoBehaviour);
|
||||
AssetTypeValueField assetValue = assetsManager.GetBaseField(resourceInst, assetFileInfo);
|
||||
|
||||
foreach (AssetTypeValueField info in assetValue["infos"])
|
||||
{
|
||||
WorldEntityInfo entityData = new()
|
||||
{
|
||||
classId = info["classId"].AsString,
|
||||
techType = (TechType)info["techType"].AsInt,
|
||||
slotType = (EntitySlot.Type)info["slotType"].AsInt,
|
||||
prefabZUp = info["prefabZUp"].AsBool,
|
||||
cellLevel = (LargeWorldEntity.CellLevel)info["cellLevel"].AsInt,
|
||||
localScale = info["localScale"].ToVector3()
|
||||
};
|
||||
|
||||
worldEntitiesByClassId.Add(entityData.classId, entityData);
|
||||
}
|
||||
|
||||
assetsManager.UnloadAll();
|
||||
return worldEntitiesByClassId;
|
||||
}
|
||||
}
|
26
NitroxServer-Subnautica/Resources/ResourceAssets.cs
Normal file
26
NitroxServer-Subnautica/Resources/ResourceAssets.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer.Resources;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources;
|
||||
|
||||
public class ResourceAssets
|
||||
{
|
||||
public Dictionary<string, WorldEntityInfo> WorldEntitiesByClassId { get; init; } = new();
|
||||
public string LootDistributionsJson { get; init; } = "";
|
||||
public Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupsByGroupClassId { get; init; } = new();
|
||||
public Dictionary<string, string[]> RandomPossibilitiesByClassId { get; init; }
|
||||
public RandomStartGenerator NitroxRandom { get; init; }
|
||||
|
||||
public static void ValidateMembers(ResourceAssets resourceAssets)
|
||||
{
|
||||
Validate.NotNull(resourceAssets);
|
||||
Validate.IsTrue(resourceAssets.WorldEntitiesByClassId.Count > 0);
|
||||
Validate.IsTrue(resourceAssets.LootDistributionsJson != "");
|
||||
Validate.IsTrue(resourceAssets.PrefabPlaceholdersGroupsByGroupClassId.Count > 0);
|
||||
Validate.IsTrue(resourceAssets.RandomPossibilitiesByClassId.Count > 0);
|
||||
Validate.NotNull(resourceAssets.NitroxRandom);
|
||||
}
|
||||
}
|
62
NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs
Normal file
62
NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using NitroxModel;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer_Subnautica.Resources.Parsers;
|
||||
|
||||
namespace NitroxServer_Subnautica.Resources;
|
||||
|
||||
public static class ResourceAssetsParser
|
||||
{
|
||||
private static ResourceAssets resourceAssets;
|
||||
|
||||
public static ResourceAssets Parse()
|
||||
{
|
||||
if (resourceAssets != null)
|
||||
{
|
||||
return resourceAssets;
|
||||
}
|
||||
|
||||
using (PrefabPlaceholderGroupsParser prefabPlaceholderGroupsParser = new())
|
||||
{
|
||||
resourceAssets = new ResourceAssets
|
||||
{
|
||||
WorldEntitiesByClassId = new WorldEntityInfoParser().ParseFile(),
|
||||
LootDistributionsJson = new EntityDistributionsParser().ParseFile(),
|
||||
PrefabPlaceholdersGroupsByGroupClassId = prefabPlaceholderGroupsParser.ParseFile(),
|
||||
NitroxRandom = new RandomStartParser().ParseFile(),
|
||||
RandomPossibilitiesByClassId = new(prefabPlaceholderGroupsParser.RandomPossibilitiesByClassId)
|
||||
};
|
||||
}
|
||||
AssetParser.Dispose();
|
||||
|
||||
ResourceAssets.ValidateMembers(resourceAssets);
|
||||
return resourceAssets;
|
||||
}
|
||||
|
||||
public static string FindDirectoryContainingResourceAssets()
|
||||
{
|
||||
string subnauticaPath = NitroxUser.GamePath;
|
||||
if (string.IsNullOrEmpty(subnauticaPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException("Could not locate Subnautica installation directory for resource parsing.");
|
||||
}
|
||||
|
||||
if (File.Exists(Path.Combine(subnauticaPath, GameInfo.Subnautica.DataFolder, "resources.assets")))
|
||||
{
|
||||
return Path.Combine(subnauticaPath, GameInfo.Subnautica.DataFolder);
|
||||
}
|
||||
if (File.Exists(Path.Combine("..", "resources.assets"))) // SubServer => Subnautica/Subnautica_Data/SubServer
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(".."));
|
||||
}
|
||||
if (File.Exists(Path.Combine("..", GameInfo.Subnautica.DataFolder, "resources.assets"))) // SubServer => Subnautica/SubServer
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine("..", GameInfo.Subnautica.DataFolder));
|
||||
}
|
||||
if (File.Exists("resources.assets")) // SubServer/* => Subnautica/Subnautica_Data/
|
||||
{
|
||||
return Directory.GetCurrentDirectory();
|
||||
}
|
||||
throw new FileNotFoundException("Make sure resources.assets is in current or parent directory and readable.");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user