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 1 commit
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 @@ -328,7 +328,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 @@ -398,7 +399,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
4 changes: 4 additions & 0 deletions NitroxClient/Debuggers/Drawer/Unity/TransformDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ private void DrawTransform(Transform transform)
GameObject.Destroy(transform.gameObject);
}
}
if (GUILayout.Button("Goto", GUILayout.MaxWidth(75)) && Player.main)
{
Player.main.SetPosition(transform.position);
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, E
{
CrashHome crashHome = parent.Value.GetComponent<CrashHome>();

GameObject gameObject = Object.Instantiate(crashHome.crashPrefab, Vector3.zero, Quaternion.Euler(-90f, 0f, 0f));
GameObject gameObject = Object.Instantiate(crashHome.crashPrefab, Vector3.zero, Quaternion.identity);
gameObject.transform.SetParent(crashHome.transform, false);
crashHome.crash = gameObject.GetComponent<Crash>();
crashHome.spawnTime = -1;
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 NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
Expand All @@ -19,17 +17,24 @@ namespace NitroxClient.GameLogic.Spawning.WorldEntities;
/// </remarks>
public class PlaceholderGroupWorldEntitySpawner : IWorldEntitySpawner
{
private readonly Entities entities;
private readonly WorldEntitySpawnerResolver spawnerResolver;
private readonly DefaultWorldEntitySpawner defaultSpawner;

public PlaceholderGroupWorldEntitySpawner(WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner)
public PlaceholderGroupWorldEntitySpawner(Entities entities, WorldEntitySpawnerResolver spawnerResolver, DefaultWorldEntitySpawner defaultSpawner)
{
this.entities = entities;
this.spawnerResolver = spawnerResolver;
this.defaultSpawner = defaultSpawner;
}

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 @@ -40,72 +45,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)
case PrefabPlaceholderEntity prefabEntity:
PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[prefabEntity.ComponentIndex];
if (!SpawnWorldEntityChildSync(prefabEntity, cellRoot, placeholder.transform.parent.gameObject, childResult, out IEnumerator asyncInstructions))
{
continue;
}

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;
yield return asyncInstructions;
}
break;

// Other layers under PlaceholderGroupWorldEntity's children
case WorldEntity worldEntity:
Optional<GameObject> slotParent = parentById[worldEntity.ParentId];

if (!SpawnWorldEntityChildSync(worldEntity, cellRoot, slotParent, childResult, out asyncInstructions))
{
yield return asyncInstructions;
}
case PlaceholderGroupWorldEntity groupEntity:
placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex];
yield return SpawnAsync(groupEntity, placeholder.transform.parent.gameObject, cellRoot, childResult);
break;
default:
Log.Error($"[{nameof(PlaceholderGroupWorldEntitySpawner)}] Found a child entity to spawn with an unmanaged type: {entity.GetType()}");
break;
}

Expand All @@ -115,73 +89,43 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, E
continue;
}

EntityMetadataProcessor.ApplyMetadata(childResult.value.Value, current.Metadata);
GameObject childObject = childResult.value.Value;
entities.MarkAsSpawned(current);
parentById[current.Id] = childObject;
EntityMetadataProcessor.ApplyMetadata(childObject, current.Metadata);
if (current is not PlaceholderGroupWorldEntity)
{
continue;
}
NitroxEntity.SetNewId(childObject, current.Id);
// Adding children to be spawned by this loop
foreach (WorldEntity slotEntityChild in current.ChildEntities.OfType<WorldEntity>())
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, goResult);

if (goResult.value)
{
SetupPlaceholder(goResult.value, prefabPlaceholder, entity, result);
}
}

private bool SpawnChildPlaceholderSync(PrefabPlaceholder prefabPlaceholder, PrefabPlaceholderEntity entity, TaskResult<Optional<GameObject>> result, out IEnumerator asyncInstructions)
{
if (!DefaultWorldEntitySpawner.TryCreateGameObjectSync(TechType.None, prefabPlaceholder.prefabClassId, out GameObject gameObject))
{
asyncInstructions = SpawnChildPlaceholderAsync(prefabPlaceholder, entity, result);
return false;
}

SetupPlaceholder(gameObject, prefabPlaceholder, entity, result);
asyncInstructions = null;
return true;
result.Set(prefabPlaceholderGroupGameObject);
}

private void SetupPlaceholder(GameObject gameObject, PrefabPlaceholder prefabPlaceholder, PrefabPlaceholderEntity entity, TaskResult<Optional<GameObject>> result)
{
try
{
gameObject.transform.SetParent(prefabPlaceholder.transform.parent, false);
gameObject.transform.localPosition = prefabPlaceholder.transform.localPosition;
gameObject.transform.localRotation = prefabPlaceholder.transform.localRotation;

NitroxEntity.SetNewId(gameObject, entity.Id);
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 +136,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
Expand Up @@ -22,7 +22,7 @@ public WorldEntitySpawnerResolver(PlayerManager playerManager, ILocalNitroxPlaye
customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner();

vehicleWorldEntitySpawner = new(entities);
prefabWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(this, defaultEntitySpawner);
prefabWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(entities, this, defaultEntitySpawner);
playerWorldEntitySpawner = new PlayerWorldEntitySpawner(playerManager, localPlayer);
}

Expand Down
Loading