diff --git a/Nitrox.Test/Server/Helper/XORRandomTest.cs b/Nitrox.Test/Server/Helper/XORRandomTest.cs new file mode 100644 index 0000000000..9d0ec98403 --- /dev/null +++ b/Nitrox.Test/Server/Helper/XORRandomTest.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxServer.Helper; + +namespace Nitrox.Test.Server.Helper; + +[TestClass] +public class XORRandomTest +{ + [TestMethod] + public void TestMeanGeneration() + { + // arbitrary values under there but we can't compare the generated values with UnityEngine.Random because it's unaccessible + XORRandom.InitSeed("cheescake".GetHashCode()); + float mean = 0; + int count = 1000000; + for (int i = 0; i < count; i++) + { + mean += XORRandom.NextFloat(); + } + mean /= count; + Assert.IsTrue(Math.Abs(0.5f - mean) < 0.001f, $"Float number generation isn't uniform enough: {mean}"); + } +} diff --git a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs index 094cf8bd11..7a4f0867dc 100644 --- a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs @@ -17,6 +17,7 @@ using NitroxModel.DataStructures.GameLogic.Entities.Bases; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases; +using NitroxModel.DataStructures; namespace NitroxServer.Serialization; @@ -343,7 +344,8 @@ private static void EntityTest(Entity entity, Entity entityAfter) { switch (worldEntity) { - case PlaceholderGroupWorldEntity _ when worldEntityAfter is PlaceholderGroupWorldEntity _: + case PlaceholderGroupWorldEntity placeholderGroupWorldEntity when worldEntityAfter is PlaceholderGroupWorldEntity placeholderGroupWorldEntityAfter: + Assert.AreEqual(placeholderGroupWorldEntity.ComponentIndex, placeholderGroupWorldEntityAfter.ComponentIndex); break; case CellRootEntity _ when worldEntityAfter is CellRootEntity _: break; @@ -354,6 +356,16 @@ private static void EntityTest(Entity entity, Entity entityAfter) Assert.AreEqual(oxygenPipeEntity.RootPipeId, oxygenPipeEntityAfter.RootPipeId); Assert.AreEqual(oxygenPipeEntity.ParentPosition, oxygenPipeEntityAfter.ParentPosition); break; + case PrefabPlaceholderEntity prefabPlaceholderEntity when entityAfter is PrefabPlaceholderEntity prefabPlaceholderEntityAfter: + Assert.AreEqual(prefabPlaceholderEntity.ComponentIndex, prefabPlaceholderEntityAfter.ComponentIndex); + break; + case SerializedWorldEntity serializedWorldEntity when entityAfter is SerializedWorldEntity serializedWorldEntityAfter: + Assert.AreEqual(serializedWorldEntity.AbsoluteEntityCell, serializedWorldEntityAfter.AbsoluteEntityCell); + AssertHelper.IsListEqual(serializedWorldEntity.Components.OrderBy(c => c.GetHashCode()), serializedWorldEntityAfter.Components.OrderBy(c => c.GetHashCode()), (SerializedComponent c1, SerializedComponent c2) => c1.Equals(c2)); + Assert.AreEqual(serializedWorldEntity.Layer, serializedWorldEntityAfter.Layer); + Assert.AreEqual(serializedWorldEntity.BatchId, serializedWorldEntityAfter.BatchId); + Assert.AreEqual(serializedWorldEntity.CellId, serializedWorldEntityAfter.CellId); + break; case GlobalRootEntity globalRootEntity when worldEntityAfter is GlobalRootEntity globalRootEntityAfter: if (globalRootEntity.GetType() != typeof(GlobalRootEntity)) { @@ -419,9 +431,6 @@ private static void EntityTest(Entity entity, Entity entityAfter) Assert.AreEqual(prefabChildEntity.ComponentIndex, prefabChildEntityAfter.ComponentIndex); Assert.AreEqual(prefabChildEntity.ClassId, prefabChildEntityAfter.ClassId); break; - case PrefabPlaceholderEntity prefabPlaceholderEntity when entityAfter is PrefabPlaceholderEntity prefabPlaceholderEntityAfter: - Assert.AreEqual(prefabPlaceholderEntity.ClassId, prefabPlaceholderEntityAfter.ClassId); - break; case InventoryEntity inventoryEntity when entityAfter is InventoryEntity inventoryEntityAfter: Assert.AreEqual(inventoryEntity.ComponentIndex, inventoryEntityAfter.ComponentIndex); break; diff --git a/NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs b/NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs index 0a684128d5..56fc9c4d77 100644 --- a/NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs +++ b/NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs @@ -93,6 +93,12 @@ private void DrawTransform(Transform transform) GameObject.Destroy(transform.gameObject); } } + if (GUILayout.Button("Goto", GUILayout.MaxWidth(75)) && Player.main) + { + SubRoot subRoot = transform.GetComponentInParent(true); + Player.main.SetCurrentSub(subRoot, true); + Player.main.SetPosition(transform.position); + } } } } diff --git a/NitroxClient/GameLogic/Entities.cs b/NitroxClient/GameLogic/Entities.cs index ddbd6627bf..ac9c6b718f 100644 --- a/NitroxClient/GameLogic/Entities.cs +++ b/NitroxClient/GameLogic/Entities.cs @@ -54,9 +54,11 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(); entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, 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)]; + entitySpawnersByType[typeof(SerializedWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(GlobalRootEntity)] = new GlobalRootEntitySpawner(); entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this); entitySpawnersByType[typeof(ModuleEntity)] = new ModuleEntitySpawner(this); @@ -108,8 +110,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 4c9a7d3b43..4551489b81 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs @@ -1,7 +1,5 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using NitroxClient.GameLogic.Spawning.Metadata; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; @@ -18,19 +16,28 @@ namespace NitroxClient.GameLogic.Spawning.WorldEntities; /// public class PlaceholderGroupWorldEntitySpawner : IWorldEntitySpawner { + private readonly Entities entities; private readonly WorldEntitySpawnerResolver spawnerResolver; private readonly DefaultWorldEntitySpawner defaultSpawner; private readonly EntityMetadataManager entityMetadataManager; + private readonly PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner; - public PlaceholderGroupWorldEntitySpawner(WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner, EntityMetadataManager entityMetadataManager) + public PlaceholderGroupWorldEntitySpawner(Entities entities, WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner, EntityMetadataManager entityMetadataManager, PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner) { + this.entities = entities; this.spawnerResolver = spawnerResolver; this.defaultSpawner = defaultSpawner; this.entityMetadataManager = entityMetadataManager; + this.prefabPlaceholderEntitySpawner = prefabPlaceholderEntitySpawner; } public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) { + if (entity is not PlaceholderGroupWorldEntity placeholderGroupEntity) + { + yield break; + } + TaskResult> prefabPlaceholderGroupTaskResult = new(); if (!defaultSpawner.SpawnSync(entity, parent, cellRoot, prefabPlaceholderGroupTaskResult)) { @@ -41,73 +48,49 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, E if (!prefabPlaceholderGroupGameObject.HasValue) { - result.Set(Optional.Empty); - yield break; - } - - if (entity is not PlaceholderGroupWorldEntity placeholderGroupEntity) - { - result.Set(Optional.Empty); yield break; } - result.Set(prefabPlaceholderGroupGameObject); - + GameObject groupObject = prefabPlaceholderGroupGameObject.Value; // Spawning PrefabPlaceholders as siblings to the group - PrefabPlaceholdersGroup prefabPlaceholderGroup = prefabPlaceholderGroupGameObject.Value.GetComponent(); + PrefabPlaceholdersGroup prefabPlaceholderGroup = groupObject.GetComponent(); // Spawning all children iteratively - Stack stack = new(placeholderGroupEntity.ChildEntities.OfType()); + Stack stack = new(placeholderGroupEntity.ChildEntities); TaskResult> childResult = new(); - Dictionary> parentById = new(); - IEnumerator asyncInstructions; + Dictionary parentById = new() + { + { entity.Id, groupObject } + }; while (stack.Count > 0) { childResult.Set(Optional.Empty); Entity current = stack.Pop(); switch (current) { - // First layer of children under PlaceholderGroupWorldEntity - case PrefabChildEntity placeholderSlot: - // Entity was a slot not spawned, picked up, or removed - if (placeholderSlot.ChildEntities.Count == 0) + case PrefabPlaceholderEntity prefabEntity: + if (!prefabPlaceholderEntitySpawner.SpawnSync(prefabEntity, groupObject, cellRoot, childResult)) { - continue; + yield return prefabPlaceholderEntitySpawner.SpawnAsync(prefabEntity, groupObject, cellRoot, childResult); } + break; - PrefabPlaceholder prefabPlaceholder = prefabPlaceholderGroup.prefabPlaceholders[placeholderSlot.ComponentIndex]; - Entity slotEntity = placeholderSlot.ChildEntities[0]; - - switch (slotEntity) - { - case PrefabPlaceholderEntity placeholder: - if (!SpawnChildPlaceholderSync(prefabPlaceholder, placeholder, childResult, out asyncInstructions)) - { - yield return asyncInstructions; - } - break; - case WorldEntity worldEntity: - if (!SpawnWorldEntityChildSync(worldEntity, cellRoot, Optional.Of(prefabPlaceholder.transform.parent.gameObject), childResult, out asyncInstructions)) - { - yield return asyncInstructions; - } - break; - default: - Log.Debug(placeholderSlot.ChildEntities.Count > 0 ? $"Unhandled child type {placeholderSlot.ChildEntities[0]}" : "Child was null"); - break; - } + case PlaceholderGroupWorldEntity groupEntity: + PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex]; + yield return SpawnAsync(groupEntity, placeholder.transform.parent.gameObject, cellRoot, childResult); break; - // Other layers under PlaceholderGroupWorldEntity's children case WorldEntity worldEntity: - Optional slotParent = parentById[worldEntity.ParentId]; - - if (!SpawnWorldEntityChildSync(worldEntity, cellRoot, slotParent, childResult, out asyncInstructions)) + if (!SpawnWorldEntityChildSync(worldEntity, cellRoot, parentById.GetOrDefault(current.ParentId, null), childResult, out IEnumerator asyncInstructions)) { yield return asyncInstructions; } break; + + default: + Log.Error($"[{nameof(PlaceholderGroupWorldEntitySpawner)}] Can't spawn a child entity which is not a WorldEntity: {current}"); + continue; } if (!childResult.value.HasValue) @@ -116,72 +99,43 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, E continue; } - entityMetadataManager.ApplyMetadata(childResult.value.Value, current.Metadata); - // Adding children to be spawned by this loop - foreach (WorldEntity slotEntityChild in current.ChildEntities.OfType()) + GameObject childObject = childResult.value.Value; + entities.MarkAsSpawned(current); + parentById[current.Id] = childObject; + entityMetadataManager.ApplyMetadata(childObject, current.Metadata); + + // PlaceholderGroupWorldEntity's children spawning is already handled by this function which is called recursively + if (current is not PlaceholderGroupWorldEntity) { - stack.Push(slotEntityChild); + // Adding children to be spawned by this loop + foreach (Entity slotEntityChild in current.ChildEntities) + { + stack.Push(slotEntityChild); + } } - parentById[current.Id] = childResult.value; } - } - public bool SpawnsOwnChildren() => true; - - private IEnumerator SpawnChildPlaceholderAsync(PrefabPlaceholder prefabPlaceholder, PrefabPlaceholderEntity entity, TaskResult> result) - { - TaskResult goResult = new(); - yield return DefaultWorldEntitySpawner.CreateGameObject(TechType.None, prefabPlaceholder.prefabClassId, entity.Id, goResult); - - if (goResult.value) - { - SetupPlaceholder(goResult.value, prefabPlaceholder, result); - } - } - - private bool SpawnChildPlaceholderSync(PrefabPlaceholder prefabPlaceholder, PrefabPlaceholderEntity entity, TaskResult> result, out IEnumerator asyncInstructions) - { - if (!DefaultWorldEntitySpawner.TryCreateGameObjectSync(TechType.None, prefabPlaceholder.prefabClassId, entity.Id, out GameObject gameObject)) - { - asyncInstructions = SpawnChildPlaceholderAsync(prefabPlaceholder, entity, result); - return false; - } - - SetupPlaceholder(gameObject, prefabPlaceholder, result); - asyncInstructions = null; - return true; + result.Set(prefabPlaceholderGroupGameObject); } - private void SetupPlaceholder(GameObject gameObject, PrefabPlaceholder prefabPlaceholder, TaskResult> result) - { - try - { - gameObject.transform.SetParent(prefabPlaceholder.transform.parent, false); - gameObject.transform.localPosition = prefabPlaceholder.transform.localPosition; - gameObject.transform.localRotation = prefabPlaceholder.transform.localRotation; - - result.Set(gameObject); - } - catch (Exception e) - { - Log.Error(e); - result.Set(Optional.Empty); - } - } + public bool SpawnsOwnChildren() => true; - private IEnumerator SpawnWorldEntityChildAsync(WorldEntity worldEntity, EntityCell cellRoot, Optional parent, TaskResult> worldEntityResult) + private IEnumerator SpawnWorldEntityChildAsync(WorldEntity worldEntity, EntityCell cellRoot, GameObject parent, TaskResult> worldEntityResult) { IWorldEntitySpawner spawner = spawnerResolver.ResolveEntitySpawner(worldEntity); yield return spawner.SpawnAsync(worldEntity, parent, cellRoot, worldEntityResult); - - if (worldEntityResult.value.HasValue) + if (!worldEntityResult.value.HasValue) { - worldEntityResult.value.Value.transform.localPosition = worldEntity.Transform.LocalPosition.ToUnity(); - worldEntityResult.value.Value.transform.localRotation = worldEntity.Transform.LocalRotation.ToUnity(); + yield break; } + GameObject spawnedObject = worldEntityResult.value.Value; + + spawnedObject.transform.localPosition = worldEntity.Transform.LocalPosition.ToUnity(); + spawnedObject.transform.localRotation = worldEntity.Transform.LocalRotation.ToUnity(); + spawnedObject.transform.localScale = worldEntity.Transform.LocalScale.ToUnity(); } - private bool SpawnWorldEntityChildSync(WorldEntity worldEntity, EntityCell cellRoot, Optional parent, TaskResult> worldEntityResult, out IEnumerator asyncInstructions) + private bool SpawnWorldEntityChildSync(WorldEntity worldEntity, EntityCell cellRoot, GameObject parent, TaskResult> worldEntityResult, out IEnumerator asyncInstructions) { IWorldEntitySpawner spawner = spawnerResolver.ResolveEntitySpawner(worldEntity); @@ -192,9 +146,11 @@ private bool SpawnWorldEntityChildSync(WorldEntity worldEntity, EntityCell cellR asyncInstructions = SpawnWorldEntityChildAsync(worldEntity, cellRoot, parent, worldEntityResult); return false; } + GameObject spawnedObject = worldEntityResult.value.Value; - worldEntityResult.value.Value.transform.localPosition = worldEntity.Transform.LocalPosition.ToUnity(); - worldEntityResult.value.Value.transform.localRotation = worldEntity.Transform.LocalRotation.ToUnity(); + spawnedObject.transform.localPosition = worldEntity.Transform.LocalPosition.ToUnity(); + spawnedObject.transform.localRotation = worldEntity.Transform.LocalRotation.ToUnity(); + spawnedObject.transform.localScale = worldEntity.Transform.LocalScale.ToUnity(); asyncInstructions = null; return true; } diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs new file mode 100644 index 0000000000..6fd25271eb --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs @@ -0,0 +1,72 @@ +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; + } + + public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + if (!VerifyCanSpawnOrError(entity, parent, out PrefabPlaceholder placeholder)) + { + yield break; + } + + 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 (!VerifyCanSpawnOrError(entity, parent, out PrefabPlaceholder placeholder)) + { + return true; + } + + if (!defaultEntitySpawner.SpawnSync(entity, placeholder.transform.parent.gameObject, cellRoot, result)) + { + return false; + } + + SetupObject(entity, result.value.Value); + return true; + } + + private bool VerifyCanSpawnOrError(WorldEntity entity, Optional parent, out PrefabPlaceholder placeholder) + { + if (entity is PrefabPlaceholderEntity prefabEntity && + parent.Value && parent.Value.TryGetComponent(out PrefabPlaceholdersGroup group)) + { + placeholder = group.prefabPlaceholders[prefabEntity.ComponentIndex]; + return true; + } + + Log.Error($"[{nameof(PrefabPlaceholderEntitySpawner)}] Can't find a {nameof(PrefabPlaceholdersGroup)} on parent for {entity.Id}"); + placeholder = null; + return false; + } + + 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 new file mode 100644 index 0000000000..94a1e8418a --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.Unity; +using NitroxModel.DataStructures.Util; +using NitroxModel.Helper; +using NitroxModel_Subnautica.DataStructures; +using UnityEngine; +using UWE; + +namespace NitroxClient.GameLogic.Spawning.WorldEntities; + +public class SerializedWorldEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner +{ + /// + /// Contains the only types we allow the server to instantiate on clients (for security concerns) + /// + private readonly HashSet typesWhitelist = new() + { + typeof(Light), typeof(DisableBeforeExplosion), typeof(BoxCollider), typeof(SphereCollider) + }; + + public SerializedWorldEntitySpawner() + { + // Preloading a useful asset + if (!NitroxEnvironment.IsTesting && !ProtobufSerializer.emptyGameObjectPrefab) + { + ProtobufSerializer.emptyGameObjectPrefab = Resources.Load("SerializerEmptyGameObject"); + } + } + + public IEnumerator SpawnAsync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + SpawnSync(entity, parent, cellRoot, result); + yield break; + } + + public bool SpawnsOwnChildren() => false; + + public bool SpawnSync(WorldEntity entity, Optional parent, EntityCell cellRoot, TaskResult> result) + { + if (entity is not SerializedWorldEntity serializedWorldEntity) + { + return true; + } + + using PooledObject proxy = ProtobufSerializerPool.GetProxy(); + ProtobufSerializer serializer = proxy.Value; + + UniqueIdentifier uniqueIdentifier = serializer.CreateEmptyGameObject("SerializerEmptyGameObject"); + GameObject gameObject = uniqueIdentifier.gameObject; + gameObject.SetActive(false); + gameObject.layer = serializedWorldEntity.Layer; + gameObject.tag = "Untagged"; // Same tag for all empty game objects + + LargeWorldEntity largeWorldEntity = gameObject.AddComponent(); + largeWorldEntity.cellLevel = (LargeWorldEntity.CellLevel)serializedWorldEntity.Level; + + Transform transform = gameObject.transform; + transform.SetParent(cellRoot.liveRoot.transform); + NitroxVector3 localPosition = serializedWorldEntity.Transform.LocalPosition - serializedWorldEntity.AbsoluteEntityCell.Position; + transform.localPosition = localPosition.ToUnity(); + transform.localRotation = serializedWorldEntity.Transform.LocalRotation.ToUnity(); + transform.localScale = serializedWorldEntity.Transform.LocalScale.ToUnity(); + + // Code inspired from ProtobufSerializer.DeserializeIntoGameObject + Dictionary dictionary = ProtobufSerializer.componentCountersPool.Get(); + dictionary.Clear(); + foreach (SerializedComponent serializedComponent in serializedWorldEntity.Components) + { + string typeName = serializedComponent.TypeName; + Type cachedType = ProtobufSerializer.GetCachedType(typeName); + if (!typesWhitelist.Contains(cachedType)) + { + Log.ErrorOnce($"Server asked to instantiate a non-whitelisted type {typeName}."); + return true; + } + + using MemoryStream stream = new(serializedComponent.Data); + int id = ProtobufSerializer.IncrementComponentCounter(dictionary, cachedType); + Component orAddComponent = ProtobufSerializer.GetOrAddComponent(gameObject, cachedType, typeName, id, true); + if (orAddComponent) + { + serializer.Deserialize(stream, orAddComponent, cachedType, false); + } + else + { + Log.ErrorOnce($"Deserializing component {typeName} into {gameObject} failed"); + } + ProtobufSerializer.SetIsEnabled(orAddComponent, serializedComponent.IsEnabled); + } + foreach (IProtoEventListener listener in gameObject.GetComponents()) + { + listener.OnProtoDeserialize(serializer); + } + dictionary.Clear(); + ProtobufSerializer.componentCountersPool.Return(dictionary); + + gameObject.SetActive(true); + + result.Set(gameObject); + return true; + } +} diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs index b378ea46bb..25571e8f79 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs @@ -11,8 +11,10 @@ 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; private readonly Dictionary customSpawnersByTechType = new(); @@ -23,20 +25,26 @@ public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, P customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner(entityMetadataManager); vehicleWorldEntitySpawner = new(entities); - prefabWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(this, defaultEntitySpawner, entityMetadataManager); + prefabPlaceholderEntitySpawner = new(defaultEntitySpawner); + placeholderGroupWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(entities, this, defaultEntitySpawner, entityMetadataManager, prefabPlaceholderEntitySpawner); playerWorldEntitySpawner = new PlayerWorldEntitySpawner(playerManager, localPlayer); + serializedWorldEntitySpawner = new SerializedWorldEntitySpawner(); } public IWorldEntitySpawner ResolveEntitySpawner(WorldEntity entity) { switch (entity) { + case PrefabPlaceholderEntity: + return prefabPlaceholderEntitySpawner; case PlaceholderGroupWorldEntity: - return prefabWorldEntitySpawner; + return placeholderGroupWorldEntitySpawner; case PlayerWorldEntity: return playerWorldEntitySpawner; case VehicleWorldEntity: return vehicleWorldEntitySpawner; + case SerializedWorldEntity: + return serializedWorldEntitySpawner; } TechType techType = entity.TechType.ToUnity(); diff --git a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs index 6cd9bf405a..f28b20968d 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs @@ -14,7 +14,7 @@ namespace NitroxClient.GameLogic.Spawning; -public class WorldEntitySpawner : EntitySpawner +public class WorldEntitySpawner : SyncEntitySpawner { private readonly WorldEntitySpawnerResolver worldEntitySpawnResolver; private readonly Dictionary batchCellsById; @@ -31,8 +31,6 @@ public WorldEntitySpawner(EntityMetadataManager entityMetadataManager, PlayerMan 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) { @@ -59,6 +57,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); + } + public 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 05d2a6cc31..c9820d1c72 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUwePrefabFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Threading; @@ -6,67 +6,75 @@ using NitroxModel.DataStructures.GameLogic.Entities; using static LootDistributionData; -namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities +namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities; + +public class SubnauticaUwePrefabFactory : IUwePrefabFactory { - public class SubnauticaUwePrefabFactory : UwePrefabFactory + private readonly LootDistributionData lootDistributionData; + private readonly Dictionary> cache = new(); + + public SubnauticaUwePrefabFactory(string lootDistributionJson) { - private readonly LootDistributionData lootDistributionData; + lootDistributionData = GetLootDistributionData(lootDistributionJson); + } - public SubnauticaUwePrefabFactory(string lootDistributionJson) + public bool TryGetPossiblePrefabs(string biome, out List prefabs) + { + if (biome == null) { - lootDistributionData = GetLootDistributionData(lootDistributionJson); + prefabs = null; + return false; } - - public override List GetPossiblePrefabs(string biome) + if (cache.TryGetValue(biome, out prefabs)) { - List prefabs = new List(); - - if (biome == null) - { - return prefabs; - } - - - BiomeType biomeType = (BiomeType)Enum.Parse(typeof(BiomeType), biome); + return true; + } - if (lootDistributionData.GetBiomeLoot(biomeType, out DstData dstData)) + prefabs = new(); + BiomeType biomeType = (BiomeType)Enum.Parse(typeof(BiomeType), biome); + if (lootDistributionData.GetBiomeLoot(biomeType, out DstData dstData)) + { + foreach (PrefabData prefabData in dstData.prefabs) { - foreach (PrefabData prefabData in dstData.prefabs) + if (lootDistributionData.srcDistribution.TryGetValue(prefabData.classId, out SrcData srcData)) { - UwePrefab prefab = new UwePrefab(prefabData.classId, prefabData.probability, prefabData.count); - prefabs.Add(prefab); + // Manually went through the list of those to make this "filter" + // You can verify this by looping through all of SrcData (e.g in LootDistributionData.Initialize) + // print the prefabPath and check the TechType related to the provided classId (WorldEntityDatabase.TryGetInfo) with PDAScanner.IsFragment + bool isFragment = srcData.prefabPath.Contains("Fragment") || srcData.prefabPath.Contains("BaseGlassDome"); + prefabs.Add(new(prefabData.classId, prefabData.count, prefabData.probability, isFragment)); } } - - return prefabs; } + cache[biome] = prefabs; + return true; + } - private LootDistributionData GetLootDistributionData(string lootDistributionJson) - { - ForceCultureOverride(); - JsonMapper.RegisterImporter((double value) => Convert.ToSingle(value)); + private LootDistributionData GetLootDistributionData(string lootDistributionJson) + { + ForceCultureOverride(); + JsonMapper.RegisterImporter((double value) => Convert.ToSingle(value)); - Dictionary result = JsonMapper.ToObject>(lootDistributionJson); + Dictionary result = JsonMapper.ToObject>(lootDistributionJson); - LootDistributionData lootDistributionData = new LootDistributionData(); - lootDistributionData.Initialize(result); + LootDistributionData lootDistributionData = new LootDistributionData(); + lootDistributionData.Initialize(result); - return lootDistributionData; - } + return lootDistributionData; + } - // LitJson uses the computers local CultureInfo when parsing the JSON files. However, - // these json files were saved in en_US. Ensure that this is done for the current thread. - private void ForceCultureOverride() - { - CultureInfo cultureInfo = new CultureInfo("en-US"); + // LitJson uses the computers local CultureInfo when parsing the JSON files. However, + // these json files were saved in en_US. Ensure that this is done for the current thread. + private void ForceCultureOverride() + { + CultureInfo cultureInfo = new CultureInfo("en-US"); - // Although we loaded the en-US cultureInfo, let's make sure to set these incase the - // default was overriden by the user. - cultureInfo.NumberFormat.NumberDecimalSeparator = "."; - cultureInfo.NumberFormat.NumberGroupSeparator = ","; + // Although we loaded the en-US cultureInfo, let's make sure to set these incase the + // default was overriden by the user. + cultureInfo.NumberFormat.NumberDecimalSeparator = "."; + cultureInfo.NumberFormat.NumberGroupSeparator = ","; - Thread.CurrentThread.CurrentCulture = cultureInfo; - Thread.CurrentThread.CurrentUICulture = cultureInfo; - } + Thread.CurrentThread.CurrentCulture = cultureInfo; + Thread.CurrentThread.CurrentUICulture = cultureInfo; } } diff --git a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs index 64011c0e7f..9c2de05b8a 100644 --- a/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs +++ b/NitroxModel-Subnautica/DataStructures/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NitroxModel.DataStructures.GameLogic.Entities; -using NitroxModel.DataStructures.Util; using UWE; -namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities +namespace NitroxModel_Subnautica.DataStructures.GameLogic.Entities; + +public class SubnauticaUweWorldEntityFactory : IUweWorldEntityFactory { - public class SubnauticaUweWorldEntityFactory : UweWorldEntityFactory - { - private Dictionary worldEntitiesByClassId; + private readonly Dictionary worldEntitiesByClassId; - public SubnauticaUweWorldEntityFactory(Dictionary worldEntitiesByClassId) - { - this.worldEntitiesByClassId = worldEntitiesByClassId; - } + public SubnauticaUweWorldEntityFactory(Dictionary worldEntitiesByClassId) + { + this.worldEntitiesByClassId = worldEntitiesByClassId; + } - public override Optional From(string classId) + public bool TryFind(string classId, out UweWorldEntity uweWorldEntity) + { + if (worldEntitiesByClassId.TryGetValue(classId, out WorldEntityInfo worldEntityInfo)) { + uweWorldEntity = new(worldEntityInfo.classId, + worldEntityInfo.techType.ToDto(), + worldEntityInfo.slotType.ToString(), + worldEntityInfo.prefabZUp, + (int)worldEntityInfo.cellLevel, + worldEntityInfo.localScale.ToDto()); - if (worldEntitiesByClassId.TryGetValue(classId, out WorldEntityInfo worldEntityInfo)) - { - UweWorldEntity uweWorldEntity = new UweWorldEntity(worldEntityInfo.techType.ToDto(), - worldEntityInfo.localScale.ToDto(), - worldEntityInfo.classId, - worldEntityInfo.slotType.ToString(), - (int)worldEntityInfo.cellLevel); - - return Optional.Of(uweWorldEntity); - } - - return Optional.Empty; + return true; } + uweWorldEntity = null; + return false; } } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs b/NitroxModel/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs new file mode 100644 index 0000000000..f600e6c0cf --- /dev/null +++ b/NitroxModel/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace NitroxModel.DataStructures.GameLogic.Entities; + +public interface IUwePrefabFactory +{ + public bool TryGetPossiblePrefabs(string biomeType, out List prefabs); +} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs b/NitroxModel/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs new file mode 100644 index 0000000000..680e0f9b8a --- /dev/null +++ b/NitroxModel/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs @@ -0,0 +1,6 @@ +namespace NitroxModel.DataStructures.GameLogic.Entities; + +public interface IUweWorldEntityFactory +{ + public bool TryFind(string classId, out UweWorldEntity uweWorldEntity); +} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/PlaceholderGroupWorldEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/PlaceholderGroupWorldEntity.cs index 0af29efadf..13a73f123c 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/PlaceholderGroupWorldEntity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/PlaceholderGroupWorldEntity.cs @@ -7,17 +7,19 @@ namespace NitroxModel.DataStructures.GameLogic.Entities; -[Serializable] -[DataContract] +[Serializable, DataContract] public class PlaceholderGroupWorldEntity : WorldEntity { + [DataMember(Order = 1)] + public int ComponentIndex { get; set; } + [IgnoreConstructor] protected PlaceholderGroupWorldEntity() { // Constructor for serialization. Has to be "protected" for json serialization. } - public PlaceholderGroupWorldEntity(WorldEntity worldEntity, List prefabPlaceholders) + public PlaceholderGroupWorldEntity(WorldEntity worldEntity, int componentIndex = 0) { Id = worldEntity.Id; TechType = worldEntity.TechType; @@ -27,25 +29,19 @@ public PlaceholderGroupWorldEntity(WorldEntity worldEntity, List prefabP Level = worldEntity.Level; ClassId = worldEntity.ClassId; SpawnedByServer = worldEntity.SpawnedByServer; - ChildEntities = prefabPlaceholders; + ChildEntities = worldEntity.ChildEntities; + ComponentIndex = componentIndex; } /// Used for deserialization - public PlaceholderGroupWorldEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) + public PlaceholderGroupWorldEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities, int componentIndex) : + base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) { - Transform = transform; - Level = level; - ClassId = classId; - SpawnedByServer = spawnedByServer; - Id = id; - TechType = techType; - Metadata = metadata; - ParentId = parentId; - ChildEntities = childEntities; + ComponentIndex = componentIndex; } public override string ToString() { - return $"[PlaceholderGroupWorldEntity {base.ToString()}]"; + return $"[PlaceholderGroupWorldEntity ComponentIndex: {ComponentIndex} {base.ToString()}]"; } } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/PrefabPlaceholderEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/PrefabPlaceholderEntity.cs index 92291e75f5..293254148e 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/PrefabPlaceholderEntity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/PrefabPlaceholderEntity.cs @@ -3,48 +3,49 @@ using System.Runtime.Serialization; using BinaryPack.Attributes; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using NitroxModel.DataStructures.Unity; -namespace NitroxModel.DataStructures.GameLogic.Entities +namespace NitroxModel.DataStructures.GameLogic.Entities; + +/// +/// Represents a Placeholder GameObject located under a PrefabPlaceholdersGroup +/// +[Serializable, DataContract] +public class PrefabPlaceholderEntity : WorldEntity { - [Serializable] - [DataContract] - public class PrefabPlaceholderEntity : Entity + [DataMember(Order = 1)] + public int ComponentIndex { get; set; } + + [IgnoreConstructor] + protected PrefabPlaceholderEntity() { - [DataMember(Order = 1)] - public string ClassId { get; set; } + // Constructor for serialization. Has to be "protected" for json serialization. + } - [IgnoreConstructor] - protected PrefabPlaceholderEntity() - { - // Constructor for serialization. Has to be "protected" for json serialization. - } + public PrefabPlaceholderEntity(WorldEntity worldEntity, int componentIndex = 0) + { + Id = worldEntity.Id; + TechType = worldEntity.TechType; + Metadata = worldEntity.Metadata; + ParentId = worldEntity.ParentId; + Transform = worldEntity.Transform; + Level = worldEntity.Level; + ClassId = worldEntity.ClassId; + SpawnedByServer = worldEntity.SpawnedByServer; + ChildEntities = worldEntity.ChildEntities; + ComponentIndex = componentIndex; + } - public PrefabPlaceholderEntity(NitroxId id, NitroxTechType techType, NitroxId parentId) - { - Id = id; - TechType = techType; - ParentId = parentId; - ChildEntities = new List(); - } - public PrefabPlaceholderEntity(NitroxId id, string classId, NitroxTechType techType, NitroxId parentId, List childEntities) - { - Id = id; - ClassId = classId; - TechType = techType; - ParentId = parentId; - ChildEntities = childEntities; - } + /// Used for deserialization + public PrefabPlaceholderEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities, int componentIndex) : + base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) + { + ComponentIndex = componentIndex; + } - /// Used for deserialization - public PrefabPlaceholderEntity(NitroxId id, string classId, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) - { - Id = id; - ClassId = classId; - TechType = techType; - Metadata = metadata; - ParentId = parentId; - ChildEntities = childEntities; - } + public override string ToString() + { + return $"[PrefabPlaceholderEntity ComponentIndex: {ComponentIndex} {base.ToString()}]"; } } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs new file mode 100644 index 0000000000..1bc3ee38fa --- /dev/null +++ b/NitroxModel/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using BinaryPack.Attributes; +using NitroxModel.DataStructures.GameLogic.Entities.Metadata; +using NitroxModel.DataStructures.Unity; + +namespace NitroxModel.DataStructures.GameLogic.Entities; + +/// +/// An implementation for GameObjects marked as CreateEmptyObject in the protobuf serializer. +/// They seem to represent unique (which is why they don't exist as a prefab) statically created objects which are part of the decor but can't be interacted with. +/// +/// +/// The associated GameObjects are mostly alone in their own EntityCell. +/// To avoid creating an EntityCell entity for each of them we give it a global position and its actual cell reference. +/// It is safe to give a static cell because those objects don't move. +/// +[Serializable] +[DataContract] +public class SerializedWorldEntity : WorldEntity +{ + public override AbsoluteEntityCell AbsoluteEntityCell => new(BatchId, CellId, Level); + + [DataMember(Order = 1)] + public List Components { get; set; } = new(); + + [DataMember(Order = 2)] + public int Layer { get; set; } + + /// + /// This entity is not parented so it doesn't have info for the cell containing it, + /// therefore we need to serialize it instead of generating it from a local position (which would make no sense) + /// + [DataMember(Order = 3)] + public NitroxInt3 BatchId { get; set; } + + /// + [DataMember(Order = 4)] + public NitroxInt3 CellId { get; set; } + + [IgnoreConstructor] + protected SerializedWorldEntity() + { + // Constructor for serialization. Has to be "protected" for json serialization. + } + + public SerializedWorldEntity(List components, int layer, NitroxTransform transform, NitroxId id, NitroxId parentId, AbsoluteEntityCell cell) + { + Components = components; + Layer = layer; + Transform = transform; + Id = id; + ParentId = parentId; + Level = cell.Level; + BatchId = cell.BatchId; + CellId = cell.CellId; + } + + /// Used for deserialization + public SerializedWorldEntity(List components, int layer, NitroxInt3 batchId, NitroxInt3 cellId, NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) : + base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) + { + Components = components; + Layer = layer; + BatchId = batchId; + CellId = cellId; + } + + public override string ToString() + { + return $"[{nameof(SerializedWorldEntity)} Components: {Components.Count}, Layer: {Layer}, BatchId: {BatchId}, CellId: {CellId} {base.ToString()}]"; + } +} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs index 2936c39d51..ab8d278b2b 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/UwePrefab.cs @@ -1,16 +1,3 @@ -namespace NitroxModel.DataStructures.GameLogic.Entities -{ - public class UwePrefab - { - public string ClassId { get; } - public float Probability { get; } - public int Count { get; } +namespace NitroxModel.DataStructures.GameLogic.Entities; - public UwePrefab(string classId, float probability, int count) - { - ClassId = classId; - Probability = probability; - Count = count; - } - } -} +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 deleted file mode 100644 index 4b6f5cfe30..0000000000 --- a/NitroxModel/DataStructures/GameLogic/Entities/UwePrefabFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace NitroxModel.DataStructures.GameLogic.Entities -{ - public abstract class UwePrefabFactory - { - public abstract List GetPossiblePrefabs(string biomeType); - } -} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntity.cs index ac031b416a..0608deb201 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntity.cs @@ -1,23 +1,23 @@ - using NitroxModel.DataStructures.Unity; -namespace NitroxModel.DataStructures.GameLogic.Entities +namespace NitroxModel.DataStructures.GameLogic.Entities; + +public class UweWorldEntity { - public class UweWorldEntity - { - public NitroxTechType TechType { get; } - public NitroxVector3 Scale { get; } - public string ClassId { get; } - public string SlotType { get; } - public int CellLevel { get; } + public string ClassId { get; } + public NitroxTechType TechType { get; } + public string SlotType { get; } + public bool PrefabZUp { get; } + public int CellLevel { get; } + public NitroxVector3 LocalScale { get; } - public UweWorldEntity(NitroxTechType techType, NitroxVector3 scale, string classId, string slotType, int cellLevel) - { - TechType = techType; - Scale = scale; - ClassId = classId; - SlotType = slotType; - CellLevel = cellLevel; - } + public UweWorldEntity(string classId, NitroxTechType techType, string slotType, bool prefabZUp, int cellLevel, NitroxVector3 localScale) + { + ClassId = classId; + TechType = techType; + SlotType = slotType; + PrefabZUp = prefabZUp; + CellLevel = cellLevel; + LocalScale = localScale; } } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs b/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs deleted file mode 100644 index 1214ee6f2a..0000000000 --- a/NitroxModel/DataStructures/GameLogic/Entities/UweWorldEntityFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NitroxModel.DataStructures.Util; - -namespace NitroxModel.DataStructures.GameLogic.Entities -{ - public abstract class UweWorldEntityFactory - { - public abstract Optional From(string classId); - } -} diff --git a/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs b/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs index 84435c0366..820a445ad8 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/WorldEntity.cs @@ -19,9 +19,11 @@ namespace NitroxModel.DataStructures.GameLogic.Entities [ProtoInclude(52, typeof(GlobalRootEntity))] [ProtoInclude(53, typeof(OxygenPipeEntity))] [ProtoInclude(54, typeof(PlacedWorldEntity))] + [ProtoInclude(55, typeof(SerializedWorldEntity))] + [ProtoInclude(56, typeof(PrefabPlaceholderEntity))] public class WorldEntity : Entity { - public AbsoluteEntityCell AbsoluteEntityCell => new AbsoluteEntityCell(Transform.Position, Level); + public virtual AbsoluteEntityCell AbsoluteEntityCell => new(Transform.Position, Level); [DataMember(Order = 1)] public NitroxTransform Transform { get; set; } diff --git a/NitroxModel/DataStructures/GameLogic/Entity.cs b/NitroxModel/DataStructures/GameLogic/Entity.cs index 1a4c1458f0..82ef2b4de8 100644 --- a/NitroxModel/DataStructures/GameLogic/Entity.cs +++ b/NitroxModel/DataStructures/GameLogic/Entity.cs @@ -10,14 +10,13 @@ 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(50, typeof(PrefabChildEntity))] + [ProtoInclude(51, typeof(InventoryEntity))] + [ProtoInclude(52, typeof(InventoryItemEntity))] + [ProtoInclude(53, typeof(PathBasedChildEntity))] + [ProtoInclude(54, typeof(InstalledBatteryEntity))] + [ProtoInclude(55, typeof(InstalledModuleEntity))] + [ProtoInclude(56, typeof(WorldEntity))] public abstract class Entity { [DataMember(Order = 1)] diff --git a/NitroxModel/DataStructures/SerializedComponent.cs b/NitroxModel/DataStructures/SerializedComponent.cs new file mode 100644 index 0000000000..8b9950a9f2 --- /dev/null +++ b/NitroxModel/DataStructures/SerializedComponent.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace NitroxModel.DataStructures; + +/// +/// Holds an Unity component's data to be restored on clients. +/// +[Serializable, DataContract] +public class SerializedComponent +{ + [DataMember(Order = 1)] + public string TypeName { get; set; } + + [DataMember(Order = 2)] + public bool IsEnabled { get; set; } + + [DataMember(Order = 3)] + public byte[] Data { get; set; } + + protected SerializedComponent() + { + //Constructor for serialization. Has to be "protected" for json serialization. + } + + public SerializedComponent(string typeName, bool isEnabled, byte[] data) + { + TypeName = typeName; + IsEnabled = isEnabled; + Data = data; + } + + // Generated by Visual Studio + public override bool Equals(object obj) + { + return obj is SerializedComponent component && + TypeName == component.TypeName && + IsEnabled == component.IsEnabled && + EqualityComparer.Default.Equals(Data, component.Data); + } + + public override int GetHashCode() + { + int hashCode = -1120560399; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(TypeName); + hashCode = hashCode * -1521134295 + IsEnabled.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Data); + return hashCode; + } +} diff --git a/NitroxModel/DataStructures/Unity/NitroxVector3.cs b/NitroxModel/DataStructures/Unity/NitroxVector3.cs index 68c21b7e34..5a380d11c1 100644 --- a/NitroxModel/DataStructures/Unity/NitroxVector3.cs +++ b/NitroxModel/DataStructures/Unity/NitroxVector3.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using System.Runtime.Serialization; using NitroxModel.Helper; @@ -18,6 +18,8 @@ public struct NitroxVector3 : IEquatable [DataMember(Order = 3)] public float Z; + public NitroxVector3 Normalized => Normalize(this); + public NitroxVector3(float x, float y, float z) { X = x; @@ -29,7 +31,6 @@ public NitroxVector3(float x, float y, float z) public static NitroxVector3 One => new(1, 1, 1); - public static bool operator ==(NitroxVector3 left, NitroxVector3 right) => left.Equals(right); public static bool operator !=(NitroxVector3 left, NitroxVector3 right) => !left.Equals(right); diff --git a/NitroxModel/Helper/IMap.cs b/NitroxModel/Helper/IMap.cs index e70904f066..e31aa61d3a 100644 --- a/NitroxModel/Helper/IMap.cs +++ b/NitroxModel/Helper/IMap.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; @@ -7,6 +7,9 @@ namespace NitroxModel.Helper public interface IMap { public int BatchSize { get; } + /// + /// AKA LargeWorldStreamer.blocksPerBatch + /// public NitroxInt3 BatchDimensions { get; } public NitroxInt3 DimensionsInMeters { get; } public NitroxInt3 DimensionsInBatches { get; } diff --git a/NitroxPatcher/Patches/Dynamic/PrefabPlaceholdersGroup_Spawn_Patch.cs b/NitroxPatcher/Patches/Dynamic/PrefabPlaceholdersGroup_Spawn_Patch.cs index 827f9297f7..9754aebb2c 100644 --- a/NitroxPatcher/Patches/Dynamic/PrefabPlaceholdersGroup_Spawn_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/PrefabPlaceholdersGroup_Spawn_Patch.cs @@ -1,8 +1,11 @@ -using System.Reflection; +using System.Reflection; using NitroxModel.Helper; namespace NitroxPatcher.Patches.Dynamic; +/// +/// Disables local spawning of placeholders in favor of an entity creation on server-side. +/// public sealed partial class PrefabPlaceholdersGroup_Spawn_Patch : NitroxPatch, IDynamicPatch { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((PrefabPlaceholdersGroup t) => t.Spawn()); diff --git a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/CrashFishBootstrapper.cs b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/CrashFishBootstrapper.cs index 091c6d1529..051377b5f4 100644 --- a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/CrashFishBootstrapper.cs +++ b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/CrashFishBootstrapper.cs @@ -12,17 +12,19 @@ public class CrashFishBootstrapper : IEntityBootstrapper { public void Prepare(WorldEntity entity, DeterministicGenerator deterministicBatchGenerator) { - // Crashfish are PrefabPlaceholderGroups so should always have a PrefabChildEntity in betweeen the crash and the home. - PrefabChildEntity placeholder = new(deterministicBatchGenerator.NextId(), null, TechType.None.ToDto(), 0, null, entity.Id); - entity.ChildEntities.Add(placeholder); - // TODO: Fix this. it currently is an issue for PlaceholderGroupWorldEntitySpawner's child spawning // On client-side, multiple crashhomes are spawned (while there probably should only be a few) // and their crash fishes are stuck in their homes // Potential fix: Only handle child spawning by clients (the simulating one) - WorldEntity crashFish = SpawnChild(placeholder, deterministicBatchGenerator, TechType.Crash, "7d307502-46b7-4f86-afb0-65fe8867f893", entity.Level); - crashFish.Transform.LocalRotation = new NitroxQuaternion(-0.7071068f, 0, 0, 0.7071068f); - placeholder.ChildEntities.Add(crashFish); + + + //WorldEntity crashFish = SpawnChild(placeholder, deterministicBatchGenerator, TechType.Crash, "7d307502-46b7-4f86-afb0-65fe8867f893", entity.Level); + // Crashfish are PrefabPlaceholderGroups so should always have a PrefabChildEntity in betweeen the crash and the home. + //PrefabChildEntity placeholder = new(deterministicBatchGenerator.NextId(), null, TechType.None.ToDto(), 0, null, entity.Id); + //entity.ChildEntities.Add(placeholder); + + //crashFish.Transform.LocalRotation = new NitroxQuaternion(-0.7071068f, 0, 0, 0.7071068f); + //placeholder.ChildEntities.Add(crashFish); } private WorldEntity SpawnChild(Entity parentEntity, DeterministicGenerator deterministicBatchGenerator, TechType techType, string classId, int level) diff --git a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SlotsHelper.cs b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SlotsHelper.cs index f135cdb3f8..0006e17196 100644 --- a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SlotsHelper.cs +++ b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SlotsHelper.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning { @@ -13,20 +13,6 @@ public static class SlotsHelper { EntitySlotData.EntitySlotType.Creature, EntitySlot.Type.Creature } }; - public static List GetEntitySlotTypes(IEntitySlot entitySlot) - { - if (entitySlot is EntitySlot) - { - return ((EntitySlot)entitySlot).allowedTypes; - } - else if (entitySlot is EntitySlotData) - { - return ConvertSlotTypes(((EntitySlotData)entitySlot).allowedTypes); - } - - throw new System.Exception($"Unknown EntitySlotType {entitySlot.GetType()}"); - } - public static List ConvertSlotTypes(EntitySlotData.EntitySlotType entitySlotType) { List slotsTypes = new List(); diff --git a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SubnauticaEntitySpawnPointFactory.cs b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SubnauticaEntitySpawnPointFactory.cs index fc53fd299a..4cae33f6c8 100644 --- a/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SubnauticaEntitySpawnPointFactory.cs +++ b/NitroxServer-Subnautica/GameLogic/Entities/Spawning/SubnauticaEntitySpawnPointFactory.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; @@ -17,13 +17,20 @@ public override List From(AbsoluteEntityCell absoluteEntityCel List spawnPoints = new List(); EntitySlotsPlaceholder entitySlotsPlaceholder = gameObject.GetComponent(); - if (!ReferenceEquals(entitySlotsPlaceholder, null)) + if (gameObject.CreateEmptyObject) + { + SerializedEntitySpawnPoint entitySpawnPoint = new(gameObject.SerializedComponents, gameObject.Layer, absoluteEntityCell, transform); + + HandleParenting(spawnPoints, entitySpawnPoint, gameObject); + spawnPoints.Add(entitySpawnPoint); + } + else if (!ReferenceEquals(entitySlotsPlaceholder, null)) { foreach (EntitySlotData entitySlotData in entitySlotsPlaceholder.slotsData) { - List slotTypes = SlotsHelper.GetEntitySlotTypes(entitySlotData); + List slotTypes = SlotsHelper.ConvertSlotTypes(entitySlotData.allowedTypes); List stringSlotTypes = slotTypes.Select(s => s.ToString()).ToList(); - EntitySpawnPoint entitySpawnPoint = new EntitySpawnPoint(absoluteEntityCell, + EntitySpawnPoint entitySpawnPoint = new(absoluteEntityCell, entitySlotData.localPosition.ToDto(), entitySlotData.localRotation.ToDto(), stringSlotTypes, @@ -36,7 +43,7 @@ public override List From(AbsoluteEntityCell absoluteEntityCel } else { - EntitySpawnPoint entitySpawnPoint = new EntitySpawnPoint(absoluteEntityCell, transform.LocalPosition, transform.LocalRotation, transform.LocalScale, gameObject.ClassId); + EntitySpawnPoint entitySpawnPoint = new(absoluteEntityCell, transform.LocalPosition, transform.LocalRotation, transform.LocalScale, gameObject.ClassId); HandleParenting(spawnPoints, entitySpawnPoint, gameObject); } @@ -56,9 +63,7 @@ private void HandleParenting(List spawnPoints, EntitySpawnPoin if (gameObject.Parent == null) { - spawnPoints.Add( - entitySpawnPoint - ); + spawnPoints.Add(entitySpawnPoint); } } } diff --git a/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetTypeValueFieldExtension.cs b/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetTypeValueFieldExtension.cs index 4f8ec2814d..fe00c7bbd3 100644 --- a/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetTypeValueFieldExtension.cs +++ b/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetTypeValueFieldExtension.cs @@ -6,17 +6,17 @@ namespace NitroxServer_Subnautica.Resources.Parsers.Helper; public static class AssetTypeValueFieldExtension { - public static Vector3 AsVector3(this AssetTypeValueField valueField) + public static Vector3 ToVector3(this AssetTypeValueField valueField) { return new Vector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat); } - public static NitroxVector3 AsNitroxVector3(this AssetTypeValueField valueField) + public static NitroxVector3 ToNitroxVector3(this AssetTypeValueField valueField) { return new NitroxVector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat); } - public static NitroxQuaternion AsNitroxQuaternion(this AssetTypeValueField valueField) + public static NitroxQuaternion ToNitroxQuaternion(this AssetTypeValueField valueField) { return new NitroxQuaternion(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat, valueField["w"].AsFloat); } diff --git a/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetsBundleManager.cs b/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetsBundleManager.cs index 7c59748d01..b51d4a7b55 100644 --- a/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetsBundleManager.cs +++ b/NitroxServer-Subnautica/Resources/Parsers/Helper/AssetsBundleManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using AssetsTools.NET; using AssetsTools.NET.Extra; +using NitroxModel.DataStructures.Unity; namespace NitroxServer_Subnautica.Resources.Parsers.Helper; @@ -97,14 +98,14 @@ public AssetFileInfo GetMonoBehaviourFromGameObject(AssetsFileInstance inst, Ass return monoBehaviourInf; } - public AssetTypeValueField GetTransformComponent(AssetsFileInstance assetFileInst, AssetTypeValueField rootGameObject) + public NitroxTransform GetTransformFromGameObject(AssetsFileInstance assetFileInst, AssetTypeValueField rootGameObject) { AssetTypeValueField componentArray = rootGameObject["m_Component"]["Array"]; AssetTypeValueField transformRef = componentArray[0]["component"]; - AssetExternal transformExt = GetExtAsset(assetFileInst, transformRef); + AssetTypeValueField transformField = GetExtAsset(assetFileInst, transformRef).baseField; - return transformExt.baseField; + return new(transformField["m_LocalPosition"].ToNitroxVector3(), transformField["m_LocalRotation"].ToNitroxQuaternion(), transformField["m_LocalScale"].ToNitroxVector3()); } public new void SetMonoTempGenerator(IMonoBehaviourTemplateGenerator generator) diff --git a/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs b/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs index b37710c1cf..11c6275250 100644 --- a/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs +++ b/NitroxServer-Subnautica/Resources/Parsers/PrefabPlaceholderGroupsParser.cs @@ -8,10 +8,10 @@ using AddressablesTools.Catalog; using AssetsTools.NET; using AssetsTools.NET.Extra; -using NitroxModel.DataStructures.GameLogic; -using NitroxServer_Subnautica.Resources.Parsers.Helper; +using NitroxModel.DataStructures.Unity; using NitroxServer.GameLogic.Entities; using NitroxServer.Resources; +using NitroxServer_Subnautica.Resources.Parsers.Helper; namespace NitroxServer_Subnautica.Resources.Parsers; @@ -22,6 +22,11 @@ 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(); + public PrefabPlaceholderGroupsParser() { string resourcePath = ResourceAssetsParser.FindDirectoryContainingResourceAssets(); @@ -38,10 +43,6 @@ public PrefabPlaceholderGroupsParser() am.SetMonoTempGenerator(monoGen = new(managedPath)); } - private readonly ConcurrentDictionary addressableCatalog = new(); - private readonly ConcurrentDictionary prefabPlaceholderByClassId = new(); - private readonly ConcurrentDictionary prefabPlaceholdersByGroupClassId = new(); - public Dictionary ParseFile() { // Get all prefab-classIds linked to the (partial) bundle path @@ -56,9 +57,7 @@ public Dictionary ParseFile() am.UnloadAll(); // Get all needed data for the filtered PrefabPlaceholdersGroups to construct PrefabPlaceholdersGroupAssets and add them to the dictionary by classId - ConcurrentDictionary prefabPlaceholderGroupsByGroupClassId = GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths); - - return new Dictionary(prefabPlaceholderGroupsByGroupClassId); + return new Dictionary(GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths)); } private static Dictionary LoadPrefabDatabase(string fullFilename) @@ -86,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]) @@ -200,63 +208,106 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle( private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId) { - if (!string.IsNullOrEmpty(classId) && prefabPlaceholdersByGroupClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedPrefabPlaceholdersGroup)) + if (!string.IsNullOrEmpty(classId) && groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup)) { - return cachedPrefabPlaceholdersGroup; + return cachedGroup; } AssetFileInfo prefabPlaceholdersGroupInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, rootGameObjectInfo, "PrefabPlaceholdersGroup"); if (prefabPlaceholdersGroupInfo == null) { - return null; + return default; } AssetTypeValueField prefabPlaceholdersGroupScript = amInst.GetBaseField(assetFileInst, prefabPlaceholdersGroupInfo); List prefabPlaceholdersOnGroup = prefabPlaceholdersGroupScript["prefabPlaceholders"].Children; - PrefabPlaceholderAsset[] prefabPlaceholders = new PrefabPlaceholderAsset[prefabPlaceholdersOnGroup.Count]; + 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; - prefabPlaceholders[index] = GetAndCachePrefabPlaceholderAsset(amInst, prefabPlaceholder["prefabClassId"].AsString); - } - NitroxTechType nitroxTechType = null; - AssetFileInfo techTagInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, rootGameObjectInfo, nameof(TechTag)); - if (techTagInfo != null) - { - AssetTypeValueField entitySlot = amInst.GetBaseField(assetFileInst, techTagInfo); - TechType techType = (TechType)entitySlot["type"].AsInt; - - nitroxTechType = new NitroxTechType(techType.ToString()); + 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(prefabPlaceholders, nitroxTechType); + PrefabPlaceholdersGroupAsset prefabPlaceholdersGroup = new(classId, prefabPlaceholders); + AssetTypeValueField rootGameObjectField = amInst.GetBaseField(assetFileInst, rootGameObjectInfo); + NitroxTransform groupTransform = amInst.GetTransformFromGameObject(assetFileInst, rootGameObjectField); + prefabPlaceholdersGroup.Transform = groupTransform; - prefabPlaceholdersByGroupClassId.TryAdd(classId, prefabPlaceholdersGroup); + groupsByClassId[classId] = prefabPlaceholdersGroup; return prefabPlaceholdersGroup; } - private PrefabPlaceholderAsset GetAndCachePrefabPlaceholderAsset(AssetsBundleManager amInst, string classId) + private IPrefabAsset GetAndCacheAsset(AssetsBundleManager amInst, string classId) { - if (!string.IsNullOrEmpty(classId) && prefabPlaceholderByClassId.TryGetValue(classId, out PrefabPlaceholderAsset cachedPrefabPlaceholder)) + if (string.IsNullOrEmpty(classId)) { - return cachedPrefabPlaceholder; + return default; } - - if (string.IsNullOrEmpty(classId) || !addressableCatalog.TryGetValue(classId, out string[] assetPaths)) + if (groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup)) { - Log.Error($"Could get PrefabPlaceholder with classId: {classId}"); - return null; + 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); - NitroxEntitySlot nitroxEntitySlot = null; + 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 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); @@ -272,7 +323,7 @@ private PrefabPlaceholderAsset GetAndCachePrefabPlaceholderAsset(AssetsBundleMan } PrefabPlaceholderAsset prefabPlaceholderAsset = new(classId, nitroxEntitySlot); - prefabPlaceholderByClassId[classId] = prefabPlaceholderAsset; + placeholdersByClassId[classId] = prefabPlaceholderAsset; return prefabPlaceholderAsset; } diff --git a/NitroxServer-Subnautica/Resources/Parsers/WorldEntityInfoParser.cs b/NitroxServer-Subnautica/Resources/Parsers/WorldEntityInfoParser.cs index bf1eb4ab5a..117423cea5 100644 --- a/NitroxServer-Subnautica/Resources/Parsers/WorldEntityInfoParser.cs +++ b/NitroxServer-Subnautica/Resources/Parsers/WorldEntityInfoParser.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AssetsTools.NET; using AssetsTools.NET.Extra; using NitroxServer_Subnautica.Resources.Parsers.Abstract; @@ -24,7 +24,8 @@ public override Dictionary ParseFile() techType = (TechType)info["techType"].AsInt, slotType = (EntitySlot.Type)info["slotType"].AsInt, prefabZUp = info["prefabZUp"].AsBool, - localScale = info["localScale"].AsVector3() + cellLevel = (LargeWorldEntity.CellLevel)info["cellLevel"].AsInt, + localScale = info["localScale"].ToVector3() }; worldEntitiesByClassId.Add(entityData.classId, entityData); diff --git a/NitroxServer-Subnautica/Resources/ResourceAssets.cs b/NitroxServer-Subnautica/Resources/ResourceAssets.cs index de6d398f78..34a40d5607 100644 --- a/NitroxServer-Subnautica/Resources/ResourceAssets.cs +++ b/NitroxServer-Subnautica/Resources/ResourceAssets.cs @@ -10,7 +10,7 @@ public class ResourceAssets { public Dictionary WorldEntitiesByClassId { get; init; } = new(); public string LootDistributionsJson { get; init; } = ""; - public Dictionary PrefabPlaceholderGroupsByGroupClassId { get; init; } = new(); + public Dictionary PrefabPlaceholdersGroupsByGroupClassId { get; init; } = new(); public RandomStartGenerator NitroxRandom { get; init; } public static void ValidateMembers(ResourceAssets resourceAssets) @@ -18,7 +18,7 @@ public static void ValidateMembers(ResourceAssets resourceAssets) Validate.NotNull(resourceAssets); Validate.IsTrue(resourceAssets.WorldEntitiesByClassId.Count > 0); Validate.IsTrue(resourceAssets.LootDistributionsJson != ""); - Validate.IsTrue(resourceAssets.PrefabPlaceholderGroupsByGroupClassId.Count > 0); + Validate.IsTrue(resourceAssets.PrefabPlaceholdersGroupsByGroupClassId.Count > 0); Validate.NotNull(resourceAssets.NitroxRandom); } } diff --git a/NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs b/NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs index aa57ec707a..3d3c05d9f3 100644 --- a/NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs +++ b/NitroxServer-Subnautica/Resources/ResourceAssetsParser.cs @@ -21,7 +21,7 @@ public static ResourceAssets Parse() { WorldEntitiesByClassId = new WorldEntityInfoParser().ParseFile(), LootDistributionsJson = new EntityDistributionsParser().ParseFile(), - PrefabPlaceholderGroupsByGroupClassId = prefabPlaceholderGroupsParser.ParseFile(), + PrefabPlaceholdersGroupsByGroupClassId = prefabPlaceholderGroupsParser.ParseFile(), NitroxRandom = new RandomStartParser().ParseFile() }; } diff --git a/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs b/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs index f51d6e2aed..ee026c7260 100644 --- a/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs +++ b/NitroxServer-Subnautica/SubnauticaServerAutoFacRegistrar.cs @@ -42,12 +42,12 @@ public override void RegisterDependencies(ContainerBuilder containerBuilder) containerBuilder.Register(c => resourceAssets).SingleInstance(); containerBuilder.Register(c => resourceAssets.WorldEntitiesByClassId).SingleInstance(); - containerBuilder.Register(c => resourceAssets.PrefabPlaceholderGroupsByGroupClassId).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 19cbb075cb..68cbf39e40 100644 --- a/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs +++ b/NitroxServer/GameLogic/Entities/Spawning/BatchEntitySpawner.cs @@ -1,11 +1,10 @@ -using System; using System.Collections.Generic; using System.Linq; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.GameLogic.Entities; using NitroxModel.DataStructures.Unity; -using NitroxModel.DataStructures.Util; +using NitroxServer.GameLogic.Unlockables; using NitroxServer.Helper; using NitroxServer.Resources; using NitroxServer.Serialization; @@ -18,12 +17,13 @@ public class BatchEntitySpawner : IEntitySpawner private readonly Dictionary customBootstrappersByTechType; private readonly HashSet emptyBatches = new(); - private readonly Dictionary prefabPlaceholderGroupsByClassId; - private readonly UwePrefabFactory prefabFactory; + private readonly Dictionary placeholdersGroupsByClassId; + 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(); @@ -57,15 +57,18 @@ public List SerializableParsedBatches } } - public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, UweWorldEntityFactory worldEntityFactory, UwePrefabFactory prefabFactory, List loadedPreviousParsed, ServerProtoBufSerializer serializer, - Dictionary customBootstrappersByTechType, Dictionary prefabPlaceholderGroupsByClassId, string seed) + private static readonly NitroxQuaternion prefabZUpRotation = NitroxQuaternion.FromEuler(new(-90f, 0f, 0f)); + + public BatchEntitySpawner(EntitySpawnPointFactory entitySpawnPointFactory, IUweWorldEntityFactory worldEntityFactory, IUwePrefabFactory prefabFactory, List loadedPreviousParsed, ServerProtoBufSerializer serializer, + Dictionary customBootstrappersByTechType, Dictionary placeholdersGroupsByClassId, PDAStateData pdaStateData, string seed) { parsedBatches = new HashSet(loadedPreviousParsed); this.worldEntityFactory = worldEntityFactory; this.prefabFactory = prefabFactory; this.customBootstrappersByTechType = customBootstrappersByTechType; - this.prefabPlaceholderGroupsByClassId = prefabPlaceholderGroupsByClassId; + this.placeholdersGroupsByClassId = placeholdersGroupsByClassId; this.seed = seed; + this.pdaStateData = pdaStateData; batchCellsParser = new BatchCellsParser(entitySpawnPointFactory, serializer); } @@ -119,59 +122,65 @@ public List LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCrea return entities; } + /// private IEnumerable SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entitySpawnPoint, List prefabs, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null) { - List allowedPrefabs = FilterAllowedPrefabs(prefabs, entitySpawnPoint); - - float rollingProbabilityDensity = allowedPrefabs.Sum(prefab => prefab.Probability / entitySpawnPoint.Density); - - if (rollingProbabilityDensity <= 0) - { - yield break; - } + // See CSVEntitySpawner.GetPrefabForSlot for reference + List allowedPrefabs = FilterAllowedPrefabs(prefabs, entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability); - float randomNumber = (float)deterministicBatchGenerator.NextDouble(); - if (rollingProbabilityDensity > 1f) + bool areFragmentProbabilitiesNonNull = fragmentProbability > 0f && completeFragmentProbability > 0f; + float probabilityMultiplier = areFragmentProbabilitiesNonNull ? (completeFragmentProbability + fragmentProbability) / fragmentProbability : 1f; + float weightedFragmentProbability = 0f; + for (int i = 0; i < allowedPrefabs.Count; i++) { - randomNumber *= rollingProbabilityDensity; + UwePrefab prefab = allowedPrefabs[i]; + if (areFragmentProbabilitiesNonNull && prefab.IsFragment) + { + prefab = prefab with { Probability = prefab.Probability * probabilityMultiplier }; + allowedPrefabs[i] = prefab; + } + weightedFragmentProbability += prefab.Probability; } - float rollingProbability = 0; - - UwePrefab selectedPrefab = allowedPrefabs.FirstOrDefault(prefab => + UwePrefab chosenPrefab = default; + if (weightedFragmentProbability > 0f) { - if (Math.Abs(prefab.Probability) < 0.0001) + float probabilityThreshold = XORRandom.NextFloat(); + if (weightedFragmentProbability > 1f) { - return false; + probabilityThreshold *= weightedFragmentProbability; } + float currentProbability = 0f; + foreach (UwePrefab prefab in allowedPrefabs) + { + currentProbability += prefab.Probability; + if (currentProbability >= probabilityThreshold) + { + chosenPrefab = prefab; + break; + } + } + } - float probabilityDensity = prefab.Probability / entitySpawnPoint.Density; - - rollingProbability += probabilityDensity; - - return rollingProbability >= randomNumber; - }); - - if (selectedPrefab == null) + if (chosenPrefab.Count == 0) { yield break; } - Optional opWorldEntity = worldEntityFactory.From(selectedPrefab.ClassId); - - if (opWorldEntity.HasValue) + if (worldEntityFactory.TryFind(chosenPrefab.ClassId, out UweWorldEntity uweWorldEntity)) { - UweWorldEntity uweWorldEntity = opWorldEntity.Value; - - for (int i = 0; i < selectedPrefab.Count; i++) + for (int i = 0; i < chosenPrefab.Count; i++) { + // Random position in sphere is only possible after first spawn, see EntitySlot.Spawn IEnumerable entities = CreateEntityWithChildren(entitySpawnPoint, - uweWorldEntity.Scale, + chosenPrefab.ClassId, uweWorldEntity.TechType, + uweWorldEntity.PrefabZUp, uweWorldEntity.CellLevel, - selectedPrefab.ClassId, + uweWorldEntity.LocalScale, deterministicBatchGenerator, - parentEntity); + parentEntity, + i > 0); foreach (Entity entity in entities) { yield return entity; @@ -180,18 +189,35 @@ private IEnumerable SpawnEntitiesUsingRandomDistribution(EntitySpawnPoin } } - private List FilterAllowedPrefabs(List prefabs, EntitySpawnPoint entitySpawnPoint) + private List FilterAllowedPrefabs(List prefabs, EntitySpawnPoint entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability) { - List allowedPrefabs = new List(); + List allowedPrefabs = new(); - foreach (UwePrefab prefab in prefabs) + fragmentProbability = 0; + completeFragmentProbability = 0; + for (int i = 0; i < prefabs.Count; i++) { - if (prefab.ClassId != "None") + UwePrefab prefab = prefabs[i]; + // Adapted code from the while loop in CSVEntitySpawner.GetPrefabForSlot + if (prefab.ClassId != "None" && worldEntityFactory.TryFind(prefab.ClassId, out UweWorldEntity uweWorldEntity) && + entitySpawnPoint.AllowedTypes.Contains(uweWorldEntity.SlotType)) { - Optional uweWorldEntity = worldEntityFactory.From(prefab.ClassId); - - if (uweWorldEntity.HasValue && entitySpawnPoint.AllowedTypes.Contains(uweWorldEntity.Value.SlotType)) + float weightedProbability = prefab.Probability / entitySpawnPoint.Density; + if (weightedProbability > 0) { + if (prefab.IsFragment) + { + if (pdaStateData.ScannerComplete.Contains(uweWorldEntity.TechType)) + { + completeFragmentProbability += weightedProbability; + continue; + } + else + { + fragmentProbability += weightedProbability; + } + } + prefab = prefab with { Probability = weightedProbability }; allowedPrefabs.Add(prefab); } } @@ -200,17 +226,21 @@ private List FilterAllowedPrefabs(List prefabs, EntitySpaw return allowedPrefabs; } + /// + /// Spawns the regular (can be children of PrefabPlaceholdersGroup) which are always the same thus context independent. + /// + /// private IEnumerable SpawnEntitiesStaticly(EntitySpawnPoint entitySpawnPoint, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null) { - Optional uweWorldEntity = worldEntityFactory.From(entitySpawnPoint.ClassId); - - if (uweWorldEntity.HasValue) + if (worldEntityFactory.TryFind(entitySpawnPoint.ClassId, out UweWorldEntity uweWorldEntity)) { + // prefabZUp should not be taken into account for statically spawned entities IEnumerable entities = CreateEntityWithChildren(entitySpawnPoint, - entitySpawnPoint.Scale, - uweWorldEntity.Value.TechType, - uweWorldEntity.Value.CellLevel, entitySpawnPoint.ClassId, + uweWorldEntity.TechType, + false, + uweWorldEntity.CellLevel, + entitySpawnPoint.Scale, deterministicBatchGenerator, parentEntity); foreach (Entity entity in entities) @@ -220,15 +250,27 @@ private IEnumerable SpawnEntitiesStaticly(EntitySpawnPoint entitySpawnPo } } - private IEnumerable CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, NitroxVector3 scale, NitroxTechType techType, int cellLevel, string classId, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null) + /// The first entity is a and the following are its children + private IEnumerable CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, string classId, NitroxTechType techType, bool prefabZUp, int cellLevel, NitroxVector3 localScale, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null, bool randomPosition = false) { WorldEntity spawnedEntity; + NitroxVector3 position = entitySpawnPoint.LocalPosition; + NitroxQuaternion rotation = entitySpawnPoint.LocalRotation; + if (prefabZUp) + { + // See EntitySlot.SpawnVirtualEntities use of WorldEntityInfo.prefabZUp + rotation *= prefabZUpRotation; + } + if (randomPosition) + { + position += XORRandom.NextInsideSphere(4f); + } if (classId == CellRootEntity.CLASS_ID) { - spawnedEntity = new CellRootEntity(entitySpawnPoint.LocalPosition, - entitySpawnPoint.LocalRotation, - scale, + spawnedEntity = new CellRootEntity(position, + rotation, + localScale, techType, cellLevel, classId, @@ -237,9 +279,9 @@ private IEnumerable CreateEntityWithChildren(EntitySpawnPoint entitySpaw } else { - spawnedEntity = new WorldEntity(entitySpawnPoint.LocalPosition, - entitySpawnPoint.LocalRotation, - scale, + spawnedEntity = new WorldEntity(position, + rotation, + localScale, techType, cellLevel, classId, @@ -248,6 +290,7 @@ private IEnumerable CreateEntityWithChildren(EntitySpawnPoint entitySpaw parentEntity); } + // See EntitySlotsPlaceholder.Spawn if (!TryCreatePrefabPlaceholdersGroupWithChildren(ref spawnedEntity, classId, deterministicBatchGenerator)) { spawnedEntity.ChildEntities = SpawnEntities(entitySpawnPoint.Children, deterministicBatchGenerator, spawnedEntity); @@ -289,18 +332,25 @@ private IEnumerable AllChildren(Entity entity) private List SpawnEntities(List entitySpawnPoints, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null) { - List entities = new List(); + List entities = new(); foreach (EntitySpawnPoint esp in entitySpawnPoints) { - if (esp.Density > 0) + if (esp is SerializedEntitySpawnPoint serializedEsp) { - List prefabs = prefabFactory.GetPossiblePrefabs(esp.BiomeType); + // We add the cell's coordinate because this entity isn't parented so it needs to know about its global position + NitroxTransform transform = new(serializedEsp.LocalPosition + serializedEsp.AbsoluteEntityCell.Position, serializedEsp.LocalRotation, serializedEsp.Scale); + SerializedWorldEntity entity = new(serializedEsp.SerializedComponents, serializedEsp.Layer, transform, deterministicBatchGenerator.NextId(), parentEntity?.Id, serializedEsp.AbsoluteEntityCell); + entities.Add(entity); + continue; + } - if (prefabs.Count > 0) + if (esp.Density > 0) + { + if (prefabFactory.TryGetPossiblePrefabs(esp.BiomeType, out List prefabs) && prefabs.Count > 0) { entities.AddRange(SpawnEntitiesUsingRandomDistribution(esp, prefabs, deterministicBatchGenerator, parentEntity)); } - else if (esp.ClassId != null) + else if (!string.IsNullOrEmpty(esp.ClassId)) { entities.AddRange(SpawnEntitiesStaticly(esp, deterministicBatchGenerator, parentEntity)); } @@ -318,51 +368,80 @@ private List SpawnEntities(List entitySpawnPoints, Det /// If this Entity is a PrefabPlaceholdersGroup private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity, string classId, DeterministicGenerator deterministicBatchGenerator) { - if (!prefabPlaceholderGroupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset group)) + if (!placeholdersGroupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset groupAsset)) { return false; } - List placeholders = new(group.PrefabPlaceholders.Length); - entity = new PlaceholderGroupWorldEntity(entity, placeholders); + entity = new PlaceholderGroupWorldEntity(entity); - for (int i = 0; i < group.PrefabPlaceholders.Length; i++) + // Adapted from PrefabPlaceholdersGroup.Spawn + for (int i = 0; i < groupAsset.PrefabAssets.Length; i++) { - PrefabPlaceholderAsset placeholder = group.PrefabPlaceholders[i]; - - PrefabChildEntity prefabChild = new(deterministicBatchGenerator.NextId(), null, NitroxTechType.None, i, null, entity.Id); - placeholders.Add(prefabChild); + // Fix positioning of children + IPrefabAsset prefabAsset = groupAsset.PrefabAssets[i]; - if (placeholder.EntitySlot == null) + // 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) { - prefabChild.ChildEntities.Add(new PrefabPlaceholderEntity(deterministicBatchGenerator.NextId(), group.TechType, prefabChild.Id)); + WorldEntity spawnedEntity = SpawnPrefabAssetInEntitySlot(placeholderAsset.Transform, placeholderAsset.EntitySlot, deterministicBatchGenerator, entity.AbsoluteEntityCell, entity); + + if (spawnedEntity != null) + { + // Spawned child will not be of the same type as the current prefabAsset + if (placeholdersGroupsByClassId.ContainsKey(spawnedEntity.ClassId)) + { + spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i); + } + else + { + spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i); + } + entity.ChildEntities.Add(spawnedEntity); + } } else { - Entity entitySlotNullableEntity = SpawnEntitySlotEntities(placeholder.EntitySlot, deterministicBatchGenerator, entity.AbsoluteEntityCell, prefabChild); + // Regular visible GameObject + 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]; + } - if (entitySlotNullableEntity != null) + 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 PrefabPlaceholdersGroupAsset) + { + spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i); + } + else { - prefabChild.ChildEntities.Add(entitySlotNullableEntity); + spawnedEntity = new PrefabPlaceholderEntity(spawnedEntity, i); } + + entity.ChildEntities.Add(spawnedEntity); } } return true; } - private Entity SpawnEntitySlotEntities(NitroxEntitySlot entitySlot, DeterministicGenerator deterministicBatchGenerator, AbsoluteEntityCell cell, PrefabChildEntity parentEntity) + private WorldEntity SpawnPrefabAssetInEntitySlot(NitroxTransform transform, NitroxEntitySlot entitySlot, DeterministicGenerator deterministicBatchGenerator, AbsoluteEntityCell cell, Entity parentEntity) { - List prefabs = prefabFactory.GetPossiblePrefabs(entitySlot.BiomeType); + if (!prefabFactory.TryGetPossiblePrefabs(entitySlot.BiomeType, out List prefabs) || prefabs.Count == 0) + { + return null; + } List entities = new(); - if (prefabs.Count > 0) + EntitySpawnPoint entitySpawnPoint = new(cell, transform.LocalPosition, transform.LocalRotation, entitySlot.AllowedTypes.ToList(), 1f, entitySlot.BiomeType); + entities.AddRange(SpawnEntitiesUsingRandomDistribution(entitySpawnPoint, prefabs, deterministicBatchGenerator, parentEntity)); + if (entities.Count > 0) { - EntitySpawnPoint entitySpawnPoint = new EntitySpawnPoint(cell, NitroxVector3.Zero, NitroxQuaternion.Identity, entitySlot.AllowedTypes.ToList(), 1f, entitySlot.BiomeType); - entities.AddRange(SpawnEntitiesUsingRandomDistribution(entitySpawnPoint, prefabs, deterministicBatchGenerator, parentEntity)); + return (WorldEntity)entities[0]; } - - return entities.FirstOrDefault(); + return null; } - } diff --git a/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs b/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs index d8b05bf123..11bba513a3 100644 --- a/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs +++ b/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPoint.cs @@ -1,46 +1,44 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; -namespace NitroxServer.GameLogic.Entities.Spawning -{ - public class EntitySpawnPoint - { - public readonly List Children = new List(); - - public NitroxVector3 LocalPosition; - - public NitroxQuaternion LocalRotation; - public AbsoluteEntityCell AbsoluteEntityCell { get; } - public NitroxVector3 Scale { get; } - public string ClassId { get; } - public string BiomeType { get; } - public float Density { get; } - public bool CanSpawnCreature { get; private set; } - public List AllowedTypes { get; } - - public EntitySpawnPoint Parent { get; set; } +namespace NitroxServer.GameLogic.Entities.Spawning; - public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, List allowedTypes, float density, string biomeType) - { - AbsoluteEntityCell = absoluteEntityCell; - LocalPosition = localPosition; - LocalRotation = localRotation; - BiomeType = biomeType; - Density = density; - AllowedTypes = allowedTypes; - } +public class EntitySpawnPoint +{ + // Fields from EntitySlotData + public string BiomeType { get; } + public List AllowedTypes { get; } + public float Density { get; } + public NitroxVector3 LocalPosition { get; set; } + public NitroxQuaternion LocalRotation { get; set; } + + public readonly List Children = new List(); + public AbsoluteEntityCell AbsoluteEntityCell { get; } + public NitroxVector3 Scale { get; protected set; } + public string ClassId { get; } + public bool CanSpawnCreature { get; private set; } + public EntitySpawnPoint Parent { get; set; } - public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 scale, string classId) - { - AbsoluteEntityCell = absoluteEntityCell; - ClassId = classId; - Density = 1; - LocalPosition = localPosition; - Scale = scale; - LocalRotation = localRotation; - } + public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, List allowedTypes, float density, string biomeType) + { + AbsoluteEntityCell = absoluteEntityCell; + LocalPosition = localPosition; + LocalRotation = localRotation; + BiomeType = biomeType; + Density = density; + AllowedTypes = allowedTypes; + } - public override string ToString() => $"[EntitySpawnPoint - {AbsoluteEntityCell}, Local Position: {LocalPosition}, Local Rotation: {LocalRotation}, Scale: {Scale}, Class Id: {ClassId}, Biome Type: {BiomeType}, Density: {Density}, Can Spawn Creature: {CanSpawnCreature}]"; + public EntitySpawnPoint(AbsoluteEntityCell absoluteEntityCell, NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 scale, string classId) + { + AbsoluteEntityCell = absoluteEntityCell; + ClassId = classId; + Density = 1; + LocalPosition = localPosition; + Scale = scale; + LocalRotation = localRotation; } + + public override string ToString() => $"[EntitySpawnPoint - {AbsoluteEntityCell}, Local Position: {LocalPosition}, Local Rotation: {LocalRotation}, Scale: {Scale}, Class Id: {ClassId}, Biome Type: {BiomeType}, Density: {Density}, Can Spawn Creature: {CanSpawnCreature}]"; } diff --git a/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPointFactory.cs b/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPointFactory.cs index 21fe1fabc4..d7799917a5 100644 --- a/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPointFactory.cs +++ b/NitroxServer/GameLogic/Entities/Spawning/EntitySpawnPointFactory.cs @@ -1,13 +1,11 @@ - using System.Collections.Generic; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; using NitroxServer.UnityStubs; -namespace NitroxServer.GameLogic.Entities.Spawning +namespace NitroxServer.GameLogic.Entities.Spawning; + +public abstract class EntitySpawnPointFactory { - public abstract class EntitySpawnPointFactory - { - public abstract List From(AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform, GameObject gameObject); - } + public abstract List From(AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform, GameObject gameObject); } diff --git a/NitroxServer/GameLogic/Entities/Spawning/SerializedEntitySpawnPoint.cs b/NitroxServer/GameLogic/Entities/Spawning/SerializedEntitySpawnPoint.cs new file mode 100644 index 0000000000..59c5700875 --- /dev/null +++ b/NitroxServer/GameLogic/Entities/Spawning/SerializedEntitySpawnPoint.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NitroxModel.DataStructures; +using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.GameLogic.Entities; +using NitroxModel.DataStructures.Unity; + +namespace NitroxServer.GameLogic.Entities.Spawning; + +/// +/// Specific type of for spawning +/// +public class SerializedEntitySpawnPoint : EntitySpawnPoint +{ + public List SerializedComponents { get; } + public int Layer { get; } + + public SerializedEntitySpawnPoint(List serializedComponents, int layer, AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform) : base(absoluteEntityCell, transform.LocalPosition, transform.LocalRotation, null, 1, null) + { + SerializedComponents = serializedComponents; + Layer = layer; + Scale = transform.LocalScale; + } +} diff --git a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs index 53d4ed66e1..3e7e4b7035 100644 --- a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs +++ b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs @@ -259,6 +259,8 @@ public int LoadUnspawnedEntities(NitroxInt3 batchId, bool suppressLogs) cellRoot.ChildEntities = new List(); } + // Specific type of entities which is not parented to a CellRootEntity + nonCellRootEntities.AddRange(spawnedEntities.OfType()); entityRegistry.AddEntitiesIgnoringDuplicate(nonCellRootEntities.OfType().ToList()); diff --git a/NitroxServer/Helper/XORRandom.cs b/NitroxServer/Helper/XORRandom.cs new file mode 100644 index 0000000000..f5d6173ebe --- /dev/null +++ b/NitroxServer/Helper/XORRandom.cs @@ -0,0 +1,142 @@ +using NitroxModel.DataStructures.Unity; + +namespace NitroxServer.Helper; + +/// +/// Stolen from +/// Aims at replicating UnityEngine.Random's implementation which is really uncommon because its uniform. +/// +public static class XORRandom +{ + private static uint x; + private static uint y; + private static uint z; + private static uint w; + + private const uint MT19937 = 1812433253; + + /// + /// Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from Mersenne Twister (MT19937) + /// https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization + /// + public static void InitSeed(int seed) + { + x = (uint)seed; + y = (MT19937 * x + 1); + z = (MT19937 * y + 1); + w = (MT19937 * z + 1); + } + + /// + /// Explicitly set the state parameters + /// + public static void InitState(uint initial_x, uint initial_y, uint initial_z, uint initial_w) + { + x = initial_x; + y = initial_y; + z = initial_x; + w = initial_w; + } + + public static uint XORShift() + { + uint t = x ^ (x << 11); + x = y; y = z; z = w; + return w = w ^ (w >> 19) ^ t ^ (t >> 8); + } + + + /// + /// Alias of base Next/XORShift. + /// UnityEngine.Random doesn't have any uint functions so these functions behave exactly like int Random.Range + /// + public static uint NextUInt() + { + return XORShift(); + } + + /// + /// A random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive) + /// + public static uint NextUIntMax(uint max) + { + if (max == 0) return 0; + return XORShift() % max; + } + + /// + /// A random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive) + /// + public static uint NextUIntRange(uint min, uint max) + { + if (max - min == 0) return min; + + if (max < min) + return min - XORShift() % (max + min); + else + return min + XORShift() % (max - min); + } + + /// + /// A random signed 32-bit integer value in the range -2,147,483,648 (inclusive) to 2,147,483,647 (inclusive) + /// + public static int NextInt() + { + return (int)(XORShift() % int.MaxValue); + } + + public static int NextIntMax(int max) + { + return NextInt() % max; + } + + /// + /// A random signed 32-bit integer value in the range min (inclusive) to max (exclusive) + /// + /// + /// If you only need to generate positive integers, use NextUIntRange instead + /// + public static int NextIntRange(int min, int max) + { + // If max and min are the same, just return min since it will result in a DivideByZeroException + if (max - min == 0) return min; + + // Do operations in Int64 to prevent overflow that might be caused by any of the following operations + // I'm sure there's a faster/better way to do this and avoid casting, but we prefer equivalence to Unity over performance + long minLong = min; + long maxLong = max; + long r = XORShift(); + + // Flip the first operator if the max is lower than the min, + if (max < min) + { + return (int)(minLong - r % (maxLong - minLong)); + } + else + { + return (int)(minLong + r % (maxLong - minLong)); + } + } + + /// + /// A random floating point between 0.0 and 1.0 (inclusive?) + /// + public static float NextFloat() + { + return 1.0f - NextFloatRange(0.0f, 1.0f); + } + + /// + /// A random floating point between min (inclusive) and max (exclusive) + /// + public static float NextFloatRange(float min, float max) + { + return (min - max) * ((float)(XORShift() << 9) / 0xFFFFFFFF) + max; + } + + public static NitroxVector3 NextInsideSphere(float radius = 1f) + { + NitroxVector3 pointInUnitSphere = new NitroxVector3(NextFloat(), NextFloat(), NextFloat()).Normalized; + return pointInUnitSphere * NextFloatRange(0, radius); + } +} diff --git a/NitroxServer/Resources/IPrefabAsset.cs b/NitroxServer/Resources/IPrefabAsset.cs new file mode 100644 index 0000000000..df0195eb2e --- /dev/null +++ b/NitroxServer/Resources/IPrefabAsset.cs @@ -0,0 +1,9 @@ +using NitroxModel.DataStructures.Unity; + +namespace NitroxServer.Resources; + +public interface IPrefabAsset +{ + public NitroxTransform Transform { get; set; } + public string ClassId { get; } +} diff --git a/NitroxServer/Resources/PrefabPlaceholderAsset.cs b/NitroxServer/Resources/PrefabPlaceholderAsset.cs index d159cb3ded..88c2186731 100644 --- a/NitroxServer/Resources/PrefabPlaceholderAsset.cs +++ b/NitroxServer/Resources/PrefabPlaceholderAsset.cs @@ -1,16 +1,14 @@ +using NitroxModel.DataStructures.Unity; using NitroxServer.GameLogic.Entities; namespace NitroxServer.Resources; -public class PrefabPlaceholderAsset +public record struct PrefabPlaceholderAsset(string ClassId, NitroxEntitySlot EntitySlot = null, NitroxTransform Transform = null) : IPrefabAsset { - public string ClassId { get; } - - public NitroxEntitySlot EntitySlot { get; } - - public PrefabPlaceholderAsset(string classId, NitroxEntitySlot entitySlot) - { - ClassId = classId; - EntitySlot = entitySlot; - } + 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. + /// + public NitroxEntitySlot EntitySlot { get; } = EntitySlot; } 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 d09c09b060..2da374e8b3 100644 --- a/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs +++ b/NitroxServer/Resources/PrefabPlaceholdersGroupAsset.cs @@ -1,19 +1,11 @@ -using NitroxModel.DataStructures.GameLogic; +using NitroxModel.DataStructures.Unity; namespace NitroxServer.Resources; -public class PrefabPlaceholdersGroupAsset +/// +/// All attached PrefabPlaceholders (and PrefabPlaceholdersGroup). Is in sync with PrefabPlaceholdersGroup.prefabPlaceholders +/// +public record struct PrefabPlaceholdersGroupAsset(string ClassId, IPrefabAsset[] PrefabAssets, NitroxTransform Transform = null) : IPrefabAsset { - /// - /// All attached PrefabPlaceholders. Is in sync with PrefabPlaceholdersGroup.prefabPlaceholders - /// - public PrefabPlaceholderAsset[] PrefabPlaceholders { get; } - - public NitroxTechType TechType { get;} - - public PrefabPlaceholdersGroupAsset(PrefabPlaceholderAsset[] prefabPlaceholders, NitroxTechType techType) - { - PrefabPlaceholders = prefabPlaceholders; - TechType = techType; - } + public NitroxTransform Transform { get; set; } = Transform; } diff --git a/NitroxServer/Serialization/BatchCellsParser.cs b/NitroxServer/Serialization/BatchCellsParser.cs index 800b3233b0..766978e565 100644 --- a/NitroxServer/Serialization/BatchCellsParser.cs +++ b/NitroxServer/Serialization/BatchCellsParser.cs @@ -6,7 +6,6 @@ using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; -using NitroxModel.Discovery; using NitroxModel.Helper; using NitroxServer.GameLogic.Entities.Spawning; using NitroxServer.UnityStubs; @@ -83,7 +82,6 @@ private void ParseCacheCells(NitroxInt3 batchId, string fileName, List(stream); - byte[] serialData = new byte[cellHeader.DataLength]; stream.Read(serialData, 0, cellHeader.DataLength); ParseGameObjectsWithHeader(serialData, batchId, cellHeader.CellId, cellHeader.Level, spawnPoints, out bool wasLegacy); @@ -92,7 +90,7 @@ private void ParseCacheCells(NitroxInt3 batchId, string fileName, List 0) + // If it is an "Empty" GameObject, we need it to have serialized components + if (!gameObject.CreateEmptyObject || gameObject.SerializedComponents.Count > 0) { - AbsoluteEntityCell absoluteEntityCell = new AbsoluteEntityCell(batchId, cellId, level); NitroxTransform transform = gameObject.GetComponent(); spawnPoints.AddRange(entitySpawnPointFactory.From(absoluteEntityCell, transform, gameObject)); @@ -147,16 +146,12 @@ private void ParseGameObjectsFromStream(Stream stream, NitroxInt3 batchId, Nitro private GameObject DeserializeGameObject(Stream stream) { - GameObjectData goData = serializer.Deserialize(stream); - - GameObject gameObject = new GameObject(goData); - DeserializeComponents(stream, gameObject); - - return gameObject; + return new(serializer.Deserialize(stream)); } private void DeserializeComponents(Stream stream, GameObject gameObject) { + gameObject.SerializedComponents.Clear(); LoopHeader components = serializer.Deserialize(stream); for (int componentCounter = 0; componentCounter < components.Count; componentCounter++) @@ -173,9 +168,21 @@ private void DeserializeComponents(Stream stream, GameObject gameObject) Validate.NotNull(type, $"No type or surrogate found for {componentHeader.TypeName}!"); object component = FormatterServices.GetUninitializedObject(type); + long startPosition = stream.Position; serializer.Deserialize(stream, component, type); gameObject.AddComponent(component, type); + // SerializedComponents only matter if this is an "Empty" GameObject + if (gameObject.CreateEmptyObject && !type.Name.Equals("NitroxTransform") && !type.Name.Equals("LargeWorldEntity")) + { + int length = (int)(stream.Position - startPosition); + byte[] data = new byte[length]; + stream.Position = startPosition; + stream.Read(data, 0, length); + SerializedComponent serializedComponent = new(componentHeader.TypeName, componentHeader.IsEnabled, data); + gameObject.SerializedComponents.Add(serializedComponent); + } + } } } @@ -239,6 +246,8 @@ public override string ToString() [ProtoMember(5)] public int WaiterDataLength; + + // There's no point in spawning allowSpawnRestrictions as SpawnRestrictionEnforcer doesn't load any restrictions } [ProtoContract] diff --git a/NitroxServer/Serialization/World/WorldPersistence.cs b/NitroxServer/Serialization/World/WorldPersistence.cs index 62ce10e88d..afc1cce4b8 100644 --- a/NitroxServer/Serialization/World/WorldPersistence.cs +++ b/NitroxServer/Serialization/World/WorldPersistence.cs @@ -16,6 +16,7 @@ using NitroxServer.GameLogic.Entities.Spawning; using NitroxServer.GameLogic.Players; using NitroxServer.GameLogic.Unlockables; +using NitroxServer.Helper; using NitroxServer.Resources; using NitroxServer.Serialization.Upgrade; @@ -189,6 +190,8 @@ public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode) seed = StringHelper.GenerateRandomString(10); #endif } + // Initialized only once, just like UnityEngine.Random + XORRandom.InitSeed(seed.GetHashCode()); Log.Info($"Loading world with seed {seed}"); @@ -216,12 +219,13 @@ 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>(), NitroxServiceLocator.LocateService>(), + pWorldData.WorldData.GameData.PDAState, world.Seed ); diff --git a/NitroxServer/UnityStubs/GameObject.cs b/NitroxServer/UnityStubs/GameObject.cs index a78e4eb36d..1f93071f0f 100644 --- a/NitroxServer/UnityStubs/GameObject.cs +++ b/NitroxServer/UnityStubs/GameObject.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using NitroxModel.DataStructures; using NitroxModel.DataStructures.Unity; using NitroxServer.Serialization; @@ -7,6 +8,7 @@ namespace NitroxServer.UnityStubs { public class GameObject { + public bool CreateEmptyObject { get; } public bool IsActive { get; } public int Layer { get; } public string Tag { get; } @@ -17,9 +19,11 @@ public class GameObject public int TotalComponents => components.Count; private readonly Dictionary components = new Dictionary(); + public readonly List SerializedComponents = new(); public GameObject(GameObjectData goData) { + CreateEmptyObject = goData.CreateEmptyObject; IsActive = goData.IsActive; Layer = goData.Layer; Tag = goData.Tag;