first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
using System.Collections;
using NitroxClient.GameLogic.Spawning.Metadata.Processor;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class CrashEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(WorldEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject, parent.Value, out Crash crash, out CrashHome crashHome))
{
yield break;
}
SetupObject(entity, crash, crashHome);
result.Set(gameObject);
}
public bool SpawnsOwnChildren() => false;
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject, parent.Value, out Crash crash, out CrashHome crashHome))
{
return true;
}
SetupObject(entity, crash, crashHome);
result.Set(gameObject);
return true;
}
private static bool VerifyCanSpawnOrError(WorldEntity entity, GameObject prefabObject, GameObject parentObject, out Crash crash, out CrashHome crashHome)
{
if (!prefabObject.TryGetComponent(out crash))
{
Log.Error($"Couldn't find component {nameof(Crash)} on prefab with ClassId: {entity.ClassId}");
crashHome = null;
return false;
}
if (parentObject && parentObject.TryGetComponent(out crashHome))
{
return true;
}
crashHome = null;
Log.Error($"Couldn't find a valid parent for {entity}");
return false;
}
private static void SetupObject(WorldEntity worldEntity, Crash crash, CrashHome crashHome)
{
crash.transform.SetPositionAndRotation(worldEntity.Transform.Position.ToUnity(), worldEntity.Transform.Rotation.ToUnity());
crash.transform.localScale = worldEntity.Transform.LocalScale.ToUnity();
crashHome.crash = crash;
CrashHomeMetadataProcessor.UpdateCrashHomeOpen(crashHome);
LargeWorldStreamer.main.MakeEntityTransient(crash.gameObject);
}
}

View File

@@ -0,0 +1,157 @@
using System.Collections;
using NitroxClient.GameLogic.Simulation;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class CreatureRespawnEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private readonly SimulationOwnership simulationOwnership;
public CreatureRespawnEntitySpawner(SimulationOwnership simulationOwnership)
{
this.simulationOwnership = simulationOwnership;
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not CreatureRespawnEntity creatureRespawnEntity)
{
yield break;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(OxygenPipeEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(creatureRespawnEntity, gameObject, out Respawn respawn))
{
yield break;
}
SetupObject(creatureRespawnEntity, gameObject, respawn);
result.Set(gameObject);
}
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not CreatureRespawnEntity creatureRespawnEntity)
{
return true;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(creatureRespawnEntity, gameObject, out Respawn respawn))
{
return true;
}
SetupObject(creatureRespawnEntity, gameObject, respawn);
result.Set(gameObject);
return true;
}
public bool SpawnsOwnChildren() => false;
private bool VerifyCanSpawnOrError(CreatureRespawnEntity entity, GameObject gameObject, out Respawn respawn)
{
// Respawn's logic only work during their Start method so we'll either execute it directly when spawning this entity, or destroy it
if (DayNightCycle.main.timePassedAsFloat < entity.SpawnTime)
{
GameObject.Destroy(gameObject);
respawn = null;
return false;
}
if (gameObject.TryGetComponent(out respawn))
{
return true;
}
Log.Error($"Could not find component {nameof(Respawn)} on prefab with ClassId: {entity.ClassId}");
return false;
}
private void SetupObject(CreatureRespawnEntity entity, GameObject gameObject, Respawn respawn)
{
RespawnContext context = new() { Entity = entity, GameObject = gameObject, Respawn = respawn };
LockRequest<RespawnContext> lockRequest = new(entity.Id, SimulationLockType.TRANSIENT, TriggerRespawnCallback, context);
simulationOwnership.RequestSimulationLock(lockRequest);
}
private static void TriggerRespawnCallback(NitroxId entityId, bool acquired, RespawnContext context)
{
if (!acquired)
{
GameObject.Destroy(context.GameObject);
return;
}
GameObject gameObject = context.GameObject;
CreatureRespawnEntity entity = context.Entity;
Respawn respawn = context.Respawn;
// This will only happen if the respawn is ready to be activated
Transform transform = gameObject.transform;
transform.localPosition = entity.Transform.Position.ToUnity();
transform.localRotation = entity.Transform.Rotation.ToUnity();
transform.localScale = entity.Transform.LocalScale.ToUnity();
// It's possible that either the respawn was parented to something (e.g. a Reefback) or directly to a cell
if (entity.ParentId != null)
{
if (LargeWorldStreamer.main)
{
LargeWorldStreamer.main.cellManager.UnregisterEntity(gameObject);
}
if (NitroxEntity.TryGetComponentFrom(entity.ParentId, out Transform parent))
{
transform.parent = parent;
}
}
else if (gameObject.TryGetComponent(out LargeWorldEntity largeWorldEntity))
{
largeWorldEntity.cellLevel = (LargeWorldEntity.CellLevel)entity.Level;
if (LargeWorldStreamer.main)
{
LargeWorldStreamer.main.cellManager.RegisterEntity(gameObject);
}
}
respawn.spawnTime = entity.SpawnTime;
respawn.techType = entity.RespawnTechType.ToUnity();
respawn.addComponents.Clear();
respawn.addComponents.AddRange(entity.AddComponents);
gameObject.SetActive(true);
}
internal class RespawnContext : LockRequestContext
{
public CreatureRespawnEntity Entity;
public GameObject GameObject;
public Respawn Respawn;
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class CreepvineEntitySpawner(DefaultWorldEntitySpawner defaultWorldEntitySpawner) : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private readonly DefaultWorldEntitySpawner defaultWorldEntitySpawner = defaultWorldEntitySpawner;
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
yield return defaultWorldEntitySpawner.SpawnAsync(entity, parent, cellRoot, result);
if (!result.value.HasValue)
{
yield break;
}
SetupObject(result.value.Value);
// result is already set by defaultWorldEntitySpawner.SpawnAsync
}
public bool SpawnsOwnChildren() => false;
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (!defaultWorldEntitySpawner.SpawnSync(entity, parent, cellRoot, result))
{
return false;
}
SetupObject(result.value.Value);
// result is already set
return true;
}
private static void SetupObject(GameObject gameObject)
{
if (gameObject.GetComponent<FruitPlant>())
{
return;
}
FruitPlant fruitPlant = gameObject.AddComponent<FruitPlant>();
fruitPlant.fruitSpawnEnabled = false;
fruitPlant.timeNextFruit = -1;
fruitPlant.fruits = gameObject.GetComponentsInChildren<PickPrefab>(true);
}
}

View File

@@ -0,0 +1,198 @@
using System.Collections;
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using UWE;
using static NitroxClient.Unity.Helper.GameObjectHelper;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class DefaultWorldEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private static readonly Dictionary<TechType, GameObject> prefabCacheByTechType = [];
private static readonly Dictionary<string, GameObject> prefabCacheByClassId = [];
private static readonly HashSet<(string, TechType)> prefabNotFound = [];
private static readonly HashSet<string> classIdsWithoutPrefab = [];
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
TechType techType = entity.TechType.ToUnity();
TaskResult<GameObject> gameObjectResult = new();
yield return CreateGameObject(techType, entity.ClassId, entity.Id, gameObjectResult);
GameObject gameObject = gameObjectResult.Get();
SetupObject(entity, parent, gameObject, cellRoot, techType);
result.Set(Optional.Of(gameObject));
}
private void SetupObject(WorldEntity entity, Optional<GameObject> parent, GameObject gameObject, EntityCell cellRoot, TechType techType)
{
gameObject.transform.position = entity.Transform.Position.ToUnity();
gameObject.transform.rotation = entity.Transform.Rotation.ToUnity();
gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity();
CrafterLogic.NotifyCraftEnd(gameObject, techType);
WaterPark parentWaterPark = null;
if (parent.HasValue)
{
Items.TryGetParentWaterPark(parent.Value.transform.parent, out parentWaterPark);
}
if (!parentWaterPark)
{
if (parent.HasValue && !parent.Value.GetComponent<LargeWorldEntityCell>())
{
LargeWorldEntity.Register(gameObject); // This calls SetActive on the GameObject
}
else if (gameObject.GetComponent<LargeWorldEntity>() && !gameObject.transform.parent && cellRoot.liveRoot)
{
gameObject.transform.SetParent(cellRoot.liveRoot.transform, true);
LargeWorldEntity.Register(gameObject);
}
else
{
gameObject.SetActive(true);
}
}
if (parent.HasValue)
{
if (parentWaterPark && gameObject.TryGetComponent(out Pickupable pickupable))
{
pickupable.SetVisible(false);
pickupable.Activate(false);
parentWaterPark.AddItem(pickupable);
}
else
{
gameObject.transform.SetParent(parent.Value.transform, true);
}
}
}
public static bool TryGetCachedPrefab(out GameObject prefab, TechType techType = TechType.None, string classId = null)
{
if (classId != null && prefabCacheByClassId.TryGetValue(classId, out prefab))
{
return true;
}
// If we've never even once issued a request prefab for the class id we need to do it because multiple prefabs
// can have the same TechType so it's not good enough to find the right prefab
if (!classIdsWithoutPrefab.Contains(classId) || techType == TechType.None)
{
prefab = null;
return false;
}
return prefabCacheByTechType.TryGetValue(techType, out prefab);
}
/// <summary>
/// Either gets the prefab reference from the cache or requests it and fills the provided result with it.
/// </summary>
/// <remarks>
/// <see cref="PrefabDatabase"/> requires executing an extra yield instruction which is avoided here.
/// Because each yield costs a non-required time (and non-neglectable considering the amount of entities) for batch spawning.
/// Pumping a coroutine isn't possible when it contains prefab loading instructions as the one used here.
/// </remarks>
public static IEnumerator RequestPrefab(TechType techType, TaskResult<GameObject> result)
{
if (prefabCacheByTechType.TryGetValue(techType, out GameObject prefab))
{
result.Set(prefab);
yield break;
}
CoroutineTask<GameObject> techPrefabCoroutine = CraftData.GetPrefabForTechTypeAsync(techType, false);
yield return techPrefabCoroutine;
prefabCacheByTechType[techType] = techPrefabCoroutine.GetResult();
result.Set(techPrefabCoroutine.GetResult());
}
/// <inheritdoc cref="RequestPrefab(TechType, TaskResult{GameObject})"/>
public static IEnumerator RequestPrefab(string classId, TaskResult<GameObject> result)
{
if (prefabCacheByClassId.TryGetValue(classId, out GameObject prefab))
{
result.Set(prefab);
yield break;
}
IPrefabRequest prefabCoroutine = PrefabDatabase.GetPrefabAsync(classId);
yield return prefabCoroutine;
if (prefabCoroutine.TryGetPrefab(out prefab))
{
prefabCacheByClassId[classId] = prefab;
}
result.Set(prefab);
}
public static IEnumerator CreateGameObject(TechType techType, string classId, NitroxId nitroxId, TaskResult<GameObject> result)
{
IPrefabRequest prefabCoroutine = PrefabDatabase.GetPrefabAsync(classId);
yield return prefabCoroutine;
if (prefabCoroutine.TryGetPrefab(out GameObject prefab))
{
prefabCacheByClassId[classId] = prefab;
}
else
{
classIdsWithoutPrefab.Add(classId);
CoroutineTask<GameObject> techPrefabCoroutine = CraftData.GetPrefabForTechTypeAsync(techType, false);
yield return techPrefabCoroutine;
prefab = techPrefabCoroutine.GetResult();
if (!prefab)
{
result.Set(CreateGenericLoot(techType, nitroxId));
prefabNotFound.Add((classId, techType));
yield break;
}
else
{
prefabCacheByTechType[techType] = prefab;
}
}
result.Set(SpawnFromPrefab(prefab, nitroxId));
}
/// <summary>
/// Looks in prefab cache and creates a GameObject out of it if possible, or returns false.
/// </summary>
public static bool TryCreateGameObjectSync(TechType techType, string classId, NitroxId nitroxId, out GameObject gameObject)
{
if (prefabNotFound.Contains((classId, techType)))
{
gameObject = CreateGenericLoot(techType, nitroxId);
return true;
}
else if (TryGetCachedPrefab(out GameObject prefab, techType, classId))
{
gameObject = SpawnFromPrefab(prefab, nitroxId);
return true;
}
gameObject = null;
return false;
}
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
TechType techType = entity.TechType.ToUnity();
if (TryCreateGameObjectSync(techType, entity.ClassId, entity.Id, out GameObject gameObject))
{
SetupObject(entity, parent, gameObject, cellRoot, techType);
result.Set(gameObject);
return true;
}
return false;
}
public bool SpawnsOwnChildren() => false;
}

View File

@@ -0,0 +1,116 @@
using System.Collections;
using NitroxClient.Communication;
using NitroxClient.GameLogic.FMOD;
using NitroxClient.GameLogic.Spawning.Metadata.Processor;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.CinematicController;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class EscapePodWorldEntitySpawner : IWorldEntitySpawner
{
/*
* When creating additional escape pods (multiple users with multiple pods)
* we want to suppress the escape pod's awake method so it doesn't override
* EscapePod.main to the new escape pod.
*/
public static bool SuppressEscapePodAwakeMethod;
private readonly LocalPlayer localPlayer;
public EscapePodWorldEntitySpawner(LocalPlayer localPlayer)
{
this.localPlayer = localPlayer;
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not EscapePodWorldEntity escapePodEntity)
{
result.Set(Optional.Empty);
Log.Error($"Received incorrect entity type: {entity.GetType()}");
yield break;
}
SuppressEscapePodAwakeMethod = true;
GameObject escapePod = CreateNewEscapePod(escapePodEntity);
SuppressEscapePodAwakeMethod = false;
result.Set(Optional.Of(escapePod));
}
private GameObject CreateNewEscapePod(EscapePodWorldEntity escapePodEntity)
{
// TODO: When we want to implement multiple escape pods, instantiate the prefab. Backlog task: #1945
// This will require some work as instantiating the prefab as-is will not make it visible.
//GameObject escapePod = Object.Instantiate(EscapePod.main.gameObject);
GameObject escapePod = EscapePod.main.gameObject;
EscapePod pod = escapePod.GetComponent<EscapePod>();
Object.DestroyImmediate(escapePod.GetComponent<NitroxEntity>()); // if template has a pre-existing NitroxEntity, remove.
NitroxEntity.SetNewId(escapePod, escapePodEntity.Id);
if (escapePod.TryGetComponent(out Rigidbody rigidbody))
{
rigidbody.constraints = RigidbodyConstraints.FreezeAll;
}
else
{
Log.Error("Escape pod did not have a rigid body!");
}
pod.anchorPosition = escapePod.transform.position = escapePodEntity.Transform.Position.ToUnity();
pod.ForceSkyApplier();
pod.escapePodCinematicControl.StopAll();
// Player is not new and has completed the intro cinematic. If not EscapePod repair status is handled by the intro cinematic.
if (escapePodEntity.Metadata is EscapePodMetadata metadata && localPlayer.IntroCinematicMode == IntroCinematicMode.COMPLETED)
{
using FMODSoundSuppressor soundSuppressor = FMODSystem.SuppressSubnauticaSounds();
using PacketSuppressor<EntityMetadataUpdate> packetSuppressor = PacketSuppressor<EntityMetadataUpdate>.Suppress();
Radio radio = pod.radioSpawner.spawnedObj.GetComponent<Radio>();
EscapePodMetadataProcessor.ProcessInitialSyncMetadata(pod, radio, metadata);
// NB: Entities.SpawnBatchAsync (which is the function calling the current spawner)
// will still apply the metadata another time but we don't care as it's not destructive
}
FixStartMethods(escapePod);
return escapePod;
}
/// <summary>
/// Start() isn't executed for the EscapePod and children (Why? Idk, maybe because it's a scene...) so we call the components here where we have patches in Start.
/// </summary>
private static void FixStartMethods(GameObject escapePod)
{
foreach (FMOD_CustomEmitter customEmitter in escapePod.GetComponentsInChildren<FMOD_CustomEmitter>(true))
{
customEmitter.Start();
}
foreach (FMOD_StudioEventEmitter studioEventEmitter in escapePod.GetComponentsInChildren<FMOD_StudioEventEmitter>(true))
{
studioEventEmitter.Start();
}
MultiplayerCinematicReference reference = escapePod.EnsureComponent<MultiplayerCinematicReference>();
foreach (PlayerCinematicController controller in escapePod.GetComponentsInChildren<PlayerCinematicController>(true))
{
reference.AddController(controller);
}
}
public bool SpawnsOwnChildren() => false;
}

View File

@@ -0,0 +1,107 @@
using System.Collections;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using UWE;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class GeyserWorldEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private readonly Entities entities;
public GeyserWorldEntitySpawner(Entities entities)
{
this.entities = entities;
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not GeyserWorldEntity geyserWorldEntity)
{
yield break;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(GeyserWorldEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(geyserWorldEntity, gameObject, out Geyser geyser))
{
yield break;
}
SetupObject(geyserWorldEntity, cellRoot, geyser);
gameObject.SetActive(true);
result.Set(Optional.Of(gameObject));
}
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not GeyserWorldEntity geyserWorldEntity)
{
return true;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(geyserWorldEntity, gameObject, out Geyser geyser))
{
return true;
}
SetupObject(geyserWorldEntity, cellRoot, geyser);
gameObject.SetActive(true);
result.Set(gameObject);
return true;
}
public bool SpawnsOwnChildren() => false;
private static bool VerifyCanSpawnOrError(GeyserWorldEntity geyserEntity, GameObject prefabObject, out Geyser geyser)
{
if (prefabObject.TryGetComponent(out geyser))
{
return true;
}
Log.Error($"Could not find component {nameof(Geyser)} on prefab with ClassId: {geyserEntity.ClassId}");
return false;
}
private static void SetupObject(GeyserWorldEntity geyserEntity, EntityCell cellRoot, Geyser geyser)
{
Transform transform = geyser.transform;
transform.localPosition = geyserEntity.Transform.LocalPosition.ToUnity();
transform.localRotation = geyserEntity.Transform.LocalRotation.ToUnity();
transform.localScale = geyserEntity.Transform.LocalScale.ToUnity();
transform.SetParent(cellRoot.liveRoot.transform);
// To ensure Geyser.Start() happens first, we delay our code by a frame
CoroutineHost.StartCoroutine(DelayedGeyserSetup(geyserEntity, geyser));
}
private static IEnumerator DelayedGeyserSetup(GeyserWorldEntity geyserEntity, Geyser geyser)
{
// Delay by an entire frame
yield return null;
geyser.gameObject.EnsureComponent<NitroxGeyser>().Initialize(geyserEntity, geyser);
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using UWE;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class GlobalRootEntitySpawner : SyncEntitySpawner<GlobalRootEntity>
{
protected override IEnumerator SpawnAsync(GlobalRootEntity entity, TaskResult<Optional<GameObject>> result)
{
TaskResult<GameObject> gameObjectResult = new();
yield return DefaultWorldEntitySpawner.CreateGameObject(entity.TechType.ToUnity(), entity.ClassId, entity.Id, gameObjectResult);
GameObject gameObject = gameObjectResult.Get();
SetupObject(entity, gameObject);
result.Set(gameObject);
}
protected override bool SpawnSync(GlobalRootEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, entity.TechType.ToUnity(), entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
SetupObject(entity, gameObject);
result.Set(gameObject);
return true;
}
private void SetupObject(GlobalRootEntity entity, GameObject gameObject)
{
LargeWorldEntity largeWorldEntity = gameObject.EnsureComponent<LargeWorldEntity>();
largeWorldEntity.cellLevel = LargeWorldEntity.CellLevel.Global;
LargeWorld.main.streamer.cellManager.RegisterEntity(largeWorldEntity);
largeWorldEntity.Start();
gameObject.transform.localPosition = entity.Transform.LocalPosition.ToUnity();
gameObject.transform.localRotation = entity.Transform.LocalRotation.ToUnity();
gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity();
if (entity.ParentId != null && NitroxEntity.TryGetComponentFrom(entity.ParentId, out Transform parentTransform))
{
// WaterParks have a child named "items_root" where the fish are put
if (parentTransform.TryGetComponent(out WaterPark waterPark))
{
SetupObjectInWaterPark(gameObject, waterPark);
// TODO: When metadata is reworked (it'll be possible to give different metadatas to the same entity)
// this will no longer be needed because the entity metadata will set this to false accordingly
// If fishes are in a WaterPark, it means that they were once picked up
if (gameObject.TryGetComponent(out CreatureDeath creatureDeath))
{
// This is set to false when picking up a fish or when a fish is born in the WaterPark
creatureDeath.respawn = false;
}
}
else
{
gameObject.transform.SetParent(parentTransform, false);
}
}
if (gameObject.GetComponent<PlaceTool>())
{
PlacedWorldEntitySpawner.AdditionalSpawningSteps(gameObject);
}
}
public static void SetupObjectInWaterPark(GameObject gameObject, WaterPark waterPark)
{
gameObject.transform.SetParent(waterPark.itemsRoot, false);
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
waterPark.AddItem(gameObject.EnsureComponent<Pickupable>());
// While being fully loaded, the base is inactive so GameObject.SendMessage doesn't work and we need to execute their callbacks manually
if (!Multiplayer.Main || Multiplayer.Main.InitialSyncCompleted)
{
return;
}
// Below are distinct incompatible cases
if (gameObject.TryGetComponent(out CreatureEgg creatureEgg) && !creatureEgg.insideWaterPark)
{
creatureEgg.OnAddToWaterPark();
}
else if (gameObject.TryGetComponent(out CuteFish cuteFish))
{
cuteFish.OnAddToWaterPark(null);
}
else if (gameObject.TryGetComponent(out CrabSnake crabSnake))
{
// This callback interacts with an animator, but this behaviour needs to be initialized (probably during Start) before it can be modified
IEnumerator PostponedCallback()
{
yield return new WaitUntil(() => !crabSnake || crabSnake.animationController.animator.isInitialized);
if (crabSnake)
{
crabSnake.OnAddToWaterPark();
}
}
CoroutineHost.StartCoroutine(PostponedCallback());
}
}
}
protected override bool SpawnsOwnChildren(GlobalRootEntity entity) => false;
}

View File

@@ -0,0 +1,17 @@
using System.Collections;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities
{
/**
* Allows us to create custom entity spawners for different world entity types.
*/
public interface IWorldEntitySpawner
{
IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result);
bool SpawnsOwnChildren();
}
}

View File

@@ -0,0 +1,10 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public interface IWorldEntitySyncSpawner
{
bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result);
}

View File

@@ -0,0 +1,127 @@
using System.Collections;
using System.Collections.Generic;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class OxygenPipeEntitySpawner : SyncEntitySpawner<OxygenPipeEntity>
{
private readonly Entities entities;
private readonly WorldEntitySpawner worldEntitySpawner;
private readonly Dictionary<NitroxId, List<OxygenPipe>> childrenPipeEntitiesByParentId = new();
public OxygenPipeEntitySpawner(Entities entities, WorldEntitySpawner worldEntitySpawner)
{
this.entities = entities;
this.worldEntitySpawner = worldEntitySpawner;
}
protected override IEnumerator SpawnAsync(OxygenPipeEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(OxygenPipeEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject, out OxygenPipe oxygenPipe))
{
yield break;
}
SetupObject(entity, gameObject, oxygenPipe);
gameObject.SetActive(true);
result.Set(Optional.Of(gameObject));
}
protected override bool SpawnSync(OxygenPipeEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateInactiveWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject, out OxygenPipe oxygenPipe))
{
return true;
}
SetupObject(entity, gameObject, oxygenPipe);
gameObject.SetActive(true);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(OxygenPipeEntity entity) => false;
private bool VerifyCanSpawnOrError(OxygenPipeEntity entity, GameObject prefabObject, out OxygenPipe oxygenPipe)
{
if (prefabObject.TryGetComponent(out oxygenPipe))
{
return true;
}
Log.Error($"Couldn't find component {nameof(OxygenPipe)} on prefab with ClassId: {entity.ClassId}");
return false;
}
private void SetupObject(OxygenPipeEntity entity, GameObject gameObject, OxygenPipe oxygenPipe)
{
EntityCell cellRoot = worldEntitySpawner.EnsureCell(entity);
gameObject.transform.SetParent(cellRoot.liveRoot.transform, false);
gameObject.transform.position = entity.Transform.Position.ToUnity();
gameObject.transform.rotation = entity.Transform.Rotation.ToUnity();
gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity();
// The reference IDs must be set even if the target is not spawned yet
oxygenPipe.parentPipeUID = entity.ParentPipeId.ToString();
oxygenPipe.rootPipeUID = entity.RootPipeId.ToString();
oxygenPipe.parentPosition = entity.ParentPosition.ToUnity();
// It can happen that the parent connection hasn't loaded yet (normal behaviour)
if (NitroxEntity.TryGetComponentFrom(entity.ParentPipeId, out IPipeConnection parentConnection))
{
oxygenPipe.parentPosition = parentConnection.GetAttachPoint();
parentConnection.AddChild(oxygenPipe);
}
else
{
// We add this pipe to a pending list so that its parent pipe will know which children are already spawned when being spanwed
if (!childrenPipeEntitiesByParentId.TryGetValue(entity.ParentPipeId, out List<OxygenPipe> pendingChildren))
{
childrenPipeEntitiesByParentId[entity.ParentPipeId] = pendingChildren = new();
}
pendingChildren.Add(oxygenPipe);
}
if (childrenPipeEntitiesByParentId.TryGetValue(entity.Id, out List<OxygenPipe> children))
{
foreach (OxygenPipe childPipe in children)
{
oxygenPipe.AddChild(childPipe);
}
childrenPipeEntitiesByParentId.Remove(entity.Id);
}
UWE.Utils.SetIsKinematicAndUpdateInterpolation(oxygenPipe.rigidBody, true, false);
oxygenPipe.UpdatePipe();
}
}

View File

@@ -0,0 +1,100 @@
using System.Collections;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class PlacedWorldEntitySpawner : SyncEntitySpawner<PlacedWorldEntity>
{
private readonly WorldEntitySpawner worldEntitySpawner;
public PlacedWorldEntitySpawner(WorldEntitySpawner worldEntitySpawner)
{
this.worldEntitySpawner = worldEntitySpawner;
}
protected override IEnumerator SpawnAsync(PlacedWorldEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(OxygenPipeEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject))
{
yield break;
}
SetupObject(entity, gameObject);
result.Set(Optional.Of(gameObject));
}
protected override bool SpawnSync(PlacedWorldEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(entity, gameObject))
{
return true;
}
SetupObject(entity, gameObject);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(PlacedWorldEntity entity) => false;
public static void AdditionalSpawningSteps(GameObject gameObject)
{
if (gameObject.TryGetComponent(out PlaceTool placeTool))
{
if (gameObject.TryGetComponentInParent(out SubRoot subRoot))
{
SkyEnvironmentChanged.Send(gameObject, subRoot);
}
if (gameObject.TryGetComponent(out Rigidbody rigidbody))
{
UWE.Utils.SetIsKinematicAndUpdateInterpolation(rigidbody, true, false);
}
placeTool.OnPlace();
}
}
private bool VerifyCanSpawnOrError(PlacedWorldEntity entity, GameObject prefabObject)
{
if (prefabObject.GetComponent<PlaceTool>())
{
return true;
}
Log.Error($"Couldn't find component {nameof(PlaceTool)} on prefab with ClassId: {entity.ClassId}");
return false;
}
private void SetupObject(PlacedWorldEntity entity, GameObject gameObject)
{
EntityCell cellRoot = worldEntitySpawner.EnsureCell(entity);
gameObject.transform.SetParent(cellRoot.liveRoot.transform, false);
gameObject.transform.position = entity.Transform.Position.ToUnity();
gameObject.transform.rotation = entity.Transform.Rotation.ToUnity();
gameObject.transform.localScale = entity.Transform.LocalScale.ToUnity();
AdditionalSpawningSteps(gameObject);
}
}

View File

@@ -0,0 +1,163 @@
using System.Collections;
using System.Collections.Generic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
/// <remarks>
/// This spawner can't hold a SpawnSync function because it is also responsible for spawning its children
/// so the <see cref="SpawnAsync"/> function will still use sync spawning when possible and fall back to async when required.
/// </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(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)
{
Log.Error($"[{nameof(PlaceholderGroupWorldEntitySpawner)}] Can't spawn {entity.Id} of type {entity.GetType()} because it is not a {nameof(PlaceholderGroupWorldEntity)}");
yield break;
}
TaskResult<Optional<GameObject>> prefabPlaceholderGroupTaskResult = new();
if (!defaultSpawner.SpawnSync(entity, parent, cellRoot, prefabPlaceholderGroupTaskResult))
{
yield return defaultSpawner.SpawnAsync(entity, parent, cellRoot, prefabPlaceholderGroupTaskResult);
}
Optional<GameObject> prefabPlaceholderGroupGameObject = prefabPlaceholderGroupTaskResult.Get();
if (!prefabPlaceholderGroupGameObject.HasValue)
{
yield break;
}
GameObject groupObject = prefabPlaceholderGroupGameObject.Value;
// Spawning PrefabPlaceholders as siblings to the group
PrefabPlaceholdersGroup prefabPlaceholderGroup = groupObject.GetComponent<PrefabPlaceholdersGroup>();
// Spawning all children iteratively
Stack<Entity> stack = new(placeholderGroupEntity.ChildEntities);
TaskResult<Optional<GameObject>> childResult = new();
Dictionary<NitroxId, GameObject> parentById = new()
{
{ entity.Id, groupObject }
};
while (stack.Count > 0)
{
// It may happen that the chunk is unloaded, and the group along so we just cancel this spawn behaviour
if (!groupObject)
{
yield break;
}
childResult.Set(Optional.Empty);
Entity current = stack.Pop();
switch (current)
{
case PrefabPlaceholderEntity prefabEntity:
if (!prefabPlaceholderEntitySpawner.SpawnSync(prefabEntity, groupObject, cellRoot, childResult))
{
yield return prefabPlaceholderEntitySpawner.SpawnAsync(prefabEntity, groupObject, cellRoot, childResult);
}
break;
case PlaceholderGroupWorldEntity groupEntity:
PrefabPlaceholder placeholder = prefabPlaceholderGroup.prefabPlaceholders[groupEntity.ComponentIndex];
yield return SpawnAsync(groupEntity, placeholder.transform.parent.gameObject, cellRoot, childResult);
break;
case WorldEntity worldEntity:
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)
{
Log.Error($"[{nameof(PlaceholderGroupWorldEntitySpawner)}] Spawning of child failed {current}");
continue;
}
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)
{
// Adding children to be spawned by this loop
foreach (Entity slotEntityChild in current.ChildEntities)
{
stack.Push(slotEntityChild);
}
}
}
result.Set(prefabPlaceholderGroupGameObject);
}
public bool SpawnsOwnChildren() => true;
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)
{
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, GameObject parent, TaskResult<Optional<GameObject>> worldEntityResult, out IEnumerator asyncInstructions)
{
IWorldEntitySpawner spawner = spawnerResolver.ResolveEntitySpawner(worldEntity);
if (spawner is not IWorldEntitySyncSpawner syncSpawner ||
!syncSpawner.SpawnSync(worldEntity, parent, cellRoot, worldEntityResult) ||
!worldEntityResult.value.HasValue)
{
asyncInstructions = SpawnWorldEntityChildAsync(worldEntity, cellRoot, parent, worldEntityResult);
return false;
}
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();
asyncInstructions = null;
return true;
}
}

View File

@@ -0,0 +1,137 @@
using System.Collections;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class PlayerWorldEntitySpawner : IWorldEntitySpawner
{
private readonly PlayerManager playerManager;
private readonly ILocalNitroxPlayer localPlayer;
public PlayerWorldEntitySpawner(PlayerManager playerManager, ILocalNitroxPlayer localPlayer)
{
this.playerManager = playerManager;
this.localPlayer = localPlayer;
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (Player.main.TryGetNitroxId(out NitroxId localPlayerId) && localPlayerId == entity.Id)
{
// No special setup for the local player. Simply return saying it is spawned.
result.Set(Player.main.gameObject);
yield break;
}
Optional<RemotePlayer> remotePlayer = playerManager.Find(entity.Id);
// The server may send us a player entity but they are not guarenteed to be actively connected at the moment - don't spawn them. In the
// future, we could make this configurable to be able to spawn disconnected players in the world.
if (remotePlayer.HasValue && !remotePlayer.Value.Body)
{
GameObject remotePlayerBody = CloneLocalPlayerBodyPrototype();
remotePlayer.Value.InitializeGameObject(remotePlayerBody);
if (!IsSwimming(entity.Transform.Position.ToUnity(), parent))
{
remotePlayer.Value.UpdateAnimationAndCollider(AnimChangeType.UNDERWATER, AnimChangeState.OFF);
}
if (parent.HasValue)
{
AttachToParent(remotePlayer.Value, parent.Value);
}
result.Set(Optional.Of(remotePlayerBody));
yield break;
}
result.Set(Optional.Empty);
}
public bool SpawnsOwnChildren()
{
return false;
}
private GameObject CloneLocalPlayerBodyPrototype()
{
GameObject clone = Object.Instantiate(localPlayer.BodyPrototype, null, false);
clone.SetActive(true);
return clone;
}
private void AttachToParent(RemotePlayer remotePlayer, GameObject parent)
{
if (parent.TryGetComponent(out SubRoot subRoot))
{
Log.Debug($"Found sub root for {remotePlayer.PlayerName}. Will add him and update animation.");
remotePlayer.SetSubRoot(subRoot);
}
else if (parent.TryGetComponent(out EscapePod escapePod))
{
Log.Debug($"Found EscapePod for {remotePlayer.PlayerName}.");
remotePlayer.SetEscapePod(escapePod);
}
else
{
Log.Error($"Found neither SubRoot component nor EscapePod on {parent.name} for {remotePlayer.PlayerName}.");
}
}
private bool IsSwimming(Vector3 playerPosition, Optional<GameObject> parent)
{
if (parent.HasValue)
{
parent.Value.TryGetComponent<SubRoot>(out SubRoot subroot);
// Set the animation for the remote player to standing instead of swimming if player is not in a flooded subroot
// or in a waterpark
if (subroot)
{
if (subroot.IsUnderwater(playerPosition))
{
return true;
}
if (subroot.isCyclops)
{
return false;
}
// We know that we are in a subroot. But we can also be in a waterpark in a subroot, where we would swim
BaseRoot baseRoot = subroot.GetComponentInParent<BaseRoot>();
if (baseRoot)
{
WaterPark[] waterParks = baseRoot.GetComponentsInChildren<WaterPark>();
foreach (WaterPark waterPark in waterParks)
{
if (waterPark.IsPointInside(playerPosition))
{
return true;
}
}
return false;
}
}
Log.Debug($"Trying to find escape pod for {parent}.");
parent.Value.TryGetComponent<EscapePod>(out EscapePod escapePod);
if (escapePod)
{
Log.Debug("Found escape pod for player. Will add him and update animation.");
return false;
}
}
// Player can be above ocean level.
float oceanLevel = Ocean.GetOceanLevel();
return playerPosition.y < oceanLevel;
}
}

View File

@@ -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<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> 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<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> 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<GameObject> 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();
}
}

View File

@@ -0,0 +1,102 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class RadiationLeakEntitySpawner : SyncEntitySpawner<RadiationLeakEntity>
{
// This constant is defined by Subnautica and should never be modified (same as for SubnauticaWorldModifier)
private const int TOTAL_LEAKS = 11;
private readonly TimeManager timeManager;
private readonly List<float> registeredLeaksFixTime = new();
public RadiationLeakEntitySpawner(TimeManager timeManager)
{
this.timeManager = timeManager;
}
protected override IEnumerator SpawnAsync(RadiationLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}
protected override bool SpawnSync(RadiationLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
// This script is located under (Aurora Scene) //Aurora-Main/Aurora so it's a good starting point to search through the GameObjects
CrashedShipExploder crashedShipExploder = CrashedShipExploder.main;
LeakingRadiation leakingRadiation = LeakingRadiation.main;
if (!crashedShipExploder || !leakingRadiation || entity.Metadata is not RadiationMetadata metadata)
{
return true;
}
Transform radiationLeaksHolder = crashedShipExploder.transform.Find("radiationleaks").GetChild(0);
RadiationLeak radiationLeak = radiationLeaksHolder.GetChild(entity.ObjectIndex).GetComponent<RadiationLeak>();
NitroxEntity.SetNewId(radiationLeak.gameObject, entity.Id);
radiationLeak.liveMixin.health = metadata.Health;
registeredLeaksFixTime.Add(metadata.FixRealTime);
// We can only calculate the radiation increment and dissipation once we got all radiation leaks info
if (crashedShipExploder.IsExploded() && registeredLeaksFixTime.Count == TOTAL_LEAKS)
{
RecalculateRadiationRadius(leakingRadiation);
}
return true;
}
public void RecalculateRadiationRadius(LeakingRadiation leakingRadiation)
{
float realElapsedTime = (float)timeManager.RealTimeElapsed;
// We substract the explosion time from the real time because before that, the radius doesn't increment
float realExplosionTime = timeManager.AuroraRealExplosionTime;
float maxRegisteredLeakFixTime = registeredLeaksFixTime.Max();
// Note: Only increment radius if leaks were fixed AFTER explosion (before, game code doesn't increase radius)
// If leaks aren't all fixed yet we calculate from current real elapsed time
float deltaTimeAfterExplosion = realElapsedTime - realExplosionTime;
if (maxRegisteredLeakFixTime == -1)
{
if (deltaTimeAfterExplosion > 0)
{
float radiusIncrement = deltaTimeAfterExplosion * leakingRadiation.kGrowRate;
// Calculation lines from LeakingRadiation.Update
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.kStartRadius + radiusIncrement, 0f, leakingRadiation.kMaxRadius);
leakingRadiation.damagePlayerInRadius.damageRadius = leakingRadiation.currentRadius;
leakingRadiation.radiatePlayerInRange.radiateRadius = leakingRadiation.currentRadius;
}
// If leaks aren't fixed, we won't need to calculate a radius decrement
return;
}
leakingRadiation.radiationFixed = true;
// If all leaks are fixed we calculate from the time they were fixed
float deltaAliveTime = maxRegisteredLeakFixTime - realExplosionTime;
if (deltaAliveTime > 0)
{
float radiusIncrement = deltaAliveTime * leakingRadiation.kGrowRate;
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.kStartRadius + radiusIncrement, 0f, leakingRadiation.kMaxRadius);
}
// Now calculate the natural dissipation decrement from the time leaks are fixed
// If they were fixed before real explosion time, we calculate from real explosion time
float deltaFixedTimeAfterExplosion = realElapsedTime - Mathf.Max(maxRegisteredLeakFixTime, realExplosionTime);
if (deltaFixedTimeAfterExplosion > 0)
{
float radiusDecrement = deltaFixedTimeAfterExplosion * leakingRadiation.kNaturalDissipation;
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.currentRadius + radiusDecrement, 0f, leakingRadiation.kMaxRadius);
}
leakingRadiation.damagePlayerInRadius.damageRadius = leakingRadiation.currentRadius;
leakingRadiation.radiatePlayerInRange.radiateRadius = leakingRadiation.currentRadius;
}
protected override bool SpawnsOwnChildren(RadiationLeakEntity entity) => false;
}

View File

@@ -0,0 +1,101 @@
using System.Collections;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class ReefbackChildEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not ReefbackChildEntity reefbackChildEntity)
{
yield break;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(OxygenPipeEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(reefbackChildEntity, out ReefbackLife parentReefbackLife))
{
yield break;
}
SetupObject(reefbackChildEntity, gameObject, parentReefbackLife);
result.Set(Optional.Of(gameObject));
}
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not ReefbackChildEntity reefbackChildEntity)
{
return true;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
if (!VerifyCanSpawnOrError(reefbackChildEntity, out ReefbackLife parentReefbackLife))
{
return true;
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
SetupObject(reefbackChildEntity, gameObject, parentReefbackLife);
result.Set(gameObject);
return true;
}
public bool SpawnsOwnChildren() => false;
private static bool VerifyCanSpawnOrError(ReefbackChildEntity entity, out ReefbackLife parentReefbackLife)
{
if (NitroxEntity.TryGetComponentFrom(entity.ParentId, out parentReefbackLife))
{
return true;
}
Log.Error($"Could not find a valid parent with {nameof(ReefbackLife)} from Id: {entity.ParentId}");
return false;
}
private static void SetupObject(ReefbackChildEntity entity, GameObject gameObject, ReefbackLife parentReefbackLife)
{
Transform transform = gameObject.transform;
transform.localPosition = entity.Transform.LocalPosition.ToUnity();
transform.localRotation = entity.Transform.LocalRotation.ToUnity();
transform.localScale = entity.Transform.LocalScale.ToUnity();
// Positioning from ReefbackLife.SpawnPlants and ReefbackLife.SpawnCreatures
switch (entity.Type)
{
case ReefbackChildEntity.ReefbackChildType.PLANT:
transform.SetParent(parentReefbackLife.plantSlots[0].parent, false);
gameObject.AddComponent<ReefbackPlant>();
break;
case ReefbackChildEntity.ReefbackChildType.CREATURE:
transform.SetParent(parentReefbackLife.creatureSlots[0].parent, false);
gameObject.AddComponent<ReefbackCreature>();
break;
}
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class ReefbackEntitySpawner : IWorldEntitySpawner, IWorldEntitySyncSpawner
{
private readonly ReefbackChildEntitySpawner reefbackChildEntitySpawner;
public ReefbackEntitySpawner(ReefbackChildEntitySpawner reefbackChildEntitySpawner)
{
this.reefbackChildEntitySpawner = reefbackChildEntitySpawner;
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not ReefbackEntity reefbackEntity)
{
yield break;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for {nameof(OxygenPipeEntity)} of ClassId {entity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(reefbackEntity, gameObject, out ReefbackLife reefbackLife))
{
yield break;
}
SetupObject(reefbackEntity, gameObject, cellRoot, reefbackLife);
result.Set(gameObject);
}
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not ReefbackEntity reefbackEntity)
{
return true;
}
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: entity.ClassId))
{
return false;
}
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefab, entity.Id);
if (!VerifyCanSpawnOrError(reefbackEntity, gameObject, out ReefbackLife reefbackLife))
{
return true;
}
SetupObject(reefbackEntity, gameObject, cellRoot, reefbackLife);
result.Set(gameObject);
return true;
}
public bool SpawnsOwnChildren() => false;
private static bool VerifyCanSpawnOrError(ReefbackEntity entity, GameObject prefabObject, out ReefbackLife reefbackLife)
{
if (prefabObject.TryGetComponent(out reefbackLife))
{
return true;
}
Log.Error($"Could not find component {nameof(ReefbackLife)} on prefab with ClassId: {entity.ClassId}");
return false;
}
private static void SetupObject(ReefbackEntity entity, GameObject gameObject, EntityCell entityCell, ReefbackLife reefbackLife)
{
Transform transform = gameObject.transform;
transform.localPosition = entity.Transform.Position.ToUnity();
transform.localRotation = entity.Transform.Rotation.ToUnity();
transform.localScale = entity.Transform.LocalScale.ToUnity();
entityCell.EnsureRoot();
transform.SetParent(entityCell.liveRoot.transform);
// Replicate only the useful parts of ReefbackLife.Initialize
reefbackLife.initialized = true;
reefbackLife.needToRemovePlantPhysics = false;
reefbackLife.hasCorals = gameObject.transform.localScale.x > 0.8f;
if (reefbackLife.hasCorals && LargeWorld.main)
{
string biome = LargeWorld.main.GetBiome(entity.OriginalPosition.ToUnity());
if (!string.IsNullOrEmpty(biome) && biome.StartsWith("grassyplateaus", StringComparison.OrdinalIgnoreCase))
{
reefbackLife.grassIndex = 0;
}
else
{
reefbackLife.grassIndex = entity.GrassIndex;
}
}
// Only useful stuff from ReefbackLife.CoSpawn
reefbackLife.corals.SetActive(reefbackLife.hasCorals);
reefbackLife.islands.SetActive(reefbackLife.hasCorals);
if (reefbackLife.grassIndex >= 0 && reefbackLife.grassIndex < reefbackLife.grassVariants.Length)
{
reefbackLife.grassVariants[reefbackLife.grassIndex].SetActive(true);
}
}
}

View File

@@ -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
{
/// <summary>
/// Contains the only types we allow the server to instantiate on clients (for security concerns)
/// </summary>
private readonly HashSet<Type> 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<GameObject>("SerializerEmptyGameObject");
}
}
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, parent, cellRoot, result);
yield break;
}
public bool SpawnsOwnChildren() => false;
public bool SpawnSync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
if (entity is not SerializedWorldEntity serializedWorldEntity)
{
return true;
}
using PooledObject<ProtobufSerializer> 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>();
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<Type, int> 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<IProtoEventListener>())
{
listener.OnProtoDeserialize(serializer);
}
dictionary.Clear();
ProtobufSerializer.componentCountersPool.Return(dictionary);
gameObject.SetActive(true);
result.Set(gameObject);
return true;
}
}

View File

@@ -0,0 +1,225 @@
using System.Collections;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.CinematicController;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class VehicleWorldEntitySpawner : IWorldEntitySpawner
{
private readonly Entities entities;
public VehicleWorldEntitySpawner(Entities entities)
{
this.entities = entities;
}
// The constructor has mixed results when the remote player is a long distance away. UWE even has a built in distance tracker to ensure
// that they are within allowed range. However, this range is a bit restrictive. We will allow constructor spawning up to a specified
// distance - anything more will simply use world spawning (no need to play the animation anyways).
private const float ALLOWED_CONSTRUCTOR_DISTANCE = 100.0f;
public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, EntityCell cellRoot, TaskResult<Optional<GameObject>> result)
{
VehicleWorldEntity vehicleEntity = (VehicleWorldEntity)entity;
bool withinConstructorSpawnWindow = (DayNightCycle.main.timePassedAsFloat - vehicleEntity.ConstructionTime) < GetCraftDuration(vehicleEntity.TechType.ToUnity());
Optional<GameObject> spawnerObj = NitroxEntity.GetObjectFrom(vehicleEntity.SpawnerId);
if (withinConstructorSpawnWindow && spawnerObj.HasValue)
{
Constructor constructor = spawnerObj.Value.GetComponent<Constructor>();
float distance = (constructor.transform.position - Player.main.transform.position).magnitude;
bool withinDistance = distance <= ALLOWED_CONSTRUCTOR_DISTANCE;
if (constructor && withinDistance)
{
MobileVehicleBay.TransmitLocalSpawns = false;
yield return SpawnViaConstructor(vehicleEntity, constructor, result);
MobileVehicleBay.TransmitLocalSpawns = true;
yield break;
}
}
yield return SpawnInWorld(vehicleEntity, result, parent);
}
private IEnumerator SpawnInWorld(VehicleWorldEntity vehicleEntity, TaskResult<Optional<GameObject>> result, Optional<GameObject> parent)
{
TechType techType = vehicleEntity.TechType.ToUnity();
GameObject gameObject = null;
bool isCyclops = techType == TechType.Cyclops;
if (isCyclops)
{
GameObject prefab = null;
LightmappedPrefabs.main.RequestScenePrefab("cyclops", (go) => prefab = go);
yield return new WaitUntil(() => prefab != null);
SubConsoleCommand.main.OnSubPrefabLoaded(prefab);
gameObject = SubConsoleCommand.main.GetLastCreatedSub();
}
else
{
CoroutineTask<GameObject> techPrefabCoroutine = CraftData.GetPrefabForTechTypeAsync(techType, false);
yield return techPrefabCoroutine;
GameObject techPrefab = techPrefabCoroutine.GetResult();
gameObject = Utils.SpawnPrefabAt(techPrefab, null, vehicleEntity.Transform.Position.ToUnity());
Validate.NotNull(gameObject, $"{nameof(VehicleWorldEntitySpawner)}: No prefab for tech type: {techType}");
Vehicle vehicle = gameObject.GetComponent<Vehicle>();
if (vehicle)
{
vehicle.LazyInitialize();
}
}
AddCinematicControllers(gameObject);
gameObject.transform.position = vehicleEntity.Transform.Position.ToUnity();
gameObject.transform.rotation = vehicleEntity.Transform.Rotation.ToUnity();
gameObject.SetActive(true);
gameObject.SendMessage("StartConstruction", SendMessageOptions.DontRequireReceiver);
CrafterLogic.NotifyCraftEnd(gameObject, CraftData.GetTechType(gameObject));
Rigidbody rigidBody = gameObject.RequireComponent<Rigidbody>();
rigidBody.isKinematic = false;
yield return Yielders.WaitForEndOfFrame;
RemoveConstructionAnimations(gameObject);
yield return Yielders.WaitForEndOfFrame;
Vehicles.RemoveNitroxEntitiesTagging(gameObject);
NitroxEntity.SetNewId(gameObject, vehicleEntity.Id);
if (vehicleEntity.Metadata is CyclopsMetadata cyclopsMetadata && cyclopsMetadata.IsDestroyed)
{
// Swap to destroyed look without triggering animations / effects
gameObject.BroadcastMessage("SwapToDamagedModels");
gameObject.BroadcastMessage("OnKill");
gameObject.BroadcastMessage("CyclopsDeathEvent", SendMessageOptions.DontRequireReceiver);
}
if (parent.HasValue)
{
DockVehicle(gameObject, parent.Value);
}
result.Set(gameObject);
}
private IEnumerator SpawnViaConstructor(VehicleWorldEntity vehicleEntity, Constructor constructor, TaskResult<Optional<GameObject>> result)
{
if (!constructor.deployed)
{
constructor.Deploy(true);
}
float craftDuration = GetCraftDuration(vehicleEntity.TechType.ToUnity()) - (DayNightCycle.main.timePassedAsFloat - vehicleEntity.ConstructionTime);
ConstructorInput crafter = constructor.gameObject.RequireComponentInChildren<ConstructorInput>(true);
yield return crafter.OnCraftingBeginAsync(vehicleEntity.TechType.ToUnity(), craftDuration);
GameObject constructedObject = MobileVehicleBay.MostRecentlyCrafted;
Validate.IsTrue(constructedObject, $"Could not find constructed object from MobileVehicleBay {constructor.gameObject.name}");
NitroxEntity.SetNewId(constructedObject, vehicleEntity.Id);
AddCinematicControllers(constructedObject);
result.Set(constructedObject);
yield break;
}
/// <summary>
/// For scene objects like cyclops, PlayerCinematicController Start() will not be called to add Cinematic reference.
/// </summary>
private void AddCinematicControllers(GameObject gameObject)
{
if (gameObject.GetComponent<MultiplayerCinematicReference>())
{
return;
}
PlayerCinematicController[] controllers = gameObject.GetComponentsInChildren<PlayerCinematicController>(true);
if (controllers.Length == 0)
{
return;
}
MultiplayerCinematicReference reference = gameObject.AddComponent<MultiplayerCinematicReference>();
foreach (PlayerCinematicController controller in controllers)
{
reference.AddController(controller);
}
}
/// <summary>
/// When loading in vehicles, they still briefly have their blue crafting animation playing. Force them to stop.
/// </summary>
private void RemoveConstructionAnimations(GameObject gameObject)
{
VFXConstructing[] vfxConstructions = gameObject.GetComponentsInChildren<VFXConstructing>();
foreach (VFXConstructing vfxConstructing in vfxConstructions)
{
vfxConstructing.EndGracefully();
}
}
private void DockVehicle(GameObject gameObject, GameObject parent)
{
Vehicle vehicle = gameObject.GetComponent<Vehicle>();
if (!vehicle)
{
Log.Info($"Could not find vehicle component on docked vehicle {gameObject.name}");
return;
}
VehicleDockingBay dockingBay = parent.GetComponentInChildren<VehicleDockingBay>(true);
if (!dockingBay)
{
Log.Info($"Could not find VehicleDockingBay component on dock object {parent.name}");
return;
}
dockingBay.DockVehicle(vehicle);
}
public bool SpawnsOwnChildren()
{
return false;
}
private float GetCraftDuration(TechType techType)
{
// UWE hard codes the build times into if/else logic inside ConstructorInput.Craft().
switch(techType)
{
case TechType.Seamoth:
return 10f;
case TechType.Exosuit:
return 10f;
case TechType.Cyclops:
return 20f;
case TechType.RocketBase:
return 25f;
}
return 10f;
}
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Generic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.GameLogic.Spawning.WorldEntities;
public class WorldEntitySpawnerResolver
{
private readonly DefaultWorldEntitySpawner defaultEntitySpawner = new();
private readonly VehicleWorldEntitySpawner vehicleWorldEntitySpawner;
private readonly PrefabPlaceholderEntitySpawner prefabPlaceholderEntitySpawner;
private readonly PlaceholderGroupWorldEntitySpawner placeholderGroupWorldEntitySpawner;
private readonly PlayerWorldEntitySpawner playerWorldEntitySpawner;
private readonly SerializedWorldEntitySpawner serializedWorldEntitySpawner;
private readonly GeyserWorldEntitySpawner geyserWorldEntitySpawner;
private readonly ReefbackEntitySpawner reefbackEntitySpawner;
private readonly ReefbackChildEntitySpawner reefbackChildEntitySpawner;
private readonly CreatureRespawnEntitySpawner creatureRespawnEntitySpawner;
private readonly Dictionary<TechType, IWorldEntitySpawner> customSpawnersByTechType = new();
public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, Entities entities, SimulationOwnership simulationOwnership)
{
customSpawnersByTechType[TechType.Crash] = new CrashEntitySpawner();
customSpawnersByTechType[TechType.EscapePod] = new EscapePodWorldEntitySpawner(localPlayer);
customSpawnersByTechType[TechType.Creepvine] = new CreepvineEntitySpawner(defaultEntitySpawner);
vehicleWorldEntitySpawner = new VehicleWorldEntitySpawner(entities);
prefabPlaceholderEntitySpawner = new PrefabPlaceholderEntitySpawner(defaultEntitySpawner);
placeholderGroupWorldEntitySpawner = new PlaceholderGroupWorldEntitySpawner(entities, this, defaultEntitySpawner, entityMetadataManager, prefabPlaceholderEntitySpawner);
playerWorldEntitySpawner = new PlayerWorldEntitySpawner(playerManager, localPlayer);
serializedWorldEntitySpawner = new SerializedWorldEntitySpawner();
geyserWorldEntitySpawner = new GeyserWorldEntitySpawner(entities);
reefbackChildEntitySpawner = new ReefbackChildEntitySpawner();
reefbackEntitySpawner = new ReefbackEntitySpawner(reefbackChildEntitySpawner);
creatureRespawnEntitySpawner = new CreatureRespawnEntitySpawner(simulationOwnership);
}
public IWorldEntitySpawner ResolveEntitySpawner(WorldEntity entity)
{
switch (entity)
{
case PrefabPlaceholderEntity:
return prefabPlaceholderEntitySpawner;
case PlaceholderGroupWorldEntity:
return placeholderGroupWorldEntitySpawner;
case PlayerWorldEntity:
return playerWorldEntitySpawner;
case VehicleWorldEntity:
return vehicleWorldEntitySpawner;
case SerializedWorldEntity:
return serializedWorldEntitySpawner;
case GeyserWorldEntity:
return geyserWorldEntitySpawner;
case ReefbackEntity:
return reefbackEntitySpawner;
case ReefbackChildEntity:
return reefbackChildEntitySpawner;
case CreatureRespawnEntity:
return creatureRespawnEntitySpawner;
}
TechType techType = entity.TechType.ToUnity();
if (customSpawnersByTechType.TryGetValue(techType, out IWorldEntitySpawner value))
{
return value;
}
return defaultEntitySpawner;
}
}