Skip to content

Commit

Permalink
Rework server-side entity spawning (#2093)
Browse files Browse the repository at this point in the history
  • Loading branch information
dartasen authored Jan 1, 2024
2 parents 3561d31 + 2af9c87 commit 8abb67b
Show file tree
Hide file tree
Showing 49 changed files with 1,179 additions and 540 deletions.
24 changes: 24 additions & 0 deletions Nitrox.Test/Server/Helper/XORRandomTest.cs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
17 changes: 13 additions & 4 deletions Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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))
{
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubRoot>(true);
Player.main.SetCurrentSub(subRoot, true);
Player.main.SetPosition(transform.position);
}
}
}
}
Expand Down
16 changes: 14 additions & 2 deletions NitroxClient/GameLogic/Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Entity> entitiesToEnqueue)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,19 +16,28 @@ namespace NitroxClient.GameLogic.Spawning.WorldEntities;
/// </remarks>
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<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not PlaceholderGroupWorldEntity placeholderGroupEntity)
{
yield break;
}

TaskResult<Optional<GameObject>> prefabPlaceholderGroupTaskResult = new();
if (!defaultSpawner.SpawnSync(entity, parent, cellRoot, prefabPlaceholderGroupTaskResult))
{
Expand All @@ -41,73 +48,49 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> 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>();
PrefabPlaceholdersGroup prefabPlaceholderGroup = groupObject.GetComponent<PrefabPlaceholdersGroup>();

// Spawning all children iteratively
Stack<Entity> stack = new(placeholderGroupEntity.ChildEntities.OfType<PrefabChildEntity>());
Stack<Entity> stack = new(placeholderGroupEntity.ChildEntities);

TaskResult<Optional<GameObject>> childResult = new();
Dictionary<NitroxId, Optional<GameObject>> parentById = new();
IEnumerator asyncInstructions;
Dictionary<NitroxId, GameObject> 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<GameObject> 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)
Expand All @@ -116,72 +99,43 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> 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<WorldEntity>())
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<Optional<GameObject>> result)
{
TaskResult<GameObject> 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<Optional<GameObject>> 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<Optional<GameObject>> 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<GameObject> parent, TaskResult<Optional<GameObject>> worldEntityResult)
private IEnumerator SpawnWorldEntityChildAsync(WorldEntity worldEntity, EntityCell cellRoot, GameObject parent, TaskResult<Optional<GameObject>> 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<GameObject> parent, TaskResult<Optional<GameObject>> worldEntityResult, out IEnumerator asyncInstructions)
private bool SpawnWorldEntityChildSync(WorldEntity worldEntity, EntityCell cellRoot, GameObject parent, TaskResult<Optional<GameObject>> worldEntityResult, out IEnumerator asyncInstructions)
{
IWorldEntitySpawner spawner = spawnerResolver.ResolveEntitySpawner(worldEntity);

Expand All @@ -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;
}
Expand Down
Loading

0 comments on commit 8abb67b

Please sign in to comment.