Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework server-side entity spawning #2093

Merged
merged 6 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}");
}
}
5 changes: 3 additions & 2 deletions Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,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 Down Expand Up @@ -420,7 +421,7 @@ private static void EntityTest(Entity entity, Entity entityAfter)
Assert.AreEqual(prefabChildEntity.ClassId, prefabChildEntityAfter.ClassId);
break;
case PrefabPlaceholderEntity prefabPlaceholderEntity when entityAfter is PrefabPlaceholderEntity prefabPlaceholderEntityAfter:
Assert.AreEqual(prefabPlaceholderEntity.ClassId, prefabPlaceholderEntityAfter.ClassId);
Assert.AreEqual(prefabPlaceholderEntity.ComponentIndex, prefabPlaceholderEntityAfter.ComponentIndex);
break;
case InventoryEntity inventoryEntity when entityAfter is InventoryEntity inventoryEntityAfter:
Assert.AreEqual(inventoryEntity.ComponentIndex, inventoryEntityAfter.ComponentIndex);
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);
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
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,69 +48,41 @@ 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)
{
continue;
}

PrefabPlaceholder prefabPlaceholder = prefabPlaceholderGroup.prefabPlaceholders[placeholderSlot.ComponentIndex];
Entity slotEntity = placeholderSlot.ChildEntities[0];

switch (slotEntity)
case PrefabPlaceholderEntity prefabEntity:
if (!prefabPlaceholderEntitySpawner.SpawnSync(prefabEntity, groupObject, cellRoot, childResult))
{
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;
yield return prefabPlaceholderEntitySpawner.SpawnAsync(prefabEntity, groupObject, cellRoot, childResult);
}
break;

// Other layers under PlaceholderGroupWorldEntity's children
case WorldEntity worldEntity:
Optional<GameObject> slotParent = parentById[worldEntity.ParentId];
case PlaceholderGroupWorldEntity groupEntity:
PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex];
yield return SpawnAsync(groupEntity, placeholder.transform.parent.gameObject, cellRoot, childResult);
break;

if (!SpawnWorldEntityChildSync(worldEntity, cellRoot, slotParent, childResult, out asyncInstructions))
default:
if (!SpawnWorldEntityChildSync(entity, cellRoot, parentById.GetOrDefault(current.ParentId, null), childResult, out IEnumerator asyncInstructions))
{
yield return asyncInstructions;
}
Expand All @@ -116,72 +95,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 +142,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Collections;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;

namespace NitroxClient.GameLogic.Spawning.WorldEntities;

public class PrefabPlaceholderEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private readonly DefaultWorldEntitySpawner defaultEntitySpawner;

public PrefabPlaceholderEntitySpawner(DefaultWorldEntitySpawner defaultEntitySpawner)
{
this.defaultEntitySpawner = defaultEntitySpawner;
}

// TODO: Clean the spawners (move to to Setup() and IsValidOrError() after rebase)
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not PrefabPlaceholderEntity prefabEntity)
{
yield break;
}
if (!parent.Value || !parent.Value.TryGetComponent(out PrefabPlaceholdersGroup group))
{
Log.Error($"[{nameof(PrefabPlaceholderEntity)}] Can't find a {nameof(PrefabPlaceholdersGroup)} on parent for {entity.Id}");
yield break;
}
PrefabPlaceholder placeholder = group.prefabPlaceholders[prefabEntity.ComponentIndex];
yield return defaultEntitySpawner.SpawnAsync(entity, placeholder.transform.parent.gameObject, cellRoot, result);
if (!result.value.HasValue)
{
yield break;
}
SetupObject(entity, result.value.Value);
}
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved

public bool SpawnsOwnChildren() => false;

public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not PrefabPlaceholderEntity prefabEntity)
{
return true;
}
if (!parent.Value || !parent.Value.TryGetComponent(out PrefabPlaceholdersGroup group))
{
Log.Error($"[{nameof(PrefabPlaceholderEntity)}] Can't find a {nameof(PrefabPlaceholdersGroup)} on parent for {entity.Id}");
return true;
}
PrefabPlaceholder placeholder = group.prefabPlaceholders[prefabEntity.ComponentIndex];
if (!defaultEntitySpawner.SpawnSync(entity, placeholder.transform.parent.gameObject, cellRoot, result))
{
return false;
}
SetupObject(entity, result.value.Value);
return true;
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
}

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