From 61f2a7c167fe82ea128f30715289edaf98c070ba Mon Sep 17 00:00:00 2001 From: tornac1234 <24827220+tornac1234@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:42:25 +0100 Subject: [PATCH] Prevent entity spawning from stopping when it errors. Implement randomly-generated-on-the-fly entities --- NitroxClient/GameLogic/Entities.cs | 15 ++++- .../PlaceholderGroupWorldEntitySpawner.cs | 32 +++++---- .../PrefabPlaceholderEntitySpawner.cs | 67 +++++++++++++++++++ .../SerializedWorldEntitySpawner.cs | 2 +- .../WorldEntitySpawnerResolver.cs | 10 ++- .../GameLogic/Spawning/WorldEntitySpawner.cs | 19 +++++- .../Entities/SubnauticaUwePrefabFactory.cs | 4 +- .../SubnauticaUweWorldEntityFactory.cs | 4 +- .../GameLogic/Entities/UwePrefab.cs | 8 +-- .../GameLogic/Entities/UwePrefabFactory.cs | 11 ++- .../Entities/UweWorldEntityFactory.cs | 9 ++- .../GameLogic/Entities/WorldEntity.cs | 1 + .../DataStructures/GameLogic/Entity.cs | 13 ++-- .../Parsers/PrefabPlaceholderGroupsParser.cs | 38 +++++++++++ .../SubnauticaServerAutoFacRegistrar.cs | 4 +- .../Entities/Spawning/BatchEntitySpawner.cs | 29 +++++--- .../Resources/PrefabPlaceholderAsset.cs | 6 +- .../Resources/PrefabPlaceholderRandomAsset.cs | 9 +++ .../Resources/PrefabPlaceholdersGroupAsset.cs | 13 ++-- .../Serialization/World/WorldPersistence.cs | 4 +- 20 files changed, 219 insertions(+), 79 deletions(-) create mode 100644 NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs create mode 100644 NitroxServer/Resources/PrefabPlaceholderRandomAsset.cs diff --git a/NitroxClient/GameLogic/Entities.cs b/NitroxClient/GameLogic/Entities.cs index fc1f0e9269..c09a75dbd7 100644 --- a/NitroxClient/GameLogic/Entities.cs +++ b/NitroxClient/GameLogic/Entities.cs @@ -51,6 +51,7 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(); entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(playerManager, localPlayer, this); entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; + entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(EscapePodWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(PlayerWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(VehicleWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; @@ -99,8 +100,18 @@ public void BroadcastEntitySpawnedByClient(WorldEntity entity) private IEnumerator SpawnNewEntities() { - yield return SpawnBatchAsync(EntitiesToSpawn).OnYieldError(Log.Error); - spawningEntities = false; + bool restarted = false; + yield return SpawnBatchAsync(EntitiesToSpawn).OnYieldError(exception => + { + Log.Error(exception); + if (EntitiesToSpawn.Count > 0) + { + restarted = true; + // It's safe to run a new time because the processed entity is removed first so it won't infinitely throw errors + CoroutineHost.StartCoroutine(SpawnNewEntities()); + } + }); + spawningEntities = restarted; } public void EnqueueEntitiesToSpawn(List entitiesToEnqueue) diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs index e6ecdb842c..de92dc139a 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Collections.Generic; using NitroxClient.GameLogic.Spawning.Metadata; -using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; @@ -20,12 +19,14 @@ public class PlaceholderGroupWorldEntitySpawner : IWorldEntitySpawner private readonly Entities entities; private readonly WorldEntitySpawnerResolver spawnerResolver; private readonly DefaultWorldEntitySpawner defaultSpawner; + private readonly PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner; - public PlaceholderGroupWorldEntitySpawner(Entities entities, WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner) + public PlaceholderGroupWorldEntitySpawner(Entities entities, WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner, PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner) { this.entities = entities; this.spawnerResolver = spawnerResolver; this.defaultSpawner = defaultSpawner; + this.prefabPlaceholderEntitySpawner = prefabPlaceholderEntitySpawner; } public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) @@ -67,19 +68,22 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, E switch (current) { case PrefabPlaceholderEntity prefabEntity: - PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[prefabEntity.ComponentIndex]; - if (!SpawnWorldEntityChildSync(prefabEntity, cellRoot, placeholder.transform.parent.gameObject, childResult, out IEnumerator asyncInstructions)) + if (!prefabPlaceholderEntitySpawner.SpawnSync(prefabEntity, groupObject, cellRoot, childResult)) { - yield return asyncInstructions; + yield return prefabPlaceholderEntitySpawner.SpawnAsync(prefabEntity, groupObject, cellRoot, childResult); } break; case PlaceholderGroupWorldEntity groupEntity: - placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex]; + PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex]; yield return SpawnAsync(groupEntity, placeholder.transform.parent.gameObject, cellRoot, childResult); break; + default: - Log.Error($"[{nameof(PlaceholderGroupWorldEntitySpawner)}] Found a child entity to spawn with an unmanaged type: {entity.GetType()}"); + if (!SpawnWorldEntityChildSync(entity, cellRoot, parentById.GetOrDefault(current.ParentId, null), childResult, out IEnumerator asyncInstructions)) + { + yield return asyncInstructions; + } break; } @@ -93,15 +97,15 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, E entities.MarkAsSpawned(current); parentById[current.Id] = childObject; EntityMetadataProcessor.ApplyMetadata(childObject, current.Metadata); + + // PlaceholderGroupWorldEntity's children spawning is already handled by this function which is called recursively if (current is not PlaceholderGroupWorldEntity) { - continue; - } - NitroxEntity.SetNewId(childObject, current.Id); - // Adding children to be spawned by this loop - foreach (Entity slotEntityChild in current.ChildEntities) - { - stack.Push(slotEntityChild); + // Adding children to be spawned by this loop + foreach (Entity slotEntityChild in current.ChildEntities) + { + stack.Push(slotEntityChild); + } } } diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs new file mode 100644 index 0000000000..79f2c33b0d --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs @@ -0,0 +1,67 @@ +using System.Collections; +using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.Util; +using NitroxModel_Subnautica.DataStructures; +using UnityEngine; + +namespace NitroxClient.GameLogic.Spawning.WorldEntities; + +public class PrefabPlaceholderEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner +{ + private readonly DefaultWorldEntitySpawner defaultEntitySpawner; + + public PrefabPlaceholderEntitySpawner(DefaultWorldEntitySpawner defaultEntitySpawner) + { + this.defaultEntitySpawner = defaultEntitySpawner; + } + + // TODO: Clean the spawners (move to to Setup() and IsValidOrError() after rebase) + public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + if (entity is not PrefabPlaceholderEntity prefabEntity) + { + yield break; + } + if (!parent.Value || !parent.Value.TryGetComponent(out PrefabPlaceholdersGroup group)) + { + Log.Error($"[{nameof(PrefabPlaceholderEntity)}] Can't find a {nameof(PrefabPlaceholdersGroup)} on parent for {entity.Id}"); + yield break; + } + PrefabPlaceholder placeholder = group.prefabPlaceholders[prefabEntity.ComponentIndex]; + yield return defaultEntitySpawner.SpawnAsync(entity, placeholder.transform.parent.gameObject, cellRoot, result); + if (!result.value.HasValue) + { + yield break; + } + SetupObject(entity, result.value.Value); + } + + public bool SpawnsOwnChildren() => false; + + public bool SpawnSync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + if (entity is not PrefabPlaceholderEntity prefabEntity) + { + return true; + } + if (!parent.Value || !parent.Value.TryGetComponent(out PrefabPlaceholdersGroup group)) + { + Log.Error($"[{nameof(PrefabPlaceholderEntity)}] Can't find a {nameof(PrefabPlaceholdersGroup)} on parent for {entity.Id}"); + return true; + } + PrefabPlaceholder placeholder = group.prefabPlaceholders[prefabEntity.ComponentIndex]; + if (!defaultEntitySpawner.SpawnSync(entity, placeholder.transform.parent.gameObject, cellRoot, result)) + { + return false; + } + SetupObject(entity, result.value.Value); + return true; + } + + private void SetupObject(WorldEntity entity, GameObject gameObject) + { + gameObject.transform.localPosition = entity.Transform.LocalPosition.ToUnity(); + gameObject.transform.localRotation = entity.Transform.LocalRotation.ToUnity(); + gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity(); + } +} diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs index cfee0cccf2..5a86a41178 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs @@ -17,7 +17,7 @@ public class SerializedWorldEntitySpawner : IWorldEntitySpawner, IWorldEntitySyn /// /// Contains the only types we allow the server to instantiate on clients (for security concerns) /// - private readonly List typesWhitelist = new() + private readonly HashSet typesWhitelist = new() { typeof(Light), typeof(DisableBeforeExplosion), typeof(BoxCollider), typeof(SphereCollider) }; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs index 4393eb5e6b..577b7ea919 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs @@ -10,7 +10,8 @@ public class WorldEntitySpawnerResolver private readonly DefaultWorldEntitySpawner defaultEntitySpawner = new(); private readonly VehicleWorldEntitySpawner vehicleWorldEntitySpawner; - private readonly PlaceholderGroupWorldEntitySpawner prefabWorldEntitySpawner; + private readonly PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner; + private readonly PlaceholderGroupWorldEntitySpawner placeholderGroupWorldEntitySpawner; private readonly PlayerWorldEntitySpawner playerWorldEntitySpawner; private readonly SerializedWorldEntitySpawner serializedWorldEntitySpawner; @@ -23,7 +24,8 @@ public WorldEntitySpawnerResolver(PlayerManager playerManager, ILocalNitroxPlaye customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner(); vehicleWorldEntitySpawner = new(entities); - prefabWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(entities, this, defaultEntitySpawner); + prefabPlaceholderEntitySpawner = new(defaultEntitySpawner); + placeholderGroupWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(entities, this, defaultEntitySpawner, prefabPlaceholderEntitySpawner); playerWorldEntitySpawner = new PlayerWorldEntitySpawner(playerManager, localPlayer); serializedWorldEntitySpawner = new SerializedWorldEntitySpawner(); } @@ -32,8 +34,10 @@ public IWorldEntitySpawner ResolveEntitySpawner(WorldEntity entity) { switch (entity) { + case PrefabPlaceholderEntity: + return prefabPlaceholderEntitySpawner; case PlaceholderGroupWorldEntity: - return prefabWorldEntitySpawner; + return placeholderGroupWorldEntitySpawner; case PlayerWorldEntity: return playerWorldEntitySpawner; case VehicleWorldEntity: diff --git a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs index e7c32723a9..5ab1500f81 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs @@ -13,7 +13,7 @@ namespace NitroxClient.GameLogic.Spawning; -public class WorldEntitySpawner : EntitySpawner +public class WorldEntitySpawner : SyncEntitySpawner { private readonly WorldEntitySpawnerResolver worldEntitySpawnResolver; private readonly Dictionary batchCellsById; @@ -30,8 +30,6 @@ public WorldEntitySpawner(PlayerManager playerManager, ILocalNitroxPlayer localP protected override IEnumerator SpawnAsync(WorldEntity entity, TaskResult> result) { - LargeWorldStreamer.main.cellManager.UnloadBatchCells(entity.AbsoluteEntityCell.CellId.ToUnity()); // Just in case - EntityCell cellRoot = EnsureCell(entity); if (cellRoot == null) { @@ -58,6 +56,21 @@ protected override bool SpawnsOwnChildren(WorldEntity entity) return entitySpawner.SpawnsOwnChildren(); } + protected override bool SpawnSync(WorldEntity entity, TaskResult> result) + { + EntityCell cellRoot = EnsureCell(entity); + if (cellRoot == null) + { + // Error logging is done in EnsureCell + return true; + } + + Optional parent = (entity.ParentId != null) ? NitroxEntity.GetObjectFrom(entity.ParentId) : Optional.Empty; + IWorldEntitySpawner entitySpawner = worldEntitySpawnResolver.ResolveEntitySpawner(entity); + + return entitySpawner is IWorldEntitySyncSpawner syncSpawner && syncSpawner.SpawnSync(entity, parent, cellRoot, result); + } + private EntityCell EnsureCell(WorldEntity entity) { EntityCell entityCell; diff --git a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs index 1bb4db5bb7..81c5613c09 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs @@ -8,7 +8,7 @@ namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities; -public class SubnauticaUwePrefabFactory : UwePrefabFactory +public class SubnauticaUwePrefabFactory : IUwePrefabFactory { private readonly LootDistributionData lootDistributionData; private readonly Dictionary> cache = new(); @@ -18,7 +18,7 @@ public SubnauticaUwePrefabFactory(string lootDistributionJson) lootDistributionData = GetLootDistributionData(lootDistributionJson); } - public override List GetPossiblePrefabs(string biome) + public List GetPossiblePrefabs(string biome) { List prefabs = new(); if (biome == null) diff --git a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs index 3b8d6e37b5..9c2de05b8a 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs @@ -4,7 +4,7 @@ namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities; -public class SubnauticaUweWorldEntityFactory : UweWorldEntityFactory +public class SubnauticaUweWorldEntityFactory : IUweWorldEntityFactory { private readonly Dictionary worldEntitiesByClassId; @@ -13,7 +13,7 @@ public SubnauticaUweWorldEntityFactory(Dictionary world this.worldEntitiesByClassId = worldEntitiesByClassId; } - public override bool TryFind(string classId, out UweWorldEntity uweWorldEntity) + public bool TryFind(string classId, out UweWorldEntity uweWorldEntity) { if (worldEntitiesByClassId.TryGetValue(classId, out WorldEntityInfo worldEntityInfo)) { diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs index 8cab06f3d9..ab8d278b2b 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs @@ -1,9 +1,3 @@ namespace NitroxModel.DataStructures.GameLogic.Entities; -public record struct UwePrefab(string ClassId, int Count, float Probability, bool IsFragment) -{ - public string ClassId { get; } = ClassId; - public int Count { get; } = Count; - public float Probability { get; set; } = Probability; - public bool IsFragment { get; } = IsFragment; -} +public readonly record struct UwePrefab(string ClassId, int Count, float Probability, bool IsFragment); diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefabFactory.cs b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefabFactory.cs index 4b6f5cfe30..51fd01205b 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefabFactory.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefabFactory.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace NitroxModel.DataStructures.GameLogic.Entities +namespace NitroxModel.DataStructures.GameLogic.Entities; + +public interface IUwePrefabFactory { - public abstract class UwePrefabFactory - { - public abstract List GetPossiblePrefabs(string biomeType); - } + public abstract List GetPossiblePrefabs(string biomeType); } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs b/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs index 76081a8a4e..b07a5260e7 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs @@ -1,7 +1,6 @@ -namespace NitroxModel.DataStructures.GameLogic.Entities +namespace NitroxModel.DataStructures.GameLogic.Entities; + +public interface IUweWorldEntityFactory { - public abstract class UweWorldEntityFactory - { - public abstract bool TryFind(string classId, out UweWorldEntity uweWorldEntity); - } + public abstract bool TryFind(string classId, out UweWorldEntity uweWorldEntity); } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs index 4e9cc6c3bf..cdfe79d60f 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs @@ -18,6 +18,7 @@ namespace NitroxModel.DataStructures.GameLogic.Entities [ProtoInclude(51, typeof(CellRootEntity))] [ProtoInclude(52, typeof(GlobalRootEntity))] [ProtoInclude(53, typeof(SerializedWorldEntity))] + [ProtoInclude(54, typeof(PrefabPlaceholderEntity))] public class WorldEntity : Entity { public virtual AbsoluteEntityCell AbsoluteEntityCell => new(Transform.Position, Level); diff --git a/NitroxModel/DataStructures/GameLogic/Entity.cs b/NitroxModel/DataStructures/GameLogic/Entity.cs index 1a4c1458f0..7eca20f5f7 100644 --- a/NitroxModel/DataStructures/GameLogic/Entity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entity.cs @@ -11,13 +11,12 @@ namespace NitroxModel.DataStructures.GameLogic [Serializable] [DataContract] [ProtoInclude(60, typeof(PrefabChildEntity))] - [ProtoInclude(70, typeof(PrefabPlaceholderEntity))] - [ProtoInclude(80, typeof(InventoryEntity))] - [ProtoInclude(90, typeof(InventoryItemEntity))] - [ProtoInclude(100, typeof(PathBasedChildEntity))] - [ProtoInclude(110, typeof(InstalledBatteryEntity))] - [ProtoInclude(120, typeof(InstalledModuleEntity))] - [ProtoInclude(130, typeof(WorldEntity))] + [ProtoInclude(70, typeof(InventoryEntity))] + [ProtoInclude(80, typeof(InventoryItemEntity))] + [ProtoInclude(90, typeof(PathBasedChildEntity))] + [ProtoInclude(100, typeof(InstalledBatteryEntity))] + [ProtoInclude(110, typeof(InstalledModuleEntity))] + [ProtoInclude(120, typeof(WorldEntity))] public abstract class Entity { [DataMember(Order = 1)] diff --git a/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs b/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs index c7df85185a..d936966fb0 100644 --- a/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs +++ b/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs @@ -22,6 +22,7 @@ public class PrefabPlaceholderGroupsParser : IDisposable private readonly AssetsBundleManager am; private readonly ThreadSafeMonoCecilTempGenerator monoGen; + private readonly ConcurrentDictionary classIdByRuntimeKey = new(); private readonly ConcurrentDictionary addressableCatalog = new(); private readonly ConcurrentDictionary placeholdersByClassId = new(); private readonly ConcurrentDictionary groupsByClassId = new(); @@ -84,7 +85,16 @@ private static Dictionary LoadPrefabDatabase(string fullFilename private void LoadAddressableCatalog(Dictionary prefabDatabase) { ContentCatalogData ccd = AddressablesJsonParser.FromString(File.ReadAllText(Path.Combine(aaRootPath, "catalog.json"))); + Dictionary classIdByPath = prefabDatabase.ToDictionary(m => m.Value, m => m.Key); + foreach (KeyValuePair> 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 prefabAddressable in prefabDatabase) { foreach (ResourceLocation resourceLocation in ccd.Resources[prefabAddressable.Value]) @@ -268,6 +278,34 @@ private IPrefabAsset GetAndCacheAsset(AssetsBundleManager amInst, string classId return groupAsset; } + AssetFileInfo spawnRandomInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom"); + if (spawnRandomInfo != null) + { + // See SpawnRandom.Start + AssetTypeValueField spawnRandom = amInst.GetBaseField(assetFileInst, spawnRandomInfo); + List 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) diff --git a/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs b/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs index 33c697748e..ee026c7260 100644 --- a/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs +++ b/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs @@ -44,10 +44,10 @@ public override void RegisterDependencies(ContainerBuilder containerBuilder) containerBuilder.Register(c => resourceAssets.WorldEntitiesByClassId).SingleInstance(); containerBuilder.Register(c => resourceAssets.PrefabPlaceholdersGroupsByGroupClassId).SingleInstance(); containerBuilder.Register(c => resourceAssets.NitroxRandom).SingleInstance(); - containerBuilder.RegisterType().As().SingleInstance(); + containerBuilder.RegisterType().As().SingleInstance(); SubnauticaUwePrefabFactory prefabFactory = new SubnauticaUwePrefabFactory(resourceAssets.LootDistributionsJson); - containerBuilder.Register(c => prefabFactory).As().SingleInstance(); + containerBuilder.Register(c => prefabFactory).As().SingleInstance(); containerBuilder.Register(c => new Dictionary { [TechType.CrashHome.ToDto()] = new CrashFishBootstrapper(), diff --git a/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs b/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs index 158e64fd50..071c517a9c 100644 --- a/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs +++ b/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs @@ -18,12 +18,12 @@ public class BatchEntitySpawner : IEntitySpawner private readonly Dictionary customBootstrappersByTechType; private readonly HashSet emptyBatches = new(); private readonly Dictionary placeholdersGroupsByClassId; - private readonly UwePrefabFactory prefabFactory; + private readonly IUwePrefabFactory prefabFactory; private readonly PDAStateData pdaStateData; private readonly string seed; - private readonly UweWorldEntityFactory worldEntityFactory; + private readonly IUweWorldEntityFactory worldEntityFactory; private readonly object parsedBatchesLock = new object(); private readonly object emptyBatchesLock = new object(); @@ -59,7 +59,7 @@ public List SerializableParsedBatches private static readonly NitroxQuaternion prefabZUpRotation = NitroxQuaternion.FromEuler(new(-90f, 0f, 0f)); - public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, UweWorldEntityFactory worldEntityFactory, UwePrefabFactory prefabFactory, List loadedPreviousParsed, ServerProtoBufSerializer serializer, + public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, IUweWorldEntityFactory worldEntityFactory, IUwePrefabFactory prefabFactory, List loadedPreviousParsed, ServerProtoBufSerializer serializer, Dictionary customBootstrappersByTechType, Dictionary placeholdersGroupsByClassId, PDAStateData pdaStateData, string seed) { parsedBatches = new HashSet(loadedPreviousParsed); @@ -136,7 +136,7 @@ private IEnumerable SpawnEntitiesUsingRandomDistribution(EntitySpawnPoin UwePrefab prefab = allowedPrefabs[i]; if (areFragmentProbabilitiesNonNull && prefab.IsFragment) { - prefab.Probability *= probabilityMultiplier; + prefab = prefab with { Probability = prefab.Probability * probabilityMultiplier }; allowedPrefabs[i] = prefab; } weightedFragmentProbability += prefab.Probability; @@ -217,7 +217,7 @@ private List FilterAllowedPrefabs(List prefabs, EntitySpaw fragmentProbability += weightedProbability; } } - prefab.Probability = weightedProbability; + prefab = prefab with { Probability = weightedProbability }; allowedPrefabs.Add(prefab); } } @@ -378,10 +378,10 @@ private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity entity = new PlaceholderGroupWorldEntity(entity); // Adapted from PrefabPlaceholdersGroup.Spawn - for (int i = 0; i < groupAsset.PrefabPlaceholders.Length; i++) + for (int i = 0; i < groupAsset.PrefabAssets.Length; i++) { // Fix positioning of children - IPrefabAsset prefabAsset = groupAsset.PrefabPlaceholders[i]; + IPrefabAsset prefabAsset = groupAsset.PrefabAssets[i]; // Two cases, either the PrefabPlaceholder holds a visible GameObject or an EntitySlot (a MB which has a chance of spawning a prefab) if (prefabAsset is PrefabPlaceholderAsset placeholderAsset && placeholderAsset.EntitySlot != null) @@ -405,15 +405,22 @@ private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity else { // Regular visible GameObject - EntitySpawnPoint esp = new(entity.AbsoluteEntityCell, prefabAsset.Transform.LocalPosition, prefabAsset.Transform.LocalRotation, prefabAsset.Transform.LocalScale, prefabAsset.ClassId); + string prefabClassId = prefabAsset.ClassId; + if (prefabAsset is PrefabPlaceholderRandomAsset randomAsset && randomAsset.ClassIds.Count > 0) + { + int randomIndex = XORRandom.NextIntRange(0, randomAsset.ClassIds.Count); + prefabClassId = randomAsset.ClassIds[randomIndex]; + } + + EntitySpawnPoint esp = new(entity.AbsoluteEntityCell, prefabAsset.Transform.LocalPosition, prefabAsset.Transform.LocalRotation, prefabAsset.Transform.LocalScale, prefabClassId); WorldEntity spawnedEntity = (WorldEntity)SpawnEntitiesStaticly(esp, deterministicBatchGenerator, entity).First(); - if (prefabAsset is PrefabPlaceholderAsset) + if (prefabAsset is PrefabPlaceholdersGroupAsset) { - spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i); + spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i); } else { - spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i); + spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i); } entity.ChildEntities.Add(spawnedEntity); diff --git a/NitroxServer/Resources/PrefabPlaceholderAsset.cs b/NitroxServer/Resources/PrefabPlaceholderAsset.cs index d389d95566..88c2186731 100644 --- a/NitroxServer/Resources/PrefabPlaceholderAsset.cs +++ b/NitroxServer/Resources/PrefabPlaceholderAsset.cs @@ -3,11 +3,9 @@ namespace NitroxServer.Resources; -public record struct PrefabPlaceholderAsset(string ClassId, NitroxEntitySlot EntitySlot) : IPrefabAsset +public record struct PrefabPlaceholderAsset(string ClassId, NitroxEntitySlot EntitySlot = null, NitroxTransform Transform = null) : IPrefabAsset { - public NitroxTransform Transform { get; set; } - public string ClassId { get; } = ClassId; - + public NitroxTransform Transform { get; set; } = Transform; /// /// Some PrefabPlaceholders spawn GameObjects that are always there (decor, environment ...) /// And some others spawn a GameObject with an EntitySlot in which case this field is not null. diff --git a/NitroxServer/Resources/PrefabPlaceholderRandomAsset.cs b/NitroxServer/Resources/PrefabPlaceholderRandomAsset.cs new file mode 100644 index 0000000000..3d86d8598f --- /dev/null +++ b/NitroxServer/Resources/PrefabPlaceholderRandomAsset.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using NitroxModel.DataStructures.Unity; + +namespace NitroxServer.Resources; + +public record struct PrefabPlaceholderRandomAsset(List ClassIds, NitroxTransform Transform = null, string ClassId = null) : IPrefabAsset +{ + public NitroxTransform Transform { get; set; } = Transform; +} diff --git a/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs b/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs index b61ff48e5e..2da374e8b3 100644 --- a/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs +++ b/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs @@ -2,13 +2,10 @@ namespace NitroxServer.Resources; -public record struct PrefabPlaceholdersGroupAsset(string ClassId, IPrefabAsset[] PrefabAssets) : IPrefabAsset +/// +/// All attached PrefabPlaceholders (and PrefabPlaceholdersGroup). Is in sync with PrefabPlaceholdersGroup.prefabPlaceholders +/// +public record struct PrefabPlaceholdersGroupAsset(string ClassId, IPrefabAsset[] PrefabAssets, NitroxTransform Transform = null) : IPrefabAsset { - public NitroxTransform Transform { get; set; } - public string ClassId { get; } = ClassId; - - /// - /// All attached PrefabPlaceholders (and PrefabPlaceholdersGroup). Is in sync with PrefabPlaceholdersGroup.prefabPlaceholders - /// - public IPrefabAsset[] PrefabPlaceholders { get; } = PrefabAssets; + public NitroxTransform Transform { get; set; } = Transform; } diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index bf826e8e42..ea32cd8cbf 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -223,8 +223,8 @@ public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode) world.BatchEntitySpawner = new BatchEntitySpawner( NitroxServiceLocator.LocateService(), - NitroxServiceLocator.LocateService(), - NitroxServiceLocator.LocateService(), + NitroxServiceLocator.LocateService(), + NitroxServiceLocator.LocateService(), pWorldData.WorldData.ParsedBatchCells, protoBufSerializer, NitroxServiceLocator.LocateService>(),