Files
Nitrox/NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs
2025-07-06 00:23:46 +02:00

171 lines
7.3 KiB
C#

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