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