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,24 @@
using System.Collections;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Abstract;
// Implements IEntitySpawner and allows double dispatch to cast to the right type.
public abstract class EntitySpawner<T> : IEntitySpawner where T : Entity
{
protected abstract bool SpawnsOwnChildren(T entity);
protected abstract IEnumerator SpawnAsync(T entity, TaskResult<Optional<GameObject>> result);
public bool SpawnsOwnChildren(Entity entity)
{
return SpawnsOwnChildren((T)entity);
}
public IEnumerator SpawnAsync(Entity entity, TaskResult<Optional<GameObject>> result)
{
return SpawnAsync((T)entity, result);
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Abstract;
public interface IEntitySpawner
{
IEnumerator SpawnAsync(Entity entity, TaskResult<Optional<GameObject>> result);
bool SpawnsOwnChildren(Entity entity);
}

View File

@@ -0,0 +1,13 @@
using System;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Abstract;
public interface ISyncEntitySpawner
{
bool SpawnSync(Entity entity, TaskResult<Optional<GameObject>> result);
bool SpawnSyncSafe(Entity entity, TaskResult<Optional<GameObject>> result, TaskResult<Exception> exception);
}

View File

@@ -0,0 +1,36 @@
using System;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Abstract;
public abstract class SyncEntitySpawner<T> : EntitySpawner<T>, ISyncEntitySpawner where T : Entity
{
protected abstract bool SpawnSync(T entity, TaskResult<Optional<GameObject>> result);
public bool SpawnSync(Entity entity, TaskResult<Optional<GameObject>> result)
{
return SpawnSync((T)entity, result);
}
/// <returns>The result of <see cref="SpawnSync(T,TaskResult{Optional{GameObject}})"/> or true with the caught exception </returns>
public bool SpawnSyncSafe(Entity entity, TaskResult<Optional<GameObject>> result, TaskResult<Exception> exception)
{
try
{
if (SpawnSync((T)entity, result))
{
exception.Set(null);
return true;
}
}
catch (Exception e)
{
exception.Set(e);
return true;
}
exception.Set(null);
return false;
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public class BaseLeakEntitySpawner : SyncEntitySpawner<BaseLeakEntity>
{
private readonly LiveMixinManager liveMixinManager;
public BaseLeakEntitySpawner(LiveMixinManager liveMixinManager)
{
this.liveMixinManager = liveMixinManager;
}
protected override IEnumerator SpawnAsync(BaseLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}
protected override bool SpawnsOwnChildren(BaseLeakEntity entity) => false;
protected override bool SpawnSync(BaseLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!NitroxEntity.TryGetComponentFrom(entity.ParentId, out BaseHullStrength baseHullStrength))
{
Log.Error($"[{nameof(BaseLeakEntitySpawner)}] Couldn't find a {nameof(BaseHullStrength)} from id {entity.ParentId}");
return true;
}
BaseLeakManager baseLeakManager = baseHullStrength.gameObject.EnsureComponent<BaseLeakManager>();
baseLeakManager.EnsureLeak(entity.RelativeCell.ToUnity(), entity.Id, entity.Health);
return true;
}
}

View File

@@ -0,0 +1,170 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Bases;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public class BuildEntitySpawner : EntitySpawner<BuildEntity>
{
private readonly Entities entities;
private readonly BaseLeakEntitySpawner baseLeakEntitySpawner;
public BuildEntitySpawner(Entities entities, BaseLeakEntitySpawner baseLeakEntitySpawner)
{
this.entities = entities;
this.baseLeakEntitySpawner = baseLeakEntitySpawner;
}
protected override IEnumerator SpawnAsync(BuildEntity entity, TaskResult<Optional<GameObject>> result)
{
if (NitroxEntity.TryGetObjectFrom(entity.Id, out GameObject gameObject) && gameObject)
{
Log.Error("Trying to respawn an already spawned Base without a proper resync process.");
yield break;
}
#if DEBUG
Stopwatch stopwatch = Stopwatch.StartNew();
#endif
GameObject newBase = UnityEngine.Object.Instantiate(BaseGhost._basePrefab, LargeWorldStreamer.main.globalRoot.transform, entity.Transform.LocalPosition.ToUnity(), entity.Transform.LocalRotation.ToUnity(), entity.Transform.LocalScale.ToUnity(), false);
if (LargeWorld.main)
{
LargeWorld.main.streamer.cellManager.RegisterEntity(newBase);
}
Base @base = newBase.GetComponent<Base>();
yield return SetupBase(entity, @base, entities, result);
#if DEBUG
Log.Verbose($"Took {stopwatch.ElapsedMilliseconds}ms to create the Base");
#endif
yield return entities.SpawnBatchAsync(entity.ChildEntities.OfType<PlayerWorldEntity>().ToList<Entity>());
yield return MoonpoolManager.RestoreMoonpools(entity.ChildEntities.OfType<MoonpoolEntity>(), @base);
TaskResult<Optional<GameObject>> childResult = new();
bool atLeastOneLeak = false;
foreach (Entity childEntity in entity.ChildEntities)
{
switch (childEntity)
{
case MapRoomEntity mapRoomEntity:
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
break;
case BaseLeakEntity baseLeakEntity:
atLeastOneLeak = true;
yield return baseLeakEntitySpawner.SpawnAsync(baseLeakEntity, childResult);
break;
}
}
if (atLeastOneLeak)
{
BaseHullStrength baseHullStrength = @base.GetComponent<BaseHullStrength>();
ErrorMessage.AddMessage(Language.main.GetFormat("BaseHullStrDamageDetected", baseHullStrength.totalStrength));
}
result.Set(@base.gameObject);
}
protected override bool SpawnsOwnChildren(BuildEntity entity) => true;
public static BuildEntity From(Base targetBase, EntityMetadataManager entityMetadataManager)
{
BuildEntity buildEntity = BuildEntity.MakeEmpty();
if (targetBase.TryGetNitroxId(out NitroxId baseId))
{
buildEntity.Id = baseId;
}
buildEntity.Transform = targetBase.transform.ToLocalDto();
buildEntity.BaseData = GetBaseData(targetBase);
buildEntity.ChildEntities.AddRange(BuildUtils.GetChildEntities(targetBase, baseId, entityMetadataManager));
return buildEntity;
}
public static BaseData GetBaseData(Base targetBase)
{
return new BaseData()
{
BaseShape = targetBase.baseShape.ToInt3().ToDto(),
Faces = BaseSerializationHelper.CompressData(targetBase.faces, faceType => (byte)faceType),
Cells = BaseSerializationHelper.CompressData(targetBase.cells, cellType => (byte)cellType),
Links = BaseSerializationHelper.CompressBytes(targetBase.links),
PreCompressionSize = targetBase.links.Length,
CellOffset = targetBase.cellOffset.ToDto(),
Masks = BaseSerializationHelper.CompressBytes(targetBase.masks),
IsGlass = BaseSerializationHelper.CompressData(targetBase.isGlass, isGlass => isGlass ? (byte)1 : (byte)0),
Anchor = targetBase.anchor.ToDto()
};
}
public static void ApplyBaseData(BaseData baseData, Base @base)
{
int size = baseData.PreCompressionSize;
@base.baseShape = new(); // Reset it so that the following instruction is understood as a change
@base.SetSize(baseData.BaseShape.ToUnity());
@base.faces = BaseSerializationHelper.DecompressData(baseData.Faces, size * 6, faceType => (Base.FaceType)faceType);
@base.cells = BaseSerializationHelper.DecompressData(baseData.Cells, size, cellType => (Base.CellType)cellType);
@base.links = BaseSerializationHelper.DecompressBytes(baseData.Links, size);
@base.cellOffset = new(baseData.CellOffset.ToUnity());
@base.masks = BaseSerializationHelper.DecompressBytes(baseData.Masks, size);
@base.isGlass = BaseSerializationHelper.DecompressData(baseData.IsGlass, size, num => num == 1);
@base.anchor = new(baseData.Anchor.ToUnity());
}
public static IEnumerator SetupBase(BuildEntity buildEntity, Base @base, Entities entities, TaskResult<Optional<GameObject>> result = null)
{
GameObject baseObject = @base.gameObject;
NitroxEntity.SetNewId(@base.gameObject, buildEntity.Id);
ApplyBaseData(buildEntity.BaseData, @base);
// Ghosts need an active base to be correctly spawned onto it
// While the rest must be spawned earlier for the base to load correctly (mostly InteriorPieceEntity)
// Which is why the spawn loops are separated by the SetActive instruction
// NB: We aim at spawning very precise entity types (InteriorPieceEntity, ModuleEntity and GlobalRootEntity)
// Thus we use GetType() == instead of "is GlobalRootEntity" so that derived types from it aren't selected
List<GhostEntity> ghostChildrenEntities = new();
foreach (Entity childEntity in buildEntity.ChildEntities)
{
if (childEntity is InteriorPieceEntity || childEntity is ModuleEntity ||
childEntity.GetType() == typeof(GlobalRootEntity))
{
switch (childEntity)
{
case GhostEntity ghostEntity:
ghostChildrenEntities.Add(ghostEntity);
continue;
}
yield return entities.SpawnEntityAsync(childEntity, true);
}
}
baseObject.SetActive(true);
foreach (GhostEntity childGhostEntity in ghostChildrenEntities)
{
yield return GhostEntitySpawner.RestoreGhost(@base.transform, childGhostEntity);
}
@base.OnProtoDeserialize(null);
@base.deserializationFinished = false;
@base.FinishDeserialization();
result?.Set(baseObject);
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.Overrides;
using NitroxModel.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public static class BuildingPostSpawner
{
public static IEnumerator ApplyPostSpawner(GameObject gameObject, NitroxId objectId)
{
// If we end up having more than 2-3 ifs in here in the future, create a PostSpawner generic class with detection of the required components from gameObject
if (gameObject.TryGetComponent(out Constructable constructable) && constructable.techType.Equals(TechType.Bench))
{
SetupBench(constructable.gameObject, objectId);
return null;
}
else if (gameObject.TryGetComponent(out WaterPark waterPark))
{
SetupWaterPark(waterPark, objectId);
return null;
}
return null;
}
private const int LAYER_USEABLE = 13;
/// <summary>
/// For better immersion we split the Bench in three parts (left/center/right). On each can sit one player.
/// </summary>
public static void SetupBench(GameObject gameObject, NitroxId benchId)
{
if (!gameObject.TryGetComponent(out Bench bench))
{
Log.Error($"[{nameof(BuildingPostSpawner)}] Could not find {nameof(Bench)} on {gameObject.name}");
return;
}
try
{
GameObject benchTileLeft = new("BenchPlaceLeft") { layer = LAYER_USEABLE };
benchTileLeft.transform.SetParent(gameObject.transform, false);
benchTileLeft.transform.localPosition -= new Vector3(0.75f, 0, 0);
BoxCollider benchTileLeftCollider = benchTileLeft.AddComponent<BoxCollider>();
benchTileLeftCollider.center = new Vector3(0, 0.25f, 0);
benchTileLeftCollider.size = new Vector3(0.85f, 0.5f, 0.65f);
benchTileLeftCollider.isTrigger = true;
GameObject benchTileCenter = new("BenchPlaceCenter") { layer = LAYER_USEABLE };
benchTileCenter.transform.SetParent(gameObject.transform, false);
BoxCollider benchTileCenterCollider = benchTileCenter.AddComponent<BoxCollider>();
benchTileCenterCollider.center = new Vector3(0, 0.25f, 0);
benchTileCenterCollider.size = new Vector3(0.7f, 0.5f, 0.65f);
benchTileCenterCollider.isTrigger = true;
GameObject benchTileRight = new("BenchPlaceRight") { layer = LAYER_USEABLE };
benchTileRight.transform.SetParent(gameObject.transform, false);
benchTileRight.transform.localPosition += new Vector3(0.75f, 0, 0);
BoxCollider benchTileRightCollider = benchTileRight.AddComponent<BoxCollider>();
benchTileRightCollider.center = new Vector3(0, 0.25f, 0);
benchTileRightCollider.size = new Vector3(0.85f, 0.5f, 0.65f);
benchTileRightCollider.isTrigger = true;
GameObject animationRoot = gameObject.FindChild("bench_animation");
MultiplayerBench.FromBench(bench, benchTileLeft, MultiplayerBench.Side.LEFT, animationRoot);
MultiplayerBench.FromBench(bench, benchTileCenter, MultiplayerBench.Side.CENTER, animationRoot);
MultiplayerBench.FromBench(bench, benchTileRight, MultiplayerBench.Side.RIGHT, animationRoot);
NitroxId benchLeftId = benchId.Increment();
NitroxId benchCenterId = benchLeftId.Increment();
NitroxId benchRightId = benchCenterId.Increment();
NitroxEntity.SetNewId(benchTileLeft, benchLeftId);
NitroxEntity.SetNewId(benchTileCenter, benchCenterId);
NitroxEntity.SetNewId(benchTileRight, benchRightId);
UnityEngine.Object.Destroy(bench);
UnityEngine.Object.Destroy(gameObject.FindChild("Builder Trigger"));
}
catch (Exception ex)
{
Log.Error(ex);
}
}
public static void SetupWaterPark(WaterPark waterPark, NitroxId waterParkId)
{
if (waterPark is LargeRoomWaterPark largeRoomWaterPark)
{
NitroxId leftId = waterParkId.Increment();
NitroxId rightId = leftId.Increment();
NitroxEntity.SetNewId(largeRoomWaterPark.planters.leftPlanter.gameObject, leftId);
NitroxEntity.SetNewId(largeRoomWaterPark.planters.rightPlanter.gameObject, rightId);
return;
}
NitroxId planterId = waterParkId.Increment();
NitroxEntity.SetNewId(waterPark.planter.gameObject, planterId);
}
}

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public class GhostEntitySpawner : EntitySpawner<GhostEntity>
{
protected override IEnumerator SpawnAsync(GhostEntity entity, TaskResult<Optional<GameObject>> result)
{
if (NitroxEntity.TryGetObjectFrom(entity.Id, out GameObject gameObject))
{
if (gameObject.TryGetComponent(out Constructable constructable))
{
constructable.constructedAmount = 0;
yield return constructable.ProgressDeconstruction();
}
GameObject.Destroy(gameObject);
}
Transform parent = BuildingHandler.GetParentOrGlobalRoot(entity.ParentId);
yield return RestoreGhost(parent, entity, result);
}
protected override bool SpawnsOwnChildren(GhostEntity entity) => true;
public static GhostEntity From(ConstructableBase constructableBase)
{
GhostEntity ghost = GhostEntity.MakeEmpty();
ModuleEntitySpawner.FillObject(ghost, constructableBase);
if (constructableBase.moduleFace.HasValue)
{
ghost.BaseFace = constructableBase.moduleFace.Value.ToDto();
}
ghost.BaseData = BuildEntitySpawner.GetBaseData(constructableBase.model.GetComponent<Base>());
if (constructableBase.name.Equals("BaseDeconstructable(Clone)"))
{
ghost.TechType = constructableBase.techType.ToDto();
}
if (constructableBase.TryGetComponentInChildren(out BaseGhost baseGhost, true))
{
ghost.Metadata = GhostMetadataRetriever.GetMetadataForGhost(baseGhost);
}
return ghost;
}
public static IEnumerator RestoreGhost(Transform parent, GhostEntity ghostEntity, TaskResult<Optional<GameObject>> result = null)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: ghostEntity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(ghostEntity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for ghost of ClassId {ghostEntity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
// The instructions for ghost spawning are written in a VERY PRECISE order which needs to be respected even if
// it looks like it can be optimized. Swapping some lines may break the full spawning behaviour.
bool isInBase = parent.TryGetComponent(out Base @base);
// Instantiating the ghost, gathering some useful references and giving it some basic data
GameObject ghostObject = UnityEngine.Object.Instantiate(prefab);
Transform ghostTransform = ghostObject.transform;
ConstructableBase constructableBase = ghostObject.GetComponent<ConstructableBase>();
GameObject ghostModel = constructableBase.model;
BaseGhost baseGhost = ghostModel.GetComponent<BaseGhost>();
Base ghostBase = ghostModel.GetComponent<Base>();
bool isBaseDeconstructable = ghostObject.name.Equals("BaseDeconstructable(Clone)");
MoveTransformToGhostEntity(ghostTransform, ghostEntity, false);
if (isBaseDeconstructable && ghostEntity.TechType != null)
{
constructableBase.techType = ghostEntity.TechType.ToUnity();
}
// only useful instruction in Builder.CreateGhost()
baseGhost.SetupGhost();
// ghost's Base should then be assigned its data (from BaseGhost.UpdatePlacement)
BuildEntitySpawner.ApplyBaseData(ghostEntity.BaseData, ghostBase);
ghostBase.OnProtoDeserialize(null);
if (@base)
{
@base.SetPlacementGhost(baseGhost);
}
baseGhost.targetBase = @base;
// Little fix for cell objects being already generated (wrongly)
if (ghostBase.cellObjects != null)
{
Array.Clear(ghostBase.cellObjects, 0, ghostBase.cellObjects.Length);
}
ghostBase.FinishDeserialization();
// Apply the right metadata accordingly
IEnumerator baseDeconstructableInstructions = GhostMetadataApplier.ApplyMetadataToGhost(baseGhost, ghostEntity.Metadata, @base);
if (baseDeconstructableInstructions != null)
{
yield return baseDeconstructableInstructions;
}
// Verify that the metadata didn't destroy the GameObject (possible in GhostMetadataApplier.ApplyBaseDeconstructableMetadataTo)
if (!ghostObject)
{
yield break;
}
// From ConstructableBase.OnProtoDeserialize()
// NB: Very important to fix the ghost visual glitch where the renderer is wrongly placed
constructableBase.SetGhostVisible(false);
// The rest is from Builder.TryPlace
if (!isBaseDeconstructable)
{
// Not executed by BaseDeconstructable
baseGhost.Place();
}
if (isInBase)
{
ghostTransform.parent = parent;
MoveTransformToGhostEntity(ghostTransform, ghostEntity);
}
constructableBase.SetState(false, false);
constructableBase.constructedAmount = ghostEntity.ConstructedAmount;
// Addition to ensure visuals appear correctly (would be called from OnGlobalEntitiesLoaded)
yield return constructableBase.ReplaceMaterialsAsync();
if (isBaseDeconstructable)
{
baseGhost.DisableGhostModelScripts();
}
NitroxEntity.SetNewId(ghostObject, ghostEntity.Id);
result?.Set(ghostObject);
}
private static void MoveTransformToGhostEntity(Transform transform, GhostEntity ghostEntity, bool localCoordinates = true)
{
if (localCoordinates)
{
transform.localPosition = ghostEntity.Transform.LocalPosition.ToUnity();
transform.localRotation = ghostEntity.Transform.LocalRotation.ToUnity();
transform.localScale = ghostEntity.Transform.LocalScale.ToUnity();
}
else
{
// TODO: Once fixed, use NitroxTransform.Position and Rotation instead of locals
// Current issue is NitroxTransform doesn't have a reparenting behaviour when deserialized on client-side
transform.SetPositionAndRotation(ghostEntity.Transform.LocalPosition.ToUnity(), ghostEntity.Transform.LocalRotation.ToUnity());
transform.localScale = ghostEntity.Transform.LocalScale.ToUnity();
}
}
}

View File

@@ -0,0 +1,183 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public class InteriorPieceEntitySpawner : EntitySpawner<InteriorPieceEntity>
{
private readonly Entities entities;
private readonly EntityMetadataManager entityMetadataManager;
public InteriorPieceEntitySpawner(Entities entities, EntityMetadataManager entityMetadataManager)
{
this.entities = entities;
this.entityMetadataManager = entityMetadataManager;
}
protected override IEnumerator SpawnAsync(InteriorPieceEntity entity, TaskResult<Optional<GameObject>> result)
{
if (entity.ParentId == null || !NitroxEntity.TryGetComponentFrom(entity.ParentId, out Base @base))
{
Log.Error($"Couldn't find a Base component on the parent object of InteriorPieceEntity {entity.Id}");
yield break;
}
yield return RestoreInteriorPiece(entity, @base, result);
if (!result.Get().HasValue)
{
Log.Error($"Restoring interior piece failed: {entity}");
yield break;
}
bool isWaterPark = entity.IsWaterPark;
List<Entity> batch = new();
foreach (Entity childEntity in entity.ChildEntities)
{
switch(childEntity)
{
case InventoryItemEntity:
case InstalledModuleEntity:
batch.Add(childEntity);
break;
case PlanterEntity:
foreach (InventoryItemEntity childItemEntity in childEntity.ChildEntities.OfType<InventoryItemEntity>())
{
batch.Add(childItemEntity);
}
break;
case WorldEntity:
if (isWaterPark)
{
batch.Add(childEntity);
}
break;
}
}
if (isWaterPark)
{
// Must happen before child plant spawning
foreach (Planter planter in result.Get().Value.GetComponentsInChildren<Planter>(true))
{
yield return planter.DeserializeAsync();
}
}
yield return entities.SpawnBatchAsync(batch, true);
if (result.Get().Value.TryGetComponent(out PowerSource powerSource))
{
// TODO: Have synced/restored power
powerSource.SetPower(powerSource.maxPower);
}
}
protected override bool SpawnsOwnChildren(InteriorPieceEntity entity) => true;
public IEnumerator RestoreInteriorPiece(InteriorPieceEntity interiorPiece, Base @base, TaskResult<Optional<GameObject>> result = null)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: interiorPiece.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(interiorPiece.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for interior piece of ClassId {interiorPiece.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
Base.Face face = interiorPiece.BaseFace.ToUnity();
face.cell += @base.GetAnchor();
GameObject moduleObject = @base.SpawnModule(prefab, face);
if (moduleObject)
{
NitroxEntity.SetNewId(moduleObject, interiorPiece.Id);
yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, interiorPiece.Id);
entityMetadataManager.ApplyMetadata(moduleObject, interiorPiece.Metadata);
result.Set(moduleObject);
}
}
public static InteriorPieceEntity From(IBaseModule module, EntityMetadataManager entityMetadataManager)
{
InteriorPieceEntity interiorPiece = InteriorPieceEntity.MakeEmpty();
GameObject gameObject = (module as Component).gameObject;
if (gameObject && gameObject.TryGetComponent(out PrefabIdentifier identifier))
{
interiorPiece.ClassId = identifier.ClassId;
}
else
{
Log.Warn($"Couldn't find an identifier for the interior piece {module.GetType()}");
}
if (gameObject.TryGetIdOrWarn(out NitroxId entityId))
{
interiorPiece.Id = entityId;
}
if (gameObject.TryGetComponentInParent(out Base parentBase, true) &&
parentBase.TryGetNitroxId(out NitroxId parentId))
{
interiorPiece.ParentId = parentId;
}
switch (module)
{
case LargeRoomWaterPark:
PlanterEntity leftPlanter = new(interiorPiece.Id.Increment(), interiorPiece.Id);
PlanterEntity rightPlanter = new(leftPlanter.Id.Increment(), interiorPiece.Id);
interiorPiece.ChildEntities.Add(leftPlanter);
interiorPiece.ChildEntities.Add(rightPlanter);
break;
// When you deconstruct (not entirely) then construct back those pieces, they keep their inventories
case BaseNuclearReactor baseNuclearReactor:
interiorPiece.ChildEntities.AddRange(Items.GetEquipmentModuleEntities(baseNuclearReactor.equipment, entityId, entityMetadataManager));
break;
case BaseBioReactor baseBioReactor:
foreach (ItemsContainer.ItemGroup itemGroup in baseBioReactor.container._items.Values)
{
foreach (InventoryItem item in itemGroup.items)
{
interiorPiece.ChildEntities.Add(Items.ConvertToInventoryItemEntity(item.item.gameObject, interiorPiece.Id, entityMetadataManager));
}
}
break;
case WaterPark:
PlanterEntity planter = new(interiorPiece.Id.Increment(), interiorPiece.Id);
interiorPiece.ChildEntities.Add(planter);
break;
}
interiorPiece.BaseFace = module.moduleFace.ToDto();
return interiorPiece;
}
public static IEnumerator RestoreMapRoom(Base @base, MapRoomEntity mapRoomEntity)
{
MapRoomFunctionality mapRoomFunctionality = @base.GetMapRoomFunctionalityForCell(mapRoomEntity.Cell.ToUnity());
if (!mapRoomFunctionality)
{
Log.Error($"Couldn't find MapRoomFunctionality in base for cell {mapRoomEntity.Cell}");
yield break;
}
NitroxEntity.SetNewId(mapRoomFunctionality.gameObject, mapRoomEntity.Id);
}
}

View File

@@ -0,0 +1,159 @@
using System.Collections;
using System.Linq;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Bases;
public class ModuleEntitySpawner : EntitySpawner<ModuleEntity>
{
private readonly Entities entities;
public ModuleEntitySpawner(Entities entities)
{
this.entities = entities;
}
protected override IEnumerator SpawnAsync(ModuleEntity entity, TaskResult<Optional<GameObject>> result)
{
if (NitroxEntity.TryGetObjectFrom(entity.Id, out GameObject gameObject) && gameObject)
{
Log.Error("Trying to respawn an already spawned module without a proper resync process.");
yield break;
}
Transform parent = BuildingHandler.GetParentOrGlobalRoot(entity.ParentId);
yield return RestoreModule(parent, entity, result);
if (!result.Get().HasValue)
{
Log.Error($"Module couldn't be spawned {entity}");
yield break;
}
GameObject moduleObject = result.Get().Value;
Optional<ItemsContainer> opContainer = InventoryContainerHelper.TryGetContainerByOwner(moduleObject);
if (opContainer.HasValue)
{
yield return entities.SpawnBatchAsync(entity.ChildEntities.OfType<InventoryItemEntity>().ToList<Entity>(), true);
}
Optional<Equipment> opEquipment = EquipmentHelper.FindEquipmentComponent(moduleObject);
if (opEquipment.HasValue)
{
yield return entities.SpawnBatchAsync(entity.ChildEntities.OfType<InstalledModuleEntity>().ToList<Entity>(), true);
}
if (moduleObject.TryGetComponent(out PowerSource powerSource))
{
// TODO: Have synced/restored power
powerSource.SetPower(powerSource.maxPower);
}
}
protected override bool SpawnsOwnChildren(ModuleEntity entity) => true;
public static IEnumerator RestoreModule(Transform parent, ModuleEntity moduleEntity, TaskResult<Optional<GameObject>> result = null)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, classId: moduleEntity.ClassId))
{
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(moduleEntity.ClassId, prefabResult);
if (!prefabResult.Get())
{
Log.Error($"Couldn't find a prefab for module of ClassId {moduleEntity.ClassId}");
yield break;
}
prefab = prefabResult.Get();
}
GameObject moduleObject = UnityEngine.Object.Instantiate(prefab);
Transform moduleTransform = moduleObject.transform;
moduleTransform.parent = parent;
moduleTransform.localPosition = moduleEntity.Transform.LocalPosition.ToUnity();
moduleTransform.localRotation = moduleEntity.Transform.LocalRotation.ToUnity();
moduleTransform.localScale = moduleEntity.Transform.LocalScale.ToUnity();
ApplyModuleData(moduleEntity, moduleObject, result);
MoveToGlobalRoot(moduleObject);
if (parent && parent.TryGetComponent(out NitroxCyclops nitroxCyclops) && nitroxCyclops.Virtual)
{
nitroxCyclops.Virtual.ReplicateConstructable(moduleObject.GetComponent<Constructable>());
}
yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, moduleEntity.Id);
}
public static void ApplyModuleData(ModuleEntity moduleEntity, GameObject moduleObject, TaskResult<Optional<GameObject>> result = null)
{
Constructable constructable = moduleObject.GetComponent<Constructable>();
constructable.SetIsInside(moduleEntity.IsInside);
if (moduleEntity.IsInside)
{
SkyEnvironmentChanged.Send(moduleObject, moduleObject.GetComponentInParent<SubRoot>(true));
}
else
{
SkyEnvironmentChanged.Send(moduleObject, (Component)null);
}
constructable.constructedAmount = moduleEntity.ConstructedAmount;
constructable.SetState(moduleEntity.ConstructedAmount >= 1f, false);
constructable.UpdateMaterial();
NitroxEntity.SetNewId(moduleObject, moduleEntity.Id);
result?.Set(moduleObject);
}
public static void FillObject(ModuleEntity moduleEntity, Constructable constructable)
{
moduleEntity.ClassId = constructable.GetComponent<PrefabIdentifier>().ClassId;
if (constructable.TryGetNitroxId(out NitroxId entityId))
{
moduleEntity.Id = entityId;
}
if (constructable.TryGetComponentInParent(out Base parentBase, true) &&
parentBase.TryGetNitroxId(out NitroxId parentId))
{
moduleEntity.ParentId = parentId;
}
moduleEntity.Transform = constructable.transform.ToLocalDto();
moduleEntity.TechType = constructable.techType.ToDto();
moduleEntity.ConstructedAmount = constructable.constructedAmount;
moduleEntity.IsInside = constructable.isInside;
}
/// <summary>
/// We don't want constructables to be put in CellRoots but in GlobalRoot, because when a player has simulation ownership over a base,
/// they also need to keep loaded everything which could be related to the said base (e.g. power relays)
/// </summary>
public static void MoveToGlobalRoot(GameObject gameObject)
{
if (!gameObject.TryGetComponent(out LargeWorldEntity largeWorldEntity))
{
return;
}
largeWorldEntity.cellLevel = LargeWorldEntity.CellLevel.Global;
largeWorldEntity.Start();
}
public static ModuleEntity From(Constructable constructable)
{
ModuleEntity module = ModuleEntity.MakeEmpty();
FillObject(module, constructable);
return module;
}
}

View File

@@ -0,0 +1,89 @@
using System.Collections;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning;
public class InstalledBatteryEntitySpawner : SyncEntitySpawner<InstalledBatteryEntity>
{
protected override IEnumerator SpawnAsync(InstalledBatteryEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!CanSpawn(entity, out EnergyMixin energyMixin, out string errorLog))
{
Log.Error(errorLog);
result.Set(Optional.Empty);
yield break;
}
TaskResult<GameObject> prefabResult = new();
yield return DefaultWorldEntitySpawner.RequestPrefab(entity.TechType.ToUnity(), prefabResult);
GameObject gameObject = GameObjectHelper.InstantiateWithId(prefabResult.Get(), entity.Id);
SetupObject(gameObject, energyMixin);
result.Set(gameObject);
}
protected override bool SpawnSync(InstalledBatteryEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, entity.TechType.ToUnity()))
{
return false;
}
if (!CanSpawn(entity, out EnergyMixin energyMixin, out string errorLog))
{
Log.Error(errorLog);
return true;
}
GameObject gameObject = GameObjectHelper.SpawnFromPrefab(prefab, entity.Id);
SetupObject(gameObject, energyMixin);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(InstalledBatteryEntity entity) => false;
private bool CanSpawn(Entity entity, out EnergyMixin energyMixin, out string errorLog)
{
if (!NitroxEntity.TryGetObjectFrom(entity.ParentId, out GameObject parentObject))
{
energyMixin = null;
errorLog = $"Unable to find parent to install battery {entity}";
return false;
}
energyMixin = parentObject.GetComponent<EnergyMixin>();
if (!energyMixin)
{
errorLog = $"Unable to find EnergyMixin on parent to install battery {entity}";
return false;
}
errorLog = null;
return true;
}
private void SetupObject(GameObject gameObject, EnergyMixin energyMixin)
{
energyMixin.Initialize();
energyMixin.RestoreBattery();
using (PacketSuppressor<EntityReparented>.Suppress())
using (PacketSuppressor<EntitySpawnedByClient>.Suppress())
{
energyMixin.batterySlot.AddItem(new InventoryItem(gameObject.GetComponent<Pickupable>()));
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.WorldEntities;
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;
public class InstalledModuleEntitySpawner : SyncEntitySpawner<InstalledModuleEntity>
{
protected override IEnumerator SpawnAsync(InstalledModuleEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!CanSpawn(entity, out GameObject parentObject, out Equipment equipment, out string errorLog))
{
Log.Info(errorLog);
result.Set(Optional.Empty);
yield break;
}
TaskResult<GameObject> gameObjectResult = new();
yield return DefaultWorldEntitySpawner.CreateGameObject(entity.TechType.ToUnity(), entity.ClassId, entity.Id, gameObjectResult);
GameObject gameObject = gameObjectResult.Get();
SetupObject(entity, gameObject, parentObject, equipment);
result.Set(Optional.Of(gameObject));
}
protected override bool SpawnSync(InstalledModuleEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, entity.TechType.ToUnity(), entity.ClassId))
{
return false;
}
if (!CanSpawn(entity, out GameObject parentObject, out Equipment equipment, out string errorLog))
{
Log.Error(errorLog);
return true;
}
GameObject gameObject = GameObjectHelper.SpawnFromPrefab(prefab, entity.Id);
SetupObject(entity, gameObject, parentObject, equipment);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(InstalledModuleEntity entity) => false;
private bool CanSpawn(InstalledModuleEntity entity, out GameObject parentObject, out Equipment equipment, out string errorLog)
{
if (!NitroxEntity.TryGetObjectFrom(entity.ParentId, out parentObject))
{
equipment = null;
errorLog = $"Unable to find inventory container with id {entity.Id} for {entity}";
return false;
}
// The game considers modules as vehicle equipment. Get the container and install it into the required slot.
Optional<Equipment> opEquipment = EquipmentHelper.FindEquipmentComponent(parentObject);
if (!opEquipment.HasValue)
{
equipment = null;
errorLog = $"Unable to find equipment container inside {parentObject}";
return false;
}
equipment = opEquipment.Value;
errorLog = null;
return true;
}
private void SetupObject(InstalledModuleEntity entity, GameObject gameObject, GameObject parentObject, Equipment equipment)
{
Pickupable pickupable = gameObject.RequireComponent<Pickupable>();
pickupable.Initialize();
InventoryItem inventoryItem = new(pickupable)
{
container = equipment
};
inventoryItem.item.Reparent(equipment.tr);
equipment.equipment[entity.Slot] = inventoryItem;
equipment.UpdateCount(pickupable.GetTechType(), true);
Equipment.SendEquipmentEvent(pickupable, 0, parentObject, entity.Slot);
equipment.NotifyEquip(entity.Slot, inventoryItem);
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning;
public class InventoryEntitySpawner : SyncEntitySpawner<InventoryEntity>
{
protected override IEnumerator SpawnAsync(InventoryEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}
protected override bool SpawnSync(InventoryEntity entity, TaskResult<Optional<GameObject>> result)
{
GameObject parent = NitroxEntity.RequireObjectFrom(entity.ParentId);
StorageContainer container = parent.GetAllComponentsInChildren<StorageContainer>()
.ElementAt(entity.ComponentIndex);
if (container)
{
NitroxEntity.SetNewId(container.gameObject, entity.Id);
result.Set(Optional.OfNullable(container.gameObject));
}
else
{
Log.Error($"Unable to find {nameof(StorageContainer)} for: {entity}");
result.Set(Optional.Empty);
}
return true;
}
protected override bool SpawnsOwnChildren(InventoryEntity entity) => false;
}

View File

@@ -0,0 +1,183 @@
using System;
using System.Collections;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using UWE;
namespace NitroxClient.GameLogic.Spawning;
public class InventoryItemEntitySpawner(EntityMetadataManager entityMetadataManager) : SyncEntitySpawner<InventoryItemEntity>
{
private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager;
protected override IEnumerator SpawnAsync(InventoryItemEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog))
{
Log.Info(errorLog);
result.Set(Optional.Empty);
yield break;
}
TaskResult<GameObject> gameObjectResult = new();
yield return DefaultWorldEntitySpawner.CreateGameObject(entity.TechType.ToUnity(), entity.ClassId, entity.Id, gameObjectResult);
GameObject gameObject = gameObjectResult.Get();
SetupObject(entity, gameObject, parentObject, container);
result.Set(Optional.Of(gameObject));
}
protected override bool SpawnSync(InventoryItemEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!DefaultWorldEntitySpawner.TryGetCachedPrefab(out GameObject prefab, entity.TechType.ToUnity(), entity.ClassId))
{
return false;
}
if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog))
{
Log.Error(errorLog);
return true;
}
GameObject gameObject = GameObjectHelper.SpawnFromPrefab(prefab, entity.Id);
SetupObject(entity, gameObject, parentObject, container);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(InventoryItemEntity entity) => false;
private bool CanSpawn(InventoryItemEntity entity, out GameObject parentObject, out ItemsContainer container, out string errorLog)
{
Optional<GameObject> owner = NitroxEntity.GetObjectFrom(entity.ParentId);
if (!owner.HasValue)
{
parentObject = null;
container = null;
errorLog = $"Unable to find inventory container with id {entity.Id} for {entity}";
return false;
}
Optional<ItemsContainer> opContainer = InventoryContainerHelper.TryGetContainerByOwner(owner.Value);
if (!opContainer.HasValue)
{
parentObject = null;
container = null;
errorLog = $"Could not find container field on GameObject {parentObject.AliveOrNull()?.GetFullHierarchyPath()}";
return false;
}
parentObject = owner.Value;
container = opContainer.Value;
errorLog = null;
return true;
}
private void SetupObject(InventoryItemEntity entity, GameObject gameObject, GameObject parentObject, ItemsContainer container)
{
Pickupable pickupable = gameObject.RequireComponent<Pickupable>();
pickupable.Initialize();
InventoryItem inventoryItem = new(pickupable);
// Items eventually get "secured" once a player gets into a SubRoot (or for other reasons) so we need to force this state by default
// so that player don't risk their whole inventory if they reconnect in the water.
pickupable.destroyOnDeath = false;
bool isPlanter = parentObject.TryGetComponent(out Planter planter);
bool subscribedValue = false;
if (isPlanter)
{
subscribedValue = planter.subscribed;
planter.Subscribe(false);
}
using (PacketSuppressor<EntityReparented>.Suppress())
using (PacketSuppressor<PlayerQuickSlotsBindingChanged>.Suppress())
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
using (PacketSuppressor<EntitySpawnedByClient>.Suppress())
{
container.UnsafeAdd(inventoryItem);
Log.Debug($"Received: Added item {pickupable.GetTechType()} ({entity.Id}) to container {parentObject.GetFullHierarchyPath()}");
}
if (isPlanter)
{
planter.Subscribe(subscribedValue);
if (entity.Metadata is PlantableMetadata metadata)
{
PostponeAddNotification(() => planter.subscribed, () => planter, true, () =>
{
// Adapted from Planter.AddItem(InventoryItem) to be able to call directly AddItem(Plantable, slotID) with our parameters
Plantable plantable = pickupable.GetComponent<Plantable>();
pickupable.SetTechTypeOverride(plantable.plantTechType, false);
inventoryItem.isEnabled = false;
planter.AddItem(plantable, metadata.SlotID);
// Apply the plantable metadata after the GrowingPlant (or the GrownPlant) is spawned
// this will allow the GrowingPlant to know about its progress
entityMetadataManager.ApplyMetadata(plantable.gameObject, metadata);
// Plant spawning occurs in multiple steps over frames:
// spawning the item, adding it to the planter, having the GrowingPlant created, and eventually having it create a GrownPlant (when progress == 1)
// therefore we give the metadata to the object so it can be used when required
if (metadata.FruitPlantMetadata != null && plantable.growingPlant && plantable.growingPlant.GetProgress() == 1f)
{
plantable.growingPlant.AddReference(metadata.FruitPlantMetadata);
}
// 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
});
}
}
else if (parentObject.TryGetComponent(out Trashcan trashcan))
{
PostponeAddNotification(() => trashcan.subscribed, () => trashcan, false, () =>
{
trashcan.AddItem(inventoryItem);
});
}
}
private static void PostponeAddNotification(Func<bool> subscribed, Func<bool> instanceValid, bool callbackIfAlreadySubscribed, Action callback)
{
IEnumerator PostponedAddCallback()
{
yield return new WaitUntil(() => subscribed() || !instanceValid());
if (instanceValid())
{
using (PacketSuppressor<EntityReparented>.Suppress())
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
callback();
}
}
}
if (!subscribed())
{
CoroutineHost.StartCoroutine(PostponedAddCallback());
}
else if (callbackIfAlreadySubscribed)
{
callback();
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata;
public class BeaconMetadataProcessor : EntityMetadataProcessor<BeaconMetadata>
{
public override void ProcessMetadata(GameObject gameObject, BeaconMetadata metadata)
{
if (gameObject.TryGetComponent(out Beacon beacon))
{
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
// Must set Beacon.label so that Beacon.Start() doesn't overwrite Beacon.beaconLabel's label
beacon.label = metadata.Label;
beacon.beaconLabel.SetLabel(metadata.Label);
}
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata;
public class EntityMetadataManager
{
private readonly Dictionary<Type, IEntityMetadataExtractor> extractors;
private readonly Dictionary<Type, IEntityMetadataProcessor> processors;
/// <summary>
/// As it takes some time spawning lots of entities, it can happen that we receive metadata updates for those which are still in the queue.
/// Therefore we list the updates which happened while we didn't have the said objects loaded
/// </summary>
private readonly Dictionary<NitroxId, EntityMetadata> newerMetadataById = new();
public EntityMetadataManager(IEnumerable<IEntityMetadataExtractor> extractors, IEnumerable<IEntityMetadataProcessor> processors)
{
this.extractors = extractors.ToDictionary(p => p.GetType().BaseType.GetGenericArguments()[0]);
this.processors = processors.ToDictionary(p => p.GetType().BaseType.GetGenericArguments()[0]);
}
public Optional<EntityMetadata> Extract(object o)
{
if (extractors.TryGetValue(o.GetType(), out IEntityMetadataExtractor extractor))
{
return extractor.From(o);
}
return Optional.Empty;
}
public Optional<EntityMetadata> Extract(GameObject o)
{
foreach (Component component in o.GetComponents<Component>())
{
if (extractors.TryGetValue(component.GetType(), out IEntityMetadataExtractor extractor))
{
Optional<EntityMetadata> metadata = extractor.From(component);
if (metadata.HasValue)
{
return metadata;
}
}
}
return Optional.Empty;
}
public bool TryExtract(object o, out EntityMetadata metadata)
{
Optional<EntityMetadata> opMetadata = Extract(o);
metadata = opMetadata.Value;
return opMetadata.HasValue;
}
public Optional<IEntityMetadataProcessor> FromMetaData(EntityMetadata metadata)
{
if (metadata != null && processors.TryGetValue(metadata.GetType(), out IEntityMetadataProcessor processor))
{
return Optional.Of(processor);
}
return Optional.Empty;
}
public void ApplyMetadata(GameObject gameObject, EntityMetadata metadata)
{
// In case a metadata update was received while this entity was in the spawn queue
if (gameObject.TryGetNitroxId(out NitroxId objectId) &&
newerMetadataById.TryGetValue(objectId, out EntityMetadata newMetadata))
{
metadata = newMetadata;
newerMetadataById.Remove(objectId);
}
Optional<IEntityMetadataProcessor> metadataProcessor = FromMetaData(metadata);
if (metadataProcessor.HasValue)
{
metadataProcessor.Value.ProcessMetadata(gameObject, metadata);
}
}
public void RegisterNewerMetadata(NitroxId entityId, EntityMetadata metadata)
{
newerMetadataById[entityId] = metadata;
}
public void ClearNewerMetadata()
{
newerMetadataById.Clear();
}
}

View File

@@ -0,0 +1,22 @@
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
public abstract class EntityMetadataExtractor<I, O> : IEntityMetadataExtractor where O : EntityMetadata
{
public abstract O Extract(I entity);
public Optional<EntityMetadata> From(object o)
{
EntityMetadata result = Extract((I)o);
return Optional.OfNullable(result);
}
protected T Resolve<T>() where T : class
{
return NitroxServiceLocator.Cache<T>.Value;
}
}

View File

@@ -0,0 +1,9 @@
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
public interface IEntityMetadataExtractor
{
public Optional<EntityMetadata> From(object o);
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class BatteryMetadataExtractor : EntityMetadataExtractor<Battery, BatteryMetadata>
{
public override BatteryMetadata Extract(Battery entity)
{
return new(entity._charge);
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class BeaconMetadataExtractor : EntityMetadataExtractor<Beacon, BeaconMetadata>
{
public override BeaconMetadata Extract(Beacon beacon)
{
return new(beacon.beaconLabel.GetLabel());
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class ConstructorMetadataExtractor : EntityMetadataExtractor<Constructor, ConstructorMetadata>
{
public override ConstructorMetadata Extract(Constructor entity)
{
return new(entity.deployed);
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class CrashHomeMetadataExtractor : EntityMetadataExtractor<CrashHome, CrashHomeMetadata>
{
public override CrashHomeMetadata Extract(CrashHome crashHome)
{
return new(crashHome.spawnTime);
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class CyclopsLightingMetadataExtractor : EntityMetadataExtractor<CyclopsLightingPanel, CyclopsLightingMetadata>
{
public override CyclopsLightingMetadata Extract(CyclopsLightingPanel lighting)
{
return new(lighting.floodlightsOn, lighting.lightingOn);
}
}

View File

@@ -0,0 +1,41 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
using static NitroxClient.GameLogic.Spawning.Metadata.Extractor.CyclopsMetadataExtractor;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class CyclopsMetadataExtractor : EntityMetadataExtractor<CyclopsGameObject, CyclopsMetadata>
{
public override CyclopsMetadata Extract(CyclopsGameObject cyclops)
{
GameObject gameObject = cyclops.GameObject;
CyclopsSilentRunningAbilityButton silentRunning = gameObject.RequireComponentInChildren<CyclopsSilentRunningAbilityButton>(true);
CyclopsEngineChangeState engineState = gameObject.RequireComponentInChildren<CyclopsEngineChangeState>(true);
bool engineShuttingDown = (engineState.motorMode.engineOn && engineState.invalidButton);
bool engineOn = (engineState.startEngine || engineState.motorMode.engineOn) && !engineShuttingDown;
CyclopsShieldButton shield = gameObject.GetComponentInChildren<CyclopsShieldButton>(true);
bool shieldOn = (shield) ? shield.active : false;
CyclopsSonarButton sonarButton = gameObject.GetComponentInChildren<CyclopsSonarButton>(true);
bool sonarOn = (sonarButton) ? sonarButton._sonarActive : false;
CyclopsMotorMode.CyclopsMotorModes motorMode = engineState.motorMode.cyclopsMotorMode;
LiveMixin liveMixin = gameObject.RequireComponentInChildren<LiveMixin>();
float health = liveMixin.health;
SubRoot subRoot = gameObject.RequireComponentInChildren<SubRoot>();
bool isDestroyed = subRoot.subDestroyed || health <= 0f;
return new(silentRunning.active, shieldOn, sonarOn, engineOn, (int)motorMode, health, isDestroyed);
}
public struct CyclopsGameObject
{
public GameObject GameObject { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class EatableMetadataExtractor : EntityMetadataExtractor<Eatable, EatableMetadata>
{
public override EatableMetadata Extract(Eatable eatable)
{
if (eatable.decomposes)
{
return new(eatable.timeDecayStart);
}
return null;
}
}

View File

@@ -0,0 +1,18 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class EggMetadataExtractor : EntityMetadataExtractor<CreatureEgg, EggMetadata>
{
public override EggMetadata Extract(CreatureEgg creatureEgg)
{
// If the egg is not in a water park (when being picked up or dropped outside of one),
// we only need the exact progress value because progress only increases while inside a water park
if (Items.PickingUpObject == creatureEgg.gameObject || !creatureEgg.insideWaterPark)
{
return new(-1f, creatureEgg.progress);
}
return new(creatureEgg.timeStartHatching, creatureEgg.progress);
}
}

View File

@@ -0,0 +1,14 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class EscapePodMetadataExtractor : EntityMetadataExtractor<EscapePod, EscapePodMetadata>
{
public override EscapePodMetadata Extract(EscapePod entity)
{
Radio radio = entity.radioSpawner.spawnedObj.RequireComponent<Radio>();
return new EscapePodMetadata(entity.liveMixin.IsFullHealth(), radio.liveMixin.IsFullHealth());
}
}

View File

@@ -0,0 +1,15 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class ExosuitMetadataExtractor : EntityMetadataExtractor<Exosuit, ExosuitMetadata>
{
public override ExosuitMetadata Extract(Exosuit exosuit)
{
LiveMixin liveMixin = exosuit.liveMixin;
SubName subName = exosuit.subName;
return new(liveMixin.health, SubNameInputMetadataExtractor.GetName(subName), SubNameInputMetadataExtractor.GetColors(subName));
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class FireExtinguisherHolderMetadataExtractor : EntityMetadataExtractor<FireExtinguisherHolder, FireExtinguisherHolderMetadata>
{
public override FireExtinguisherHolderMetadata Extract(FireExtinguisherHolder entity)
{
return new(entity.hasTank, entity.fuel);
}
}

View File

@@ -0,0 +1,17 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class FlareMetadataExtractor : EntityMetadataExtractor<Flare, FlareMetadata>
{
public override FlareMetadata Extract(Flare flare)
{
// If the flare is thrown, set its activation time
if (flare.flareActiveState && Items.PickingUpObject != flare.gameObject)
{
return new(flare.energyLeft, flare.hasBeenThrown, flare.flareActivateTime);
}
return new(flare.energyLeft, flare.hasBeenThrown, null);
}
}

View File

@@ -0,0 +1,15 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class FlashlightMetadataExtractor : EntityMetadataExtractor<FlashLight, FlashlightMetadata>
{
public override FlashlightMetadata Extract(FlashLight entity)
{
ToggleLights lights = entity.RequireComponent<ToggleLights>();
return new(lights.lightsActive);
}
}

View File

@@ -0,0 +1,22 @@
using System.Linq;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class FruitPlantMetadataExtractor : EntityMetadataExtractor<FruitPlant, FruitPlantMetadata>
{
public override FruitPlantMetadata Extract(FruitPlant fruitPlant)
{
bool[] prefabsPicked = fruitPlant.fruits.Select(prefab => prefab.pickedState).ToArray();
// If fruit spawn is disabled (certain plants like kelp don't regrow their fruits) and if none of the fruits were picked (all picked = false)
// then we don't need to save this data because the plant is spawned like this by default
if (!fruitPlant.fruitSpawnEnabled && prefabsPicked.All(b => !b))
{
return null;
}
return new(prefabsPicked, fruitPlant.timeNextFruit);
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class PlantableMetadataExtractor(FruitPlantMetadataExtractor fruitPlantMetadataExtractor) : EntityMetadataExtractor<Plantable, PlantableMetadata>
{
private readonly FruitPlantMetadataExtractor fruitPlantMetadataExtractor = fruitPlantMetadataExtractor;
public override PlantableMetadata Extract(Plantable plantable)
{
// Default value for no progress is -1
PlantableMetadata metadata = new(plantable.growingPlant ? plantable.growingPlant.timeStartGrowth : -1, plantable.GetSlotID());
// TODO: Refer to the TODO in PlantableMetadata
if (plantable.linkedGrownPlant && plantable.linkedGrownPlant.TryGetComponent(out FruitPlant fruitPlant))
{
metadata.FruitPlantMetadata = fruitPlantMetadataExtractor.Extract(fruitPlant);
}
return metadata;
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
using static NitroxModel.DataStructures.GameLogic.Entities.Metadata.PlayerMetadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class PlayerMetadataExtractor : EntityMetadataExtractor<Player, PlayerMetadata>
{
public override PlayerMetadata Extract(Player player)
{
return new PlayerMetadata(ExtractEquippedItems());
}
private List<EquippedItem> ExtractEquippedItems()
{
Equipment equipment = Inventory.main.equipment;
List<EquippedItem> equipped = new();
foreach (KeyValuePair<string, InventoryItem> slotWithItem in equipment.equipment)
{
InventoryItem item = slotWithItem.Value;
// not every slot will always contain an item.
if (item != null && item.item.TryGetIdOrWarn(out NitroxId itemId))
{
equipped.Add(new EquippedItem(itemId, slotWithItem.Key, item.techType.ToDto()));
}
}
return equipped;
}
}

View File

@@ -0,0 +1,14 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class RadiationMetadataExtractor : EntityMetadataExtractor<RadiationLeak, RadiationMetadata>
{
public override RadiationMetadata Extract(RadiationLeak leak)
{
// Note: this extractor should only be used when this radiation leak is being repaired
float realTimeFix = leak.liveMixin.IsFullHealth() ? (float)Resolve<TimeManager>().RealTimeElapsed : -1;
return new(leak.liveMixin.health, realTimeFix);
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class RocketMetadataExtractor : EntityMetadataExtractor<Rocket, RocketMetadata>
{
public override RocketMetadata Extract(Rocket rocket)
{
RocketPreflightCheckManager rocketPreflightCheckManager = rocket.RequireComponent<RocketPreflightCheckManager>();
List<int> prechecks = rocketPreflightCheckManager.preflightChecks.Select(i => (int)i).ToList();
return new(rocket.currentRocketStage, DayNightCycle.main.timePassedAsFloat, (int)rocket.elevatorState, rocket.elevatorPosition, prechecks);
}
}

View File

@@ -0,0 +1,18 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class SeaTreaderMetadataExtractor : EntityMetadataExtractor<SeaTreader, SeaTreaderMetadata>
{
public override SeaTreaderMetadata Extract(SeaTreader seaTreader)
{
if (!DayNightCycle.main)
{
return null;
}
float grazingEndTime = DayNightCycle.main.timePassedAsFloat + seaTreader.grazingTimeLeft;
return new(seaTreader.reverseDirection, grazingEndTime, seaTreader.leashPosition.ToDto());
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class SealedDoorMetadataExtractor : EntityMetadataExtractor<Sealed, SealedDoorMetadata>
{
public override SealedDoorMetadata Extract(Sealed entity)
{
return new(entity._sealed, entity.openedAmount);
}
}

View File

@@ -0,0 +1,16 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class SeamothMetadataExtractor : EntityMetadataExtractor<SeaMoth, SeamothMetadata>
{
public override SeamothMetadata Extract(SeaMoth seamoth)
{
bool lightsOn = (seamoth.toggleLights) ? seamoth.toggleLights.GetLightsActive() : true;
LiveMixin liveMixin = seamoth.liveMixin;
SubName subName = seamoth.subName;
return new(lightsOn, liveMixin.health, SubNameInputMetadataExtractor.GetName(subName), SubNameInputMetadataExtractor.GetColors(subName));
}
}

View File

@@ -0,0 +1,27 @@
using System.Linq;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Unity;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class SubNameInputMetadataExtractor : EntityMetadataExtractor<SubNameInput, SubNameInputMetadata>
{
public override SubNameInputMetadata Extract(SubNameInput subNameInput)
{
SubName subName = subNameInput.target;
return new(subNameInput.selectedColorIndex, GetName(subName), GetColors(subName));
}
public static string GetName(SubName subName)
{
return subName.AliveOrNull()?.hullName.AliveOrNull()?.text;
}
public static NitroxVector3[] GetColors(SubName subName)
{
return subName.AliveOrNull()?.GetColors().Select(color => color.ToDto()).ToArray();
}
}

View File

@@ -0,0 +1,17 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;
public class WaterParkCreatureMetadataExtractor : EntityMetadataExtractor<WaterParkCreature, WaterParkCreatureMetadata>
{
public override WaterParkCreatureMetadata Extract(WaterParkCreature entity)
{
// We don't need to save metadata for fishes with default values
if (entity.age == -1)
{
return null;
}
return new(entity.age, entity.matureTime, entity.timeNextBreed, entity.bornInside);
}
}

View File

@@ -0,0 +1,39 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata;
public class FlareMetadataProcessor : EntityMetadataProcessor<FlareMetadata>
{
public override void ProcessMetadata(GameObject gameObject, FlareMetadata metadata)
{
if (!gameObject.TryGetComponent(out Flare flare))
{
Log.Error($"[{nameof(FlareMetadataProcessor)}] Can't apply metadata to {gameObject} because it doesn't have a {nameof(Flare)} component");
return;
}
flare.energyLeft = metadata.EnergyLeft;
flare.hasBeenThrown = metadata.HasBeenThrown;
if (metadata.FlareActivateTime.HasValue)
{
flare.flareActivateTime = metadata.FlareActivateTime.Value;
flare.flareActiveState = true;
// From Flare.OnDrop
flare.useRigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
// Subtract the passed time to get to the current real amount of energy
flare.energyLeft -= DayNightCycle.main.timePassedAsFloat - metadata.FlareActivateTime.Value;
flare.GetComponent<WorldForces>().enabled = true;
// From Flare.Awake but without the part disabling the light
flare.capRenderer.enabled = true;
if (flare.fxControl && !flare.fxIsPlaying)
{
flare.fxControl.Play(1);
flare.fxIsPlaying = true;
flare.light.enabled = true;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
public abstract class EntityMetadataProcessor<TMetadata> : IEntityMetadataProcessor where TMetadata : EntityMetadata
{
public abstract void ProcessMetadata(GameObject gameObject, TMetadata metadata);
public void ProcessMetadata(GameObject gameObject, EntityMetadata metadata)
{
ProcessMetadata(gameObject, (TMetadata)metadata);
}
protected TService Resolve<TService>() where TService : class
{
return NitroxServiceLocator.Cache<TService>.Value;
}
}

View File

@@ -0,0 +1,9 @@
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
public interface IEntityMetadataProcessor
{
public abstract void ProcessMetadata(GameObject gameObject, EntityMetadata metadata);
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Unity;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
public abstract class VehicleMetadataProcessor<T> : EntityMetadataProcessor<T> where T : VehicleMetadata
{
private readonly LiveMixinManager liveMixinManager;
public VehicleMetadataProcessor(LiveMixinManager liveMixinManager)
{
this.liveMixinManager = liveMixinManager;
}
protected void SetHealth(GameObject gameObject, float health)
{
LiveMixin liveMixin = gameObject.RequireComponentInChildren<LiveMixin>(true);
liveMixinManager.SyncRemoteHealth(liveMixin, health);
}
protected void SetNameAndColors(SubName subName, string text, NitroxVector3[] nitroxColor) => SubNameInputMetadataProcessor.SetNameAndColors(subName, text, nitroxColor);
}

View File

@@ -0,0 +1,22 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class BatteryMetadataProcessor : EntityMetadataProcessor<BatteryMetadata>
{
public override void ProcessMetadata(GameObject gameObject, BatteryMetadata metadata)
{
Battery battery = gameObject.GetComponent<Battery>();
if (battery)
{
battery._charge = metadata.Charge;
}
else
{
Log.Error($"Could not find Battery on {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class ConstructorMetadataProcessor : EntityMetadataProcessor<ConstructorMetadata>
{
public override void ProcessMetadata(GameObject gameObject, ConstructorMetadata metadata)
{
if (gameObject.TryGetComponent(out Constructor constructor))
{
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
constructor.Deploy(metadata.Deployed);
}
}
else
{
Log.Error($"[{nameof(ConstructorMetadataProcessor)}] Could not find {nameof(Constructor)} on {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,45 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class CrafterMetadataProcessor : EntityMetadataProcessor<CrafterMetadata>
{
// small increase to prevent this player from swiping item from remote player
public const float ANTI_GRIEF_DURATION_BUFFER = 0.2f;
public override void ProcessMetadata(GameObject gameObject, CrafterMetadata metadata)
{
if (metadata.TechType == null)
{
EnsureCrafterReset(gameObject);
}
else
{
SpawnItemInCrafter(gameObject, metadata);
}
}
private void EnsureCrafterReset(GameObject gameObject)
{
CrafterLogic crafterLogic = gameObject.RequireComponentInChildren<CrafterLogic>(true);
crafterLogic.ResetCrafter();
}
private void SpawnItemInCrafter(GameObject gameObject, CrafterMetadata metadata)
{
GhostCrafter ghostCrafter = gameObject.RequireComponentInChildren<GhostCrafter>(true);
float elapsedFromStart = DayNightCycle.main.timePassedAsFloat - metadata.StartTime;
// If a craft started way in the past, set duration to 0.01 (the craft function will not work with 0)
// Keeping track of both the duration and start time allows us to solve use-cases such as reloading
// when an item is being crafted or not picked up yet.
float duration = Mathf.Max(metadata.Duration - elapsedFromStart + ANTI_GRIEF_DURATION_BUFFER, 0.01f);
ghostCrafter.logic.Craft(metadata.TechType.ToUnity(), duration);
}
}

View File

@@ -0,0 +1,31 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class CrashHomeMetadataProcessor : EntityMetadataProcessor<CrashHomeMetadata>
{
public override void ProcessMetadata(GameObject gameObject, CrashHomeMetadata metadata)
{
if (gameObject.TryGetComponent(out CrashHome crashHome))
{
crashHome.spawnTime = metadata.SpawnTime;
UpdateCrashHomeOpen(crashHome);
}
else
{
Log.Error($"[{nameof(CrashHomeMetadataProcessor)}] Could not find {nameof(CrashHome)} on {gameObject}");
}
}
public static void UpdateCrashHomeOpen(CrashHome crashHome)
{
// From CrashHome.Update
// We also add a distance detection to take into account if the crash is still in the home or not
bool isCrashResting = crashHome.crash && crashHome.crash.IsResting() && crashHome &&
Vector3.Distance(crashHome.transform.position, crashHome.crash.transform.position) < 1f;
crashHome.animator.SetBool(AnimatorHashID.attacking, !isCrashResting);
crashHome.prevClosed = isCrashResting;
}
}

View File

@@ -0,0 +1,58 @@
using NitroxClient.Communication;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class CyclopsLightingMetadataProcessor : EntityMetadataProcessor<CyclopsLightingMetadata>
{
private readonly IPacketSender packetSender;
public CyclopsLightingMetadataProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void ProcessMetadata(GameObject gameObject, CyclopsLightingMetadata metadata)
{
CyclopsLightingPanel lighting = gameObject.RequireComponentInChildren<CyclopsLightingPanel>(true);
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
SetInternalLighting(lighting, metadata.InternalLightsOn);
SetFloodLighting(lighting, metadata.FloodLightsOn);
}
}
private void SetInternalLighting(CyclopsLightingPanel lighting, bool isOn)
{
if (lighting.lightingOn == isOn)
{
return;
}
lighting.lightingOn = !lighting.lightingOn;
lighting.cyclopsRoot.ForceLightingState(lighting.lightingOn);
FMODAsset asset = !lighting.lightingOn ? lighting.vn_lightsOff : lighting.vn_lightsOn;
FMODUWE.PlayOneShot(asset, lighting.transform.position, 1f);
lighting.UpdateLightingButtons();
}
private void SetFloodLighting(CyclopsLightingPanel lighting, bool isOn)
{
if (lighting.floodlightsOn == isOn)
{
return;
}
lighting.floodlightsOn = !lighting.floodlightsOn;
lighting.SetExternalLighting(lighting.floodlightsOn);
FMODAsset asset = !lighting.floodlightsOn ? lighting.vn_floodlightsOff : lighting.vn_floodlightsOn;
FMODUWE.PlayOneShot(asset, lighting.transform.position, 1f);
lighting.UpdateLightingButtons();
}
}

View File

@@ -0,0 +1,174 @@
using NitroxClient.Communication;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class CyclopsMetadataProcessor : EntityMetadataProcessor<CyclopsMetadata>
{
private readonly IPacketSender packetSender;
private readonly LiveMixinManager liveMixinManager;
public CyclopsMetadataProcessor(IPacketSender packetSender, LiveMixinManager liveMixinManager)
{
this.packetSender = packetSender;
this.liveMixinManager = liveMixinManager;
}
public override void ProcessMetadata(GameObject cyclops, CyclopsMetadata metadata)
{
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
SetEngineMode(cyclops, (CyclopsMotorMode.CyclopsMotorModes)metadata.EngineMode);
ChangeSilentRunning(cyclops, metadata.SilentRunningOn);
ChangeShieldMode(cyclops, metadata.ShieldOn);
ChangeSonarMode(cyclops, metadata.SonarOn);
SetEngineState(cyclops, metadata.EngineOn);
SetHealth(cyclops, metadata.Health);
SetDestroyed(cyclops, metadata.IsDestroyed);
}
}
private void SetEngineState(GameObject cyclops, bool isOn)
{
CyclopsEngineChangeState engineState = cyclops.RequireComponentInChildren<CyclopsEngineChangeState>(true);
if (isOn == engineState.motorMode.engineOn)
{
// engine state is the same - nothing to do.
return;
}
// During initial sync or when the cyclops HUD isn't shown (from outside of the cyclops)
if (Player.main.currentSub != engineState.subRoot)
{
engineState.startEngine = isOn;
engineState.subRoot.BroadcastMessage(nameof(CyclopsMotorMode.InvokeChangeEngineState), isOn, SendMessageOptions.RequireReceiver);
engineState.invalidButton = true;
engineState.Invoke(nameof(CyclopsEngineChangeState.ResetInvalidButton), 2.5f);
}
// When inside of the cyclops, we play the cinematics
else
{
// To invoke the whole OnClick method we need to set the right parameters first
engineState.invalidButton = false;
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
engineState.OnClick();
}
}
}
private void SetEngineMode(GameObject cyclops, CyclopsMotorMode.CyclopsMotorModes mode)
{
foreach (CyclopsMotorModeButton button in cyclops.GetComponentsInChildren<CyclopsMotorModeButton>(true))
{
// At initial sync, this kind of processor is executed before the Start()
if (!button.subRoot)
{
button.Start();
}
button.SetCyclopsMotorMode(mode);
}
}
private void ChangeSilentRunning(GameObject cyclops, bool isOn)
{
CyclopsSilentRunningAbilityButton ability = cyclops.RequireComponentInChildren<CyclopsSilentRunningAbilityButton>(true);
if (isOn == ability.active)
{
return;
}
Log.Debug($"Set silent running to {isOn} for cyclops");
ability.active = isOn;
if (isOn)
{
ability.image.sprite = ability.activeSprite;
ability.subRoot.BroadcastMessage("RigForSilentRunning");
ability.InvokeRepeating(nameof(CyclopsSilentRunningAbilityButton.SilentRunningIteration), 0f, ability.silentRunningIteration);
}
else
{
ability.image.sprite = ability.inactiveSprite;
ability.subRoot.BroadcastMessage("SecureFromSilentRunning");
ability.CancelInvoke(nameof(CyclopsSilentRunningAbilityButton.SilentRunningIteration));
}
}
private void ChangeShieldMode(GameObject cyclops, bool isOn)
{
CyclopsShieldButton shield = cyclops.GetComponentInChildren<CyclopsShieldButton>(true);
if (!shield)
{
// may not have a shield installed.
return;
}
bool isShieldOn = shield.activeSprite == shield.image.sprite;
if (isShieldOn == isOn)
{
return;
}
if (isOn)
{
shield.StartShield();
}
else
{
shield.StopShield();
}
}
private void ChangeSonarMode(GameObject cyclops, bool isOn)
{
CyclopsSonarButton sonarButton = cyclops.GetComponentInChildren<CyclopsSonarButton>(true);
if (sonarButton && sonarButton.sonarActive != isOn)
{
if (isOn)
{
sonarButton.TurnOnSonar();
}
else
{
sonarButton.TurnOffSonar();
}
}
}
private void SetHealth(GameObject gameObject, float health)
{
LiveMixin liveMixin = gameObject.RequireComponentInChildren<LiveMixin>(true);
liveMixinManager.SyncRemoteHealth(liveMixin, health);
}
private void SetDestroyed(GameObject gameObject, bool isDestroyed)
{
CyclopsDestructionEvent destructionEvent = gameObject.RequireComponentInChildren<CyclopsDestructionEvent>(true);
// Don't play VFX and SFX if the Cyclops is already destroyed or was spawned in as destroyed
if (destructionEvent.subRoot.subDestroyed == isDestroyed) return;
if (isDestroyed)
{
// Use packet suppressor as sentinel so the patch callback knows not to spawn loot
using (PacketSuppressor<EntitySpawnedByClient>.Suppress())
{
destructionEvent.DestroyCyclops();
}
}
else
{
destructionEvent.RestoreCyclops();
}
}
}

View File

@@ -0,0 +1,21 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class EatableMetadataProcessor : EntityMetadataProcessor<EatableMetadata>
{
public override void ProcessMetadata(GameObject gameObject, EatableMetadata metadata)
{
if (gameObject.TryGetComponent(out Eatable eatable))
{
eatable.SetDecomposes(true);
eatable.timeDecayStart = metadata.TimeDecayStart;
}
if (gameObject.TryGetComponent(out LiveMixin liveMixin))
{
Resolve<LiveMixinManager>().SyncRemoteHealth(liveMixin, 0);
}
}
}

View File

@@ -0,0 +1,39 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class EggMetadataProcessor : EntityMetadataProcessor<EggMetadata>
{
public override void ProcessMetadata(GameObject gameObject, EggMetadata metadata)
{
if (!gameObject.TryGetComponent(out CreatureEgg creatureEgg))
{
Log.Error($"[{nameof(EggMetadataProcessor)}] Could not find {nameof(CreatureEgg)} on {gameObject.name}");
return;
}
if (metadata.TimeStartHatching == -1f)
{
// If the egg is not in a water park we only need its progress value
creatureEgg.progress = metadata.Progress;
return;
}
// If the egg is in a water park we only need its time start hatching value
// the current progress will be automatically computed by UpdateProgress()
creatureEgg.timeStartHatching = metadata.TimeStartHatching;
// While being fully loaded, the base is inactive and coroutines shouldn't be started (they'll throw an exception)
// To avoid, that we postpone their execution to 1 more second which is enough because time is frozen during initial sync
if (Multiplayer.Main && !Multiplayer.Main.InitialSyncCompleted &&
creatureEgg.timeStartHatching + creatureEgg.GetHatchDuration() < DayNightCycle.main.timePassedAsFloat)
{
creatureEgg.timeStartHatching = DayNightCycle.main.timePassedAsFloat + 1 - creatureEgg.GetHatchDuration();
}
creatureEgg.UpdateProgress();
}
}

View File

@@ -0,0 +1,21 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class EntitySignMetadataProcessor : EntityMetadataProcessor<EntitySignMetadata>
{
public override void ProcessMetadata(GameObject gameObject, EntitySignMetadata metadata)
{
uGUI_SignInput sign = gameObject.GetComponentInChildren<uGUI_SignInput>(true);
if (sign)
{
sign.text = metadata.Text;
sign.colorIndex = metadata.ColorIndex;
sign.elementsState = metadata.Elements;
sign.scaleIndex = metadata.ScaleIndex;
sign.SetBackground(metadata.Background);
}
}
}

View File

@@ -0,0 +1,75 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class EscapePodMetadataProcessor : EntityMetadataProcessor<EscapePodMetadata>
{
// For metadata changes outside initial sync we only care about broken -> repaired
public override void ProcessMetadata(GameObject gameObject, EscapePodMetadata metadata)
{
if (!gameObject.TryGetComponent(out EscapePod pod))
{
Log.Error($"[{nameof(EscapePodMetadataProcessor)}] Could not get the EscapePod component from the provided gameobject.");
return;
}
if (!pod.liveMixin.IsFullHealth() && metadata.PodRepaired)
{
pod.liveMixin.health = pod.liveMixin.maxHealth;
pod.healthScalar = 1;
pod.damageEffectsShowing = true;
pod.UpdateDamagedEffects();
pod.OnRepair();
}
if (!pod.radioSpawner.spawnedObj.TryGetComponent(out Radio radio))
{
Log.Error($"[{nameof(EscapePodMetadataProcessor)}] Could not get Radio from EscapePod.");
return;
}
if (!radio.liveMixin.IsFullHealth() && metadata.RadioRepaired)
{
radio.liveMixin.AddHealth(radio.liveMixin.maxHealth);
}
}
/// <summary>
/// Applies repaired state without animations and minimal audio playback
/// </summary>
public static void ProcessInitialSyncMetadata(EscapePod pod, Radio radio, EscapePodMetadata metadata)
{
if (metadata.PodRepaired)
{
pod.liveMixin.health = pod.liveMixin.maxHealth;
pod.healthScalar = 1;
pod.damageEffectsShowing = true; // Needs to be set to true for UpdateDamagedEffects() to function
pod.UpdateDamagedEffects();
pod.vfxSpawner.SpawnManual(); // Spawn vfx to instantly disable it so no smoke is fading after player has joined
pod.vfxSpawner.spawnedObj.SetActive(false);
pod.lightingController.SnapToState(0);
}
else
{
IntroLifepodDirector introLifepodDirector = pod.GetComponent<IntroLifepodDirector>();
introLifepodDirector.OnProtoDeserializeObjectTree(null);
introLifepodDirector.ToggleActiveObjects(false);
pod.lightingController.SnapToState(2);
}
if (metadata.RadioRepaired)
{
radio.liveMixin.health = radio.liveMixin.maxHealth;
if (radio.liveMixin.loopingDamageEffectObj)
{
Object.Destroy(radio.liveMixin.loopingDamageEffectObj);
}
}
else
{
pod.DamageRadio();
}
}
}

View File

@@ -0,0 +1,32 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class ExosuitMetadataProcessor : VehicleMetadataProcessor<ExosuitMetadata>
{
public ExosuitMetadataProcessor(LiveMixinManager liveMixinManager) : base(liveMixinManager) { }
public override void ProcessMetadata(GameObject gameObject, ExosuitMetadata metadata)
{
if (!gameObject.TryGetComponent(out Exosuit exosuit))
{
Log.ErrorOnce($"[{nameof(ExosuitMetadataProcessor)}] Could not find {nameof(Exosuit)} on {gameObject}");
return;
}
if (!gameObject.TryGetComponent(out SubName subName))
{
Log.ErrorOnce($"[{nameof(ExosuitMetadataProcessor)}] Could not find {nameof(SubName)} on {gameObject}");
return;
}
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
SetHealth(gameObject, metadata.Health);
SetNameAndColors(subName, metadata.Name, metadata.Colors);
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class FireExtinguisherHolderMetadataProcessor : EntityMetadataProcessor<FireExtinguisherHolderMetadata>
{
public override void ProcessMetadata(GameObject gameObject, FireExtinguisherHolderMetadata metadata)
{
FireExtinguisherHolder holder = gameObject.GetComponent<FireExtinguisherHolder>();
if (holder)
{
holder.fuel = metadata.Fuel;
holder.hasTank = metadata.HasExtinguisher;
holder.tankObject.SetActive(metadata.HasExtinguisher);
}
else
{
Log.Error($"Could not find FireExtinguisherHolder on {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,31 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class FlashlightMetadataProcessor : EntityMetadataProcessor<FlashlightMetadata>
{
public override void ProcessMetadata(GameObject gameObject, FlashlightMetadata metadata)
{
FlashLight flashLight = gameObject.GetComponent<FlashLight>();
if (flashLight)
{
ToggleLights lights = flashLight.gameObject.GetComponent<ToggleLights>();
if (lights)
{
lights.lightsActive = metadata.On;
}
else
{
Log.Error($"Could not find ToggleLights on {flashLight.name}");
}
}
else
{
Log.Error($"Could not find FlashLight on {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,63 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class FruitPlantMetadataProcessor : EntityMetadataProcessor<FruitPlantMetadata>
{
public override void ProcessMetadata(GameObject gameObject, FruitPlantMetadata metadata)
{
// Two cases:
// 1. The entity with an id directly has a FruitPlant onto it
if (gameObject.TryGetComponent(out FruitPlant fruitPlant))
{
ProcessMetadata(fruitPlant, metadata);
return;
}
// 2. The entity with an id has a Plantable (located in the plot's storage),
// we want to access the FruitPlant component which is on the spawned plant object
if (!gameObject.TryGetComponent(out Plantable plantable))
{
Log.Error($"[{nameof(FruitPlantMetadataProcessor)}] Could not find {nameof(FruitPlant)} related to {gameObject.name}");
return;
}
if (!plantable.linkedGrownPlant)
{
// This is an error which will happen quite often since this metadata
// is applied from PlantableMetadataProcessor even when linkedGrownPlant isn't available yet
return;
}
if (!plantable.linkedGrownPlant.TryGetComponent(out fruitPlant))
{
Log.Error($"[{nameof(FruitPlantMetadataProcessor)}] Could not find {nameof(FruitPlant)} on {gameObject.name}'s linkedGrownPlant {plantable.linkedGrownPlant.name}");
return;
}
ProcessMetadata(fruitPlant, metadata);
}
private static void ProcessMetadata(FruitPlant fruitPlant, FruitPlantMetadata metadata)
{
// Inspired by FruitPlant.Initialize
fruitPlant.inactiveFruits.Clear();
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
for (int i = 0; i < fruitPlant.fruits.Length; i++)
{
fruitPlant.fruits[i].SetPickedState(metadata.PickedStates[i]);
if (metadata.PickedStates[i])
{
fruitPlant.inactiveFruits.Add(fruitPlant.fruits[i]);
}
}
}
fruitPlant.timeNextFruit = metadata.TimeNextFruit;
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class IncubatorMetadataProcessor : EntityMetadataProcessor<IncubatorMetadata>
{
public override void ProcessMetadata(GameObject gameObject, IncubatorMetadata metadata)
{
if (metadata.Powered)
{
IncubatorActivationTerminal terminal = gameObject.GetComponentInChildren<IncubatorActivationTerminal>();
terminal.incubator.SetPowered(true);
terminal.onUseGoal?.Trigger();
terminal.CloseDeck();
}
if (metadata.Hatched)
{
Incubator incubator = gameObject.GetComponentInChildren<Incubator>();
incubator.hatched = true;
incubator.OnHatched();
}
}
}

View File

@@ -0,0 +1,30 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class KeypadMetadataProcessor : EntityMetadataProcessor<KeypadMetadata>
{
public override void ProcessMetadata(GameObject gameObject, KeypadMetadata metadata)
{
Log.Debug($"Received keypad metadata change for {gameObject.name} with data of {metadata}");
KeypadDoorConsole keypad = gameObject.GetComponent<KeypadDoorConsole>();
keypad.unlocked = metadata.Unlocked;
if (metadata.Unlocked)
{
if (keypad.root)
{
keypad.root.BroadcastMessage("UnlockDoor");
}
else
{
keypad.BroadcastMessage("UnlockDoor");
}
keypad.UnlockDoor();
}
}
}

View File

@@ -0,0 +1,45 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PlantableMetadataProcessor(FruitPlantMetadataProcessor fruitPlantMetadataProcessor) : EntityMetadataProcessor<PlantableMetadata>
{
private readonly FruitPlantMetadataProcessor fruitPlantMetadataProcessor = fruitPlantMetadataProcessor;
public override void ProcessMetadata(GameObject gameObject, PlantableMetadata metadata)
{
if (!gameObject.TryGetComponent(out Plantable plantable))
{
Log.Error($"[{nameof(PlantableMetadataProcessor)}] Could not find {nameof(Plantable)} on {gameObject.name}");
return;
}
// For Plantables which were replaced by the GrowingPlant object
if (plantable.growingPlant)
{
plantable.growingPlant.timeStartGrowth = metadata.TimeStartGrowth;
}
// For regular Plantables
else if (plantable.model.TryGetComponent(out GrowingPlant growingPlant))
{
// Calculation from GrowingPlant.GetProgress
if (metadata.TimeStartGrowth == -1f)
{
plantable.plantAge = 0;
}
else
{
// This is the reversed calculation because we're looking for "progress" while we already know timeStartGrowth
plantable.plantAge = Mathf.Clamp((DayNightCycle.main.timePassedAsFloat - metadata.TimeStartGrowth) / growingPlant.GetGrowthDuration(), 0f, growingPlant.maxProgress);
}
}
// TODO: Refer to the TODO in PlantableMetadata
if (metadata.FruitPlantMetadata != null)
{
fruitPlantMetadataProcessor.ProcessMetadata(gameObject, metadata.FruitPlantMetadata);
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using static NitroxModel.DataStructures.GameLogic.Entities.Metadata.PlayerMetadata;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PlayerMetadataProcessor : EntityMetadataProcessor<PlayerMetadata>
{
private NitroxId localPlayerId = null;
public override void ProcessMetadata(GameObject gameObject, PlayerMetadata metadata)
{
if (!gameObject.TryGetIdOrWarn(out NitroxId id))
{
return;
}
// The local player id should be static, therefor we can cache the id for performance
if (localPlayerId == null && !Player.main.TryGetIdOrWarn(out localPlayerId))
{
return;
}
if (id == localPlayerId)
{
UpdateForLocalPlayer(metadata);
}
else
{
UpdateForRemotePlayer(gameObject, metadata);
}
}
private void UpdateForLocalPlayer(PlayerMetadata metadata)
{
ItemsContainer currentItems = Inventory.Get().container;
Equipment equipment = Inventory.main.equipment;
foreach (EquippedItem equippedItem in metadata.EquippedItems)
{
InventoryItem inventoryItem = currentItems.FirstOrDefault(item => item.item.TryGetNitroxId(out NitroxId id) && equippedItem.Id == id);
// It is OK if we don't find the item, this could be a rebroadcast and we've already equipped the item.
if (inventoryItem != null)
{
Pickupable pickupable = inventoryItem.item;
currentItems.RemoveItem(pickupable, true);
inventoryItem.container = equipment;
pickupable.Reparent(equipment.tr);
equipment.equipment[equippedItem.Slot] = inventoryItem;
equipment.UpdateCount(pickupable.GetTechType(), true);
Equipment.SendEquipmentEvent(pickupable, 0, equipment.owner, equippedItem.Slot);
equipment.NotifyEquip(equippedItem.Slot, inventoryItem);
currentItems.RemoveItem(inventoryItem.item);
}
}
}
private void UpdateForRemotePlayer(GameObject gameObject, PlayerMetadata metadata)
{
Log.Info("Calling UpdateForRemotePlayer");
RemotePlayerIdentifier remotePlayerId = gameObject.RequireComponent<RemotePlayerIdentifier>();
List<TechType> equippedTechTypes = metadata.EquippedItems.Select(x => x.TechType.ToUnity()).ToList();
remotePlayerId.RemotePlayer.UpdateEquipmentVisibility(equippedTechTypes);
}
}

View File

@@ -0,0 +1,25 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PrecursorDoorwayMetadataProcessor : EntityMetadataProcessor<PrecursorDoorwayMetadata>
{
public override void ProcessMetadata(GameObject gameObject, PrecursorDoorwayMetadata metadata)
{
Log.Info($"Received precursor door metadata change for {gameObject.name} with data of {metadata}");
PrecursorDoorway precursorDoorway = gameObject.GetComponent<PrecursorDoorway>();
precursorDoorway.isOpen = metadata.IsOpen;
if (metadata.IsOpen)
{
precursorDoorway.BroadcastMessage("DisableField");
}
else
{
precursorDoorway.BroadcastMessage("EnableField");
}
}
}

View File

@@ -0,0 +1,19 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PrecursorKeyTerminalMetadataProcessor : EntityMetadataProcessor<PrecursorKeyTerminalMetadata>
{
public override void ProcessMetadata(GameObject gameObject, PrecursorKeyTerminalMetadata metadata)
{
Log.Debug($"Received precursor key terminal metadata change for {gameObject.name} with data of {metadata}");
PrecursorKeyTerminal precursorKeyTerminal = gameObject.GetComponent<PrecursorKeyTerminal>();
if (precursorKeyTerminal)
{
precursorKeyTerminal.slotted = metadata.Slotted;
}
}
}

View File

@@ -0,0 +1,19 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PrecursorTeleporterActivationTerminalMetadataProcessor : EntityMetadataProcessor<PrecursorTeleporterActivationTerminalMetadata>
{
public override void ProcessMetadata(GameObject gameObject, PrecursorTeleporterActivationTerminalMetadata metadata)
{
Log.Debug($"Received precursor teleporter activation terminal metadata change for {gameObject.name} with data of {metadata}");
PrecursorTeleporterActivationTerminal precursorTeleporterActivationTerminal = gameObject.GetComponent<PrecursorTeleporterActivationTerminal>();
if (precursorTeleporterActivationTerminal)
{
precursorTeleporterActivationTerminal.unlocked = metadata.Unlocked;
}
}
}

View File

@@ -0,0 +1,19 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class PrecursorTeleporterMetadataProcessor : EntityMetadataProcessor<PrecursorTeleporterMetadata>
{
public override void ProcessMetadata(GameObject gameObject, PrecursorTeleporterMetadata metadata)
{
Log.Debug($"Received precursor teleporter metadata change for {gameObject.name} with data of {metadata}");
PrecursorTeleporter precursorTeleporter = gameObject.GetComponent<PrecursorTeleporter>();
if (precursorTeleporter)
{
precursorTeleporter.ToggleDoor(metadata.IsOpen);
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class RadiationMetadataProcessor : EntityMetadataProcessor<RadiationMetadata>
{
public override void ProcessMetadata(GameObject gameObject, RadiationMetadata metadata)
{
if (!gameObject.TryGetComponent(out LiveMixin liveMixin))
{
Log.Error($"[{nameof(RadiationMetadataProcessor)}] Couldn't find LiveMixin on {gameObject}");
return;
}
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
Resolve<LiveMixinManager>().SyncRemoteHealth(liveMixin, metadata.Health);
}
}
}

View File

@@ -0,0 +1,157 @@
using System.Collections.Generic;
using System.Linq;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
using static Rocket;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class RocketMetadataProcessor : EntityMetadataProcessor<RocketMetadata>
{
// For newly connected players, we will only build the previous stage with construction bots for a certain time period.
private const float MAX_ALLOWABLE_TIME_FOR_CONSTRUCTOR_BOTS = 10;
/** Rocket states :
* 0 : Launch Platform
* 1 : Gantry
* 2 : Boosters
* 3 : Fuel Reserve
* 4 : Cockpit
* 5 : Final rocket
**/
public override void ProcessMetadata(GameObject gameObject, RocketMetadata metadata)
{
Rocket rocket = gameObject.GetComponent<Rocket>();
if (!rocket)
{
Log.Error($"Could not find Rocket on {gameObject.name}");
return;
}
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
UpdateElevator(rocket, metadata);
UpdateStage(rocket, metadata);
UpdatePreflightChecks(rocket, metadata);
}
}
private void UpdateElevator(Rocket rocket, RocketMetadata metadata)
{
// elevators will only be present on this model after the gantry (p1) is built
if (rocket.currentRocketStage > 1)
{
rocket.elevatorPosition = metadata.ElevatorPosition;
rocket.elevatorState = (RocketElevatorStates)metadata.ElevatorState;
rocket.SetElevatorPosition();
}
}
private void UpdateStage(Rocket rocket, RocketMetadata metadata)
{
if (rocket.currentRocketStage == metadata.CurrentStage)
{
return;
}
bool allowConstructorBots = DayNightCycle.main.timePassedAsFloat - metadata.LastStageTransitionTime < MAX_ALLOWABLE_TIME_FOR_CONSTRUCTOR_BOTS;
RocketConstructor rocketConstructor = rocket.RequireComponentInChildren<RocketConstructor>(true);
for (int stage = rocket.currentRocketStage; stage < metadata.CurrentStage; stage++)
{
bool lastStageToBuild = stage == metadata.CurrentStage - 1;
GameObject build = rocket.StartRocketConstruction();
// We only want to use construction bots for the last constructed stage (just in case the client is out dated by multiple stages).
// For all others, just force the completion of that stage.
if (lastStageToBuild && allowConstructorBots)
{
rocketConstructor.SendBuildBots(build);
}
else
{
VFXConstructing vfxConstructing = build.GetComponent<VFXConstructing>();
vfxConstructing.EndGracefully();
}
}
}
private void UpdatePreflightChecks(Rocket rocket, RocketMetadata metadata)
{
if (rocket.currentRocketStage < 4)
{
return;
}
IEnumerable<PreflightCheck> completedChecks = metadata.PreflightChecks.Select(i => (PreflightCheck)i);
RocketPreflightCheckManager rocketPreflightCheckManager = rocket.RequireComponent<RocketPreflightCheckManager>();
foreach (PreflightCheck completedCheck in completedChecks)
{
if (!rocketPreflightCheckManager.preflightChecks.Contains(completedCheck))
{
CompletePreflightCheck(rocket, completedCheck);
rocketPreflightCheckManager.CompletePreflightCheck(completedCheck);
}
}
}
private void CompletePreflightCheck(Rocket rocket, PreflightCheck preflightCheck)
{
bool isCockpitCheck = preflightCheck == PreflightCheck.LifeSupport ||
preflightCheck == PreflightCheck.PrimaryComputer;
if (isCockpitCheck)
{
CompleteCockpitPreflightCheck(rocket, preflightCheck);
}
else
{
CompleteBasicPreflightCheck(rocket, preflightCheck);
}
}
private void CompleteCockpitPreflightCheck(Rocket rocket, PreflightCheck preflightCheck)
{
CockpitSwitch[] cockpitSwitches = rocket.GetComponentsInChildren<CockpitSwitch>(true);
foreach (CockpitSwitch cockpitSwitch in cockpitSwitches)
{
if (!cockpitSwitch.completed && cockpitSwitch.preflightCheck == preflightCheck)
{
cockpitSwitch.animator.SetBool("Completed", true);
cockpitSwitch.completed = true;
if (cockpitSwitch.collision)
{
cockpitSwitch.collision.SetActive(false);
}
}
}
}
private void CompleteBasicPreflightCheck(Rocket rocket, PreflightCheck preflightCheck)
{
ThrowSwitch[] throwSwitches = rocket.GetComponentsInChildren<ThrowSwitch>(true);
foreach (ThrowSwitch throwSwitch in throwSwitches)
{
if (!throwSwitch.completed && throwSwitch.preflightCheck == preflightCheck)
{
throwSwitch.animator.AliveOrNull()?.SetTrigger("Throw");
throwSwitch.completed = true;
throwSwitch.cinematicTrigger.showIconOnHandHover = false;
throwSwitch.triggerCollider.enabled = false;
throwSwitch.lamp.GetComponent<SkinnedMeshRenderer>().material = throwSwitch.completeMat;
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class SeaTreaderMetadataProcessor : EntityMetadataProcessor<SeaTreaderMetadata>
{
public override void ProcessMetadata(GameObject gameObject, SeaTreaderMetadata metadata)
{
if (gameObject.TryGetComponent(out SeaTreader seaTreader))
{
if (!seaTreader.isInitialized)
{
seaTreader.InitializeOnce();
}
seaTreader.reverseDirection = metadata.ReverseDirection;
float grazingTimeLeft = Math.Max(0, metadata.GrazingEndTime - DayNightCycle.main.timePassedAsFloat);
seaTreader.grazing = grazingTimeLeft > 0;
seaTreader.grazingTimeLeft = grazingTimeLeft;
seaTreader.leashPosition = metadata.LeashPosition.ToUnity();
seaTreader.leashPosition.y = gameObject.transform.position.y;
seaTreader.isInitialized = true;
seaTreader.InitializeAgain();
}
else
{
Log.Error($"Could not find {nameof(SeaTreader)} on {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class SealedDoorMetadataProcessor : EntityMetadataProcessor<SealedDoorMetadata>
{
public override void ProcessMetadata(GameObject gameObject, SealedDoorMetadata metadata)
{
Log.Info($"Received door metadata change for {gameObject.name} with data of {metadata}");
Sealed door = gameObject.GetComponent<Sealed>();
door._sealed = metadata.Sealed;
door.openedAmount = metadata.OpenedAmount;
LaserCutObject laseredObject = gameObject.GetComponent<LaserCutObject>();
if (laseredObject && door._sealed)
{
laseredObject.lastCutValue = door.openedAmount;
laseredObject.ActivateFX();
}
}
}

View File

@@ -0,0 +1,44 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.FMOD;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class SeamothMetadataProcessor : VehicleMetadataProcessor<SeamothMetadata>
{
public SeamothMetadataProcessor(LiveMixinManager liveMixinManager) : base(liveMixinManager)
{ }
public override void ProcessMetadata(GameObject gameObject, SeamothMetadata metadata)
{
if (!gameObject.TryGetComponent(out SeaMoth seamoth))
{
Log.ErrorOnce($"[{nameof(SeamothMetadataProcessor)}] Could not find {nameof(SeaMoth)} on {gameObject}");
return;
}
if (!gameObject.TryGetComponent(out SubName subName))
{
Log.ErrorOnce($"[{nameof(SeamothMetadataProcessor)}] Could not find {nameof(SubName)} on {gameObject}");
return;
}
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
SetLights(seamoth, metadata.LightsOn);
SetHealth(seamoth.gameObject, metadata.Health);
SetNameAndColors(subName, metadata.Name, metadata.Colors);
}
}
private void SetLights(SeaMoth seamoth, bool lightsOn)
{
using (FMODSystem.SuppressSendingSounds())
{
seamoth.toggleLights.SetLightsActive(lightsOn);
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class StarshipDoorMetadataProcessor : EntityMetadataProcessor<StarshipDoorMetadata>
{
public override void ProcessMetadata(GameObject gameObject, StarshipDoorMetadata metadata)
{
StarshipDoor starshipDoor = gameObject.GetComponent<StarshipDoor>();
starshipDoor.doorOpen = metadata.DoorOpen;
starshipDoor.doorLocked = metadata.DoorLocked;
if (metadata.DoorLocked)
{
starshipDoor.LockDoor();
}
else
{
starshipDoor.UnlockDoor();
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class StayAtLeashPositionMetadataProcessor : EntityMetadataProcessor<StayAtLeashPositionMetadata>
{
public override void ProcessMetadata(GameObject gameObject, StayAtLeashPositionMetadata metadata)
{
if (!gameObject.TryGetComponent(out Creature creature))
{
Log.Error($"Could not find {nameof(Creature)} on {gameObject.name}");
return;
}
if (!creature.isInitialized)
{
// TODO: When #2137 is merged, only a MetadataHolder to the creature and postfix patch creature.Start to consume it
creature.InitializeOnce();
creature.isInitialized = true;
}
creature.leashPosition = metadata.LeashPosition.ToUnity();
}
}

View File

@@ -0,0 +1,53 @@
using System.Linq;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Unity;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class SubNameInputMetadataProcessor : EntityMetadataProcessor<SubNameInputMetadata>
{
public override void ProcessMetadata(GameObject gameObject, SubNameInputMetadata metadata)
{
if (!gameObject.TryGetComponent(out SubNameInput subNameInput))
{
Log.ErrorOnce($"[{nameof(SubNameInputMetadataProcessor)}] Could not find {nameof(SubNameInput)} on {gameObject}");
return;
}
SubName subName = subNameInput.target;
if (!subName && !subNameInput.TryGetComponent(out subName))
{
Log.ErrorOnce($"[{nameof(SubNameInputMetadataProcessor)}] {gameObject}'s {nameof(subNameInput)} doesn't have a target.");
return;
}
// Ensure the SubNameInput's object is active so that it receives events from its SubName
gameObject.SetActive(true);
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
// Name and color applying must be applied before SelectedColorIndex
SetNameAndColors(subName, metadata.Name, metadata.Colors);
subNameInput.SetSelected(metadata.SelectedColorIndex);
}
}
public static void SetNameAndColors(SubName subName, string text, NitroxVector3[] nitroxColors)
{
if (!string.IsNullOrEmpty(text))
{
subName.DeserializeName(text);
}
if (nitroxColors != null)
{
Vector3[] colors = nitroxColors.Select(c => c.ToUnity()).ToArray();
subName.DeserializeColors(colors);
}
}
}

View File

@@ -0,0 +1,55 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class WaterParkCreatureMetadataProcessor : EntityMetadataProcessor<WaterParkCreatureMetadata>
{
public override void ProcessMetadata(GameObject gameObject, WaterParkCreatureMetadata metadata)
{
if (!gameObject.TryGetComponent(out WaterParkCreature waterParkCreature))
{
Log.Error($"[{nameof(WaterParkCreatureMetadataProcessor)}] Could not find {nameof(WaterParkCreature)} on {gameObject.name}");
return;
}
if (waterParkCreature.currentWaterPark)
{
// MatureTime is important for fishes that are already in a WaterPark, to calculate the right age
waterParkCreature.matureTime = metadata.MatureTime;
double startTime = metadata.MatureTime - waterParkCreature.data.growingPeriod;
waterParkCreature.age = Mathf.InverseLerp((float)startTime, (float)metadata.MatureTime, DayNightCycle.main.timePassedAsFloat);
waterParkCreature.timeNextBreed = metadata.TimeNextBreed;
}
else
{
// Age is the only important constant for fishes that are in an item state
waterParkCreature.matureTime = -1;
waterParkCreature.age = metadata.Age;
waterParkCreature.timeNextBreed = -1;
}
// Scaling according to WaterParkCreature.ManagedUpdate
waterParkCreature.transform.localScale = Mathf.Lerp(waterParkCreature.data.initialSize, waterParkCreature.data.maxSize, waterParkCreature.age) * Vector3.one;
waterParkCreature.isMature = waterParkCreature.age == 1f;
waterParkCreature.bornInside = metadata.BornInside;
// This field is not serialized but is always the exact same so it's supposedly recomputed but it would break with our system
// (calculation from WaterParkCreature.ManagedUpdate)
waterParkCreature.breedInterval = waterParkCreature.data.growingPeriod * 0.5f;
// While being fully loaded, the base is inactive and coroutines shouldn't be started (they'll throw an exception)
// To avoid, that we postpone their execution to 1 more second which is enough because time is frozen during initial sync
// This is the mating condition from WaterParkCreature.ManagedUpdate to postpone mating
if (Multiplayer.Main && !Multiplayer.Main.InitialSyncCompleted && waterParkCreature.currentWaterPark && waterParkCreature.GetCanBreed() &&
waterParkCreature.timeNextBreed != -1 && DayNightCycle.main.timePassedAsFloat > waterParkCreature.timeNextBreed)
{
waterParkCreature.timeNextBreed = DayNightCycle.main.timePassedAsFloat + 1;
}
waterParkCreature.OnProtoDeserialize(null);
}
}

View File

@@ -0,0 +1,14 @@
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;
public class WeldableWallPanelGenericMetadataProcessor : EntityMetadataProcessor<WeldableWallPanelGenericMetadata>
{
public override void ProcessMetadata(GameObject gameObject, WeldableWallPanelGenericMetadata metadata)
{
WeldableWallPanelGeneric weldableWallPanelGeneric = gameObject.GetComponent<WeldableWallPanelGeneric>();
weldableWallPanelGeneric.liveMixin.health = metadata.LiveMixInHealth;
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning;
public class PathBasedChildEntitySpawner : SyncEntitySpawner<PathBasedChildEntity>
{
protected override IEnumerator SpawnAsync(PathBasedChildEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}
protected override bool SpawnSync(PathBasedChildEntity entity, TaskResult<Optional<GameObject>> result)
{
Optional<GameObject> owner = NitroxEntity.GetObjectFrom(entity.ParentId);
if (!owner.HasValue)
{
Log.Error($"Unable to find parent entity: {entity}");
result.Set(Optional.Empty);
return true;
}
Transform child = owner.Value.transform.Find(entity.Path);
if (!child)
{
Log.Error($"Could not locate child at path {entity.Path} in {owner.Value.name}");
result.Set(Optional.Empty);
return true;
}
GameObject gameObject = child.gameObject;
NitroxEntity.SetNewId(gameObject, entity.Id);
result.Set(gameObject);
return true;
}
protected override bool SpawnsOwnChildren(PathBasedChildEntity entity) => false;
}

View File

@@ -0,0 +1,41 @@
using System.Collections;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning;
public class PrefabChildEntitySpawner : SyncEntitySpawner<PrefabChildEntity>
{
// When we encounter a PrefabChildEntity, we need to assign the id to a prefab with the same class id and index.
protected override IEnumerator SpawnAsync(PrefabChildEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}
protected override bool SpawnSync(PrefabChildEntity entity, TaskResult<Optional<GameObject>> result)
{
GameObject parent = NitroxEntity.RequireObjectFrom(entity.ParentId);
PrefabIdentifier prefab = parent.GetAllComponentsInChildren<PrefabIdentifier>()
.Where(prefab => prefab.classId == entity.ClassId)
.ElementAt(entity.ComponentIndex);
if (prefab)
{
NitroxEntity.SetNewId(prefab.gameObject, entity.Id);
result.Set(Optional.OfNullable(prefab.gameObject));
}
else
{
Log.Error($"Unable to find prefab for: {entity}");
result.Set(Optional.Empty);
}
return true;
}
protected override bool SpawnsOwnChildren(PrefabChildEntity entity) => false;
}

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;
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.GameLogic.Spawning;
public class WorldEntitySpawner : SyncEntitySpawner<WorldEntity>
{
private readonly WorldEntitySpawnerResolver worldEntitySpawnResolver;
private readonly Dictionary<Int3, BatchCells> batchCellsById;
public WorldEntitySpawner(EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, Entities entities, SimulationOwnership simulationOwnership)
{
worldEntitySpawnResolver = new WorldEntitySpawnerResolver(entityMetadataManager, playerManager, localPlayer, entities, simulationOwnership);
if (NitroxEnvironment.IsNormal)
{
batchCellsById = (Dictionary<Int3, BatchCells>)LargeWorldStreamer.main.cellManager.batch2cells;
}
}
protected override IEnumerator SpawnAsync(WorldEntity entity, TaskResult<Optional<GameObject>> result)
{
EntityCell cellRoot = EnsureCell(entity);
if (cellRoot == null)
{
// Error logging is done in EnsureCell
return null;
}
Optional<GameObject> parent = (entity.ParentId != null) ? NitroxEntity.GetObjectFrom(entity.ParentId) : Optional.Empty;
IWorldEntitySpawner entitySpawner = worldEntitySpawnResolver.ResolveEntitySpawner(entity);
if (entitySpawner is IWorldEntitySyncSpawner syncSpawner &&
syncSpawner.SpawnSync(entity, parent, cellRoot, result))
{
return null;
}
return entitySpawner.SpawnAsync(entity, parent, cellRoot, result);
}
protected override bool SpawnsOwnChildren(WorldEntity entity)
{
IWorldEntitySpawner entitySpawner = worldEntitySpawnResolver.ResolveEntitySpawner(entity);
return entitySpawner.SpawnsOwnChildren();
}
protected override bool SpawnSync(WorldEntity entity, TaskResult<Optional<GameObject>> result)
{
EntityCell cellRoot = EnsureCell(entity);
if (cellRoot == null)
{
// Error logging is done in EnsureCell
return true;
}
Optional<GameObject> parent = (entity.ParentId != null) ? NitroxEntity.GetObjectFrom(entity.ParentId) : Optional.Empty;
IWorldEntitySpawner entitySpawner = worldEntitySpawnResolver.ResolveEntitySpawner(entity);
return entitySpawner is IWorldEntitySyncSpawner syncSpawner && syncSpawner.SpawnSync(entity, parent, cellRoot, result);
}
public EntityCell EnsureCell(WorldEntity entity)
{
EntityCell entityCell;
Int3 batchId = entity.AbsoluteEntityCell.BatchId.ToUnity();
Int3 cellId = entity.AbsoluteEntityCell.CellId.ToUnity();
if (!batchCellsById.TryGetValue(batchId, out BatchCells batchCells))
{
batchCells = LargeWorldStreamer.main.cellManager.InitializeBatchCells(batchId);
}
try
{
entityCell = batchCells.EnsureCell(cellId, entity.Level);
}
catch (Exception)
{
// Error logging is done in BatchCells.EnsureCell
return null;
}
entityCell.EnsureRoot();
return entityCell;
}
}