Files
Nitrox/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs
2025-07-06 00:23:46 +02:00

437 lines
20 KiB
C#

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