Skip to content

Commit

Permalink
Fully treat SpawnRandom monobehaviour on server-side spawning, add a …
Browse files Browse the repository at this point in the history
…cache for prefab placeholder and SpawnRandom parsing
  • Loading branch information
tornac1234 committed Jan 7, 2025
1 parent 48412cf commit c529216
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using AddressablesTools.Catalog;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using Newtonsoft.Json;
using NitroxModel.DataStructures.Unity;
using NitroxServer.GameLogic.Entities;
using NitroxServer.Resources;
Expand All @@ -21,11 +22,13 @@ public class PrefabPlaceholderGroupsParser : IDisposable
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()
{
Expand All @@ -41,6 +44,11 @@ public PrefabPlaceholderGroupsParser()
am.LoadClassPackage(Path.Combine("Resources", "classdata.tpk"));
am.LoadClassDatabaseFromPackage("2019.4.36f1");
am.SetMonoTempGenerator(monoGen = new(managedPath));

serializer = new()
{
TypeNameHandling = TypeNameHandling.Auto
};
}

public Dictionary<string, PrefabPlaceholdersGroupAsset> ParseFile()
Expand All @@ -51,13 +59,62 @@ public Dictionary<string, PrefabPlaceholdersGroupAsset> ParseFile()
// Loading all prefabs by their classId and file paths (first the path to the prefab then the dependencies)
LoadAddressableCatalog(prefabDatabase);

string nitroxCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nitrox", "Cache");
if (!Directory.Exists(nitroxCachePath))
{
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
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = GetAllPrefabPlaceholdersGroupsFast();
// 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 new Dictionary<string, PrefabPlaceholdersGroupAsset>(GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths));
return prefabPlaceholdersGroupPaths;
}

private Dictionary<string, PrefabPlaceholdersGroupAsset> MakeAndSerializeCache(string path)
{
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = GetAllPrefabPlaceholdersGroupsFast();
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupAssets = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths));
using StreamWriter stream = File.CreateText(path);
serializer.Serialize(stream, new Cache(prefabPlaceholdersGroupAssets, RandomPossibilitiesByClassId));

return prefabPlaceholdersGroupAssets;
}

private Cache? DeserializeCache(string path)
{
try
{
using StreamReader reader = File.OpenText(path);

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)
Expand Down Expand Up @@ -86,7 +143,7 @@ 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 &&
Expand Down Expand Up @@ -116,13 +173,20 @@ private void LoadAddressableCatalog(Dictionary<string, string> prefabDatabase)
}
}

/// <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();
byte[] prefabPlaceholdersGroupHash = Array.Empty<byte>();

int aaIndex;
for (aaIndex = 0; aaIndex < addressableCatalog.Count; aaIndex++)
// 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]));
Expand All @@ -132,36 +196,64 @@ private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFas
{
AssetTypeValueField monoScript = am.GetBaseField(assetFileInstance, monoScriptInfo);

if (monoScript["m_Name"].AsString != "PrefabPlaceholdersGroup")
{
continue;
}

prefabPlaceholdersGroupHash = new byte[16];
for (int i = 0; i < 16; i++)
switch (monoScript["m_Name"].AsString)
{
prefabPlaceholdersGroupHash[i] = monoScript["m_PropertiesHash"][i].AsByte;
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;
}

break;
}

if (prefabPlaceholdersGroupHash.Length != 0)
if (prefabPlaceholdersGroupHash.Length > 0 && spawnRandomHash.Length > 0)
{
break;
}
}

Parallel.ForEach(addressableCatalog.Skip(aaIndex), (keyValuePair) =>
// 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();
BundleFileInstance bundleFile = bundleManagerInst.LoadBundleFile(bundleManagerInst.CleanBundlePath(keyValuePair.Value[0]));
AssetsFileInstance assetFileInstance = bundleManagerInst.LoadAssetsFileFromBundle(bundleFile, 0);
AssetsFileInstance assetFileInstance = bundleManagerInst.LoadBundleWithDependencies(assetPaths);

if (assetFileInstance.file.Metadata.TypeTreeTypes.Any(typeTree => typeTree.TypeId == (int)AssetClassID.MonoBehaviour && typeTree.TypeHash.data.SequenceEqual(prefabPlaceholdersGroupHash)))
{
prefabPlaceholdersGroupPaths.TryAdd(keyValuePair.Key, keyValuePair.Value);
}
else if (assetFileInstance.file.Metadata.TypeTreeTypes.Any(typeTree => typeTree.TypeId == (int)AssetClassID.MonoBehaviour && typeTree.TypeHash.data.SequenceEqual(spawnRandomHash)))
{
AssetsFileInstance assetFileInst = am.LoadBundleWithDependencies(assetPaths);

GetPrefabGameObjectInfoFromBundle(am, assetFileInst, out AssetFileInfo prefabGameObjectInfo);

AssetFileInfo spawnRandomInfo = am.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom");
if (spawnRandomInfo != null)
{
// See SpawnRandom.Start
AssetTypeValueField spawnRandom = am.GetBaseField(assetFileInst, spawnRandomInfo);
List<string> classIds = [];
foreach (AssetTypeValueField assetReference in spawnRandom["assetReferences"])
{
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
}

RandomPossibilitiesByClassId.TryAdd(keyValuePair.Key, [.. classIds]);
}
}

bundleManagerInst.UnloadAll();
});
Expand Down Expand Up @@ -288,7 +380,7 @@ private IPrefabAsset GetAndCacheAsset(AssetsBundleManager amInst, string classId
{
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
}

return new PrefabPlaceholderRandomAsset(classIds);
}

Expand Down Expand Up @@ -332,4 +424,6 @@ public void Dispose()
monoGen.Dispose();
am.UnloadAll(true);
}

record struct Cache(Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupPaths, ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId);
}
31 changes: 16 additions & 15 deletions NitroxServer-Subnautica/Resources/ResourceAssets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@
using NitroxServer.Resources;
using UWE;

namespace NitroxServer_Subnautica.Resources
namespace NitroxServer_Subnautica.Resources;

public class ResourceAssets
{
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 RandomStartGenerator NitroxRandom { get; init; }
public Dictionary<string, WorldEntityInfo> WorldEntitiesByClassId { get; init; } = new();
public string LootDistributionsJson { get; init; } = "";
public Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupsByGroupClassId { get; init; } = new();
public RandomStartGenerator NitroxRandom { get; init; }
public Dictionary<string, string[]> RandomPossibilitiesByClassId { 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.NotNull(resourceAssets.NitroxRandom);
}
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.NotNull(resourceAssets.NitroxRandom);
Validate.IsTrue(resourceAssets.RandomPossibilitiesByClassId.Count > 0);
}
}
5 changes: 3 additions & 2 deletions NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using NitroxModel.Helper;
using NitroxServer_Subnautica.Resources.Parsers;

Expand All @@ -22,7 +22,8 @@ public static ResourceAssets Parse()
WorldEntitiesByClassId = new WorldEntityInfoParser().ParseFile(),
LootDistributionsJson = new EntityDistributionsParser().ParseFile(),
PrefabPlaceholdersGroupsByGroupClassId = prefabPlaceholderGroupsParser.ParseFile(),
NitroxRandom = new RandomStartParser().ParseFile()
NitroxRandom = new RandomStartParser().ParseFile(),
RandomPossibilitiesByClassId = new(prefabPlaceholderGroupsParser.RandomPossibilitiesByClassId)
};
}
AssetParser.Dispose();
Expand Down
8 changes: 4 additions & 4 deletions NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
using System.Collections.Generic;
using Autofac;
using NitroxModel;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel_Subnautica.DataStructures.GameLogic.Entities;
using NitroxModel_Subnautica.Helper;
using NitroxServer;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Resources;
using NitroxServer.Serialization;
using NitroxServer_Subnautica.GameLogic;
using NitroxServer_Subnautica.GameLogic.Entities;
using NitroxServer_Subnautica.GameLogic.Entities.Spawning;
using NitroxServer_Subnautica.GameLogic.Entities.Spawning.EntityBootstrappers;
using NitroxServer_Subnautica.Resources;
using NitroxServer_Subnautica.Serialization;

Expand Down Expand Up @@ -62,6 +59,9 @@ public override void RegisterDependencies(ContainerBuilder containerBuilder)
containerBuilder.RegisterType<EntityRegistry>().AsSelf().InstancePerLifetimeScope();
containerBuilder.RegisterType<SubnauticaWorldModifier>().As<IWorldModifier>().InstancePerLifetimeScope();
containerBuilder.Register(c => new FMODWhitelist(GameInfo.Subnautica)).InstancePerLifetimeScope();

containerBuilder.Register(_ => new RandomSpawnSpoofer(resourceAssets.RandomPossibilitiesByClassId))
.SingleInstance();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class BatchEntitySpawner : IEntitySpawner

private readonly HashSet<NitroxInt3> emptyBatches = new();
private readonly Dictionary<string, PrefabPlaceholdersGroupAsset> placeholdersGroupsByClassId;
private readonly RandomSpawnSpoofer randomSpawnSpoofer;
private readonly IUwePrefabFactory prefabFactory;
private readonly IEntityBootstrapperManager entityBootstrapperManager;
private readonly PDAStateData pdaStateData;
Expand Down Expand Up @@ -60,7 +61,7 @@ public List<NitroxInt3> SerializableParsedBatches
private static readonly NitroxQuaternion prefabZUpRotation = NitroxQuaternion.FromEuler(new(-90f, 0f, 0f));

public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, IUweWorldEntityFactory worldEntityFactory, IUwePrefabFactory prefabFactory, List<NitroxInt3> loadedPreviousParsed, ServerProtoBufSerializer serializer,
IEntityBootstrapperManager entityBootstrapperManager, Dictionary<string, PrefabPlaceholdersGroupAsset> placeholdersGroupsByClassId, PDAStateData pdaStateData, string seed)
IEntityBootstrapperManager entityBootstrapperManager, Dictionary<string, PrefabPlaceholdersGroupAsset> placeholdersGroupsByClassId, PDAStateData pdaStateData, RandomSpawnSpoofer randomSpawnSpoofer, string seed)
{
parsedBatches = new HashSet<NitroxInt3>(loadedPreviousParsed);
this.worldEntityFactory = worldEntityFactory;
Expand All @@ -69,6 +70,7 @@ public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, IUweW
this.placeholdersGroupsByClassId = placeholdersGroupsByClassId;
this.pdaStateData = pdaStateData;
batchCellsParser = new BatchCellsParser(entitySpawnPointFactory, serializer);
this.randomSpawnSpoofer = randomSpawnSpoofer;
this.seed = seed;
}

Expand Down Expand Up @@ -279,6 +281,7 @@ private IEnumerable<Entity> CreateEntityWithChildren(EntitySpawnPoint entitySpaw
}
else
{
randomSpawnSpoofer.PickRandomClassIdIfRequired(ref classId);
spawnedEntity = new WorldEntity(position,
rotation,
localScale,
Expand Down
23 changes: 23 additions & 0 deletions NitroxServer/Resources/RandomSpawnSpoofer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using NitroxServer.Helper;

namespace NitroxServer.Resources;

public class RandomSpawnSpoofer
{
private readonly Dictionary<string, string[]> randomPossibilitiesByClassId;

public RandomSpawnSpoofer(Dictionary<string, string[]> randomPossibilitiesByClassId)
{
this.randomPossibilitiesByClassId = randomPossibilitiesByClassId;
}

public void PickRandomClassIdIfRequired(ref string classId)
{
if (randomPossibilitiesByClassId.TryGetValue(classId, out string[] choices))
{
int randomIndex = XORRandom.NextIntRange(0, choices.Length);
classId = choices[randomIndex];
}
}
}
Loading

0 comments on commit c529216

Please sign in to comment.