first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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);
}
}

View 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.");
}
}