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,402 @@
using System;
using System.Collections.Generic;
using System.Linq;
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.Packets;
using NitroxModel.Serialization;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.GameLogic.Bases;
public class BuildingManager
{
private readonly EntityRegistry entityRegistry;
private readonly WorldEntityManager worldEntityManager;
private readonly SubnauticaServerConfig config;
public BuildingManager(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SubnauticaServerConfig config)
{
this.entityRegistry = entityRegistry;
this.worldEntityManager = worldEntityManager;
this.config = config;
}
public bool AddGhost(PlaceGhost placeGhost)
{
GhostEntity ghostEntity = placeGhost.GhostEntity;
if (ghostEntity.ParentId == null)
{
if (entityRegistry.GetEntityById(ghostEntity.Id).HasValue)
{
Log.Error($"Trying to add a ghost to Global Root but another entity with the same id already exists (GhostId: {ghostEntity.Id})");
return false;
}
worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity);
return true;
}
if (!entityRegistry.TryGetEntityById(ghostEntity.ParentId, out Entity parentEntity))
{
Log.Error($"Trying to add a ghost to a build that isn't registered (ParentId: {ghostEntity.ParentId})");
return false;
}
if (parentEntity is not BuildEntity)
{
Log.Error($"Trying to add a ghost to an entity that is not a building (ParentId: {ghostEntity.ParentId})");
return false;
}
if (parentEntity.ChildEntities.Any(childEntity => childEntity.Id.Equals(ghostEntity.Id)))
{
Log.Error($"Trying to add a ghost to a building but another child with the same id already exists (GhostId: {ghostEntity.Id})");
return false;
}
worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity);
return true;
}
public bool AddModule(PlaceModule placeModule)
{
ModuleEntity moduleEntity = placeModule.ModuleEntity;
if (moduleEntity.ParentId == null)
{
if (entityRegistry.GetEntityById(moduleEntity.Id).HasValue)
{
Log.Error($"Trying to add a module to Global Root but another entity with the same id already exists ({moduleEntity.Id})");
return false;
}
worldEntityManager.AddOrUpdateGlobalRootEntity(moduleEntity);
return true;
}
if (!entityRegistry.TryGetEntityById(moduleEntity.ParentId, out Entity parentEntity))
{
Log.Error($"Trying to add a module to a build that isn't registered (ParentId: {moduleEntity.ParentId})");
return false;
}
if (parentEntity is not BuildEntity && parentEntity is not VehicleWorldEntity)
{
Log.Error($"Trying to add a module to an entity that is not a building/vehicle (ParentId: {moduleEntity.ParentId})");
return false;
}
if (parentEntity.ChildEntities.Any(childEntity => childEntity.Id.Equals(moduleEntity.Id)))
{
Log.Error($"Trying to add a module to a building but another child with the same id already exists (ModuleId: {moduleEntity.Id})");
return false;
}
worldEntityManager.AddOrUpdateGlobalRootEntity(moduleEntity);
return true;
}
public bool ModifyConstructedAmount(ModifyConstructedAmount modifyConstructedAmount)
{
if (!entityRegistry.TryGetEntityById(modifyConstructedAmount.GhostId, out Entity entity))
{
Log.Error($"Trying to modify the constructed amount of a non-registered object (GhostId: {modifyConstructedAmount.GhostId})");
return false;
}
// Certain entities with a Constructable are just "regular" WorldEntities (e.g. starship boxes) and for simplicity we'll just not persist their progress
// since their only use is to be deconstructed to give materials to players
if (entity is not GhostEntity && entity is not ModuleEntity)
{
// In case the entity was fully deconstructed
if (modifyConstructedAmount.ConstructedAmount == 0f)
{
if (entity is GlobalRootEntity)
{
worldEntityManager.RemoveGlobalRootEntity(entity.Id);
}
else
{
entityRegistry.RemoveEntity(entity.Id);
}
}
// In any case we'll broadcast the packet
return true;
}
if (modifyConstructedAmount.ConstructedAmount == 0f)
{
worldEntityManager.RemoveGlobalRootEntity(entity.Id);
return true;
}
switch (entity)
{
case GhostEntity ghostEntity:
ghostEntity.ConstructedAmount = modifyConstructedAmount.ConstructedAmount;
break;
case ModuleEntity moduleEntity:
moduleEntity.ConstructedAmount = modifyConstructedAmount.ConstructedAmount;
break;
}
return true;
}
public bool CreateBase(PlaceBase placeBase)
{
if (!entityRegistry.TryGetEntityById(placeBase.FormerGhostId, out Entity entity))
{
Log.Error($"Trying to place a base from a non-registered ghost (Id: {placeBase.FormerGhostId})");
return false;
}
if (entity is not GhostEntity)
{
Log.Error($"Trying to add a new build to Global Root but another build with the same id already exists (GhostId: {placeBase.FormerGhostId})");
return false;
}
worldEntityManager.RemoveGlobalRootEntity(entity.Id);
worldEntityManager.AddOrUpdateGlobalRootEntity(placeBase.BuildEntity);
return true;
}
public bool UpdateBase(Player player, UpdateBase updateBase, out int operationId)
{
if (!entityRegistry.TryGetEntityById<GhostEntity>(updateBase.FormerGhostId, out _))
{
Log.Error($"Trying to place a base from a non-registered ghost (GhostId: {updateBase.FormerGhostId})");
operationId = -1;
return false;
}
if (!entityRegistry.TryGetEntityById(updateBase.BaseId, out BuildEntity buildEntity))
{
Log.Error($"Trying to update a non-registered build (BaseId: {updateBase.BaseId})");
operationId = -1;
return false;
}
int deltaOperations = buildEntity.OperationId + 1 - updateBase.OperationId;
if (deltaOperations != 0 && config.SafeBuilding)
{
Log.Warn($"Ignoring an {nameof(UpdateBase)} packet from [{player.Name}] which is {Math.Abs(deltaOperations) + (deltaOperations > 0 ? " operations ahead" : " operations late")}");
NotifyPlayerDesync(player);
operationId = -1;
return false;
}
worldEntityManager.RemoveGlobalRootEntity(updateBase.FormerGhostId);
buildEntity.BaseData = updateBase.BaseData;
foreach (KeyValuePair<NitroxId, NitroxBaseFace> updatedChild in updateBase.UpdatedChildren)
{
if (entityRegistry.TryGetEntityById(updatedChild.Key, out InteriorPieceEntity childEntity))
{
childEntity.BaseFace = updatedChild.Value;
}
}
foreach (KeyValuePair<NitroxId, NitroxInt3> updatedMoonpool in updateBase.UpdatedMoonpools)
{
if (entityRegistry.TryGetEntityById(updatedMoonpool.Key, out MoonpoolEntity childEntity))
{
childEntity.Cell = updatedMoonpool.Value;
}
}
foreach (KeyValuePair<NitroxId, NitroxInt3> updatedMapRoom in updateBase.UpdatedMapRooms)
{
if (entityRegistry.TryGetEntityById(updatedMapRoom.Key, out MapRoomEntity childEntity))
{
childEntity.Cell = updatedMapRoom.Value;
}
}
if (updateBase.BuiltPieceEntity != null && updateBase.BuiltPieceEntity is GlobalRootEntity builtPieceEntity)
{
worldEntityManager.AddOrUpdateGlobalRootEntity(builtPieceEntity);
}
NitroxId transferFromId = updateBase.ChildrenTransfer.Item1;
NitroxId transferToId = updateBase.ChildrenTransfer.Item2;
if (transferFromId != null && transferToId != null)
{
// NB: we don't want certain entities to be transferred (e.g. planters)
entityRegistry.TransferChildren(transferFromId, transferToId, entity => entity is not PlanterEntity);
// Edge case for when you place a new water park under another one
if (transferFromId == transferToId && entityRegistry.TryGetEntityById(updateBase.ChildrenTransfer.Item1, out InteriorPieceEntity interiorPieceEntity) &&
interiorPieceEntity.IsWaterPark)
{
// Cleaning all elements from the planter because plants are not kept in this case
CleanPlanterChildren(interiorPieceEntity);
}
}
// After transferring required children, we need to clean the waterparks that were potentially removed when being merged
List<NitroxId> removedChildIds = buildEntity.ChildEntities.OfType<InteriorPieceEntity>()
.Where(entity => entity.IsWaterPark).Select(childEntity => childEntity.Id)
.Except(updateBase.UpdatedChildren.Keys).ToList();
foreach (NitroxId removedChildId in removedChildIds)
{
if (entityRegistry.GetEntityById(removedChildId).HasValue)
{
worldEntityManager.RemoveGlobalRootEntity(removedChildId);
}
}
buildEntity.OperationId++;
operationId = buildEntity.OperationId;
return true;
}
/// <summary>
/// Removes all children from the planter(s) of the param water park (<paramref name="interiorPieceEntity"/>)
/// </summary>
private void CleanPlanterChildren(InteriorPieceEntity interiorPieceEntity)
{
NitroxId leftPlanterId = interiorPieceEntity.Id.Increment();
NitroxId rightPlanterId = leftPlanterId.Increment();
// We don't care if either one of those doesn't exist, we just treat both possibility
// which avoids an o(n) search in interiorPieceEntity's children, and makes it into a o(1) get
if (entityRegistry.TryGetEntityById(leftPlanterId, out PlanterEntity leftPlanterEntity))
{
entityRegistry.CleanChildren(leftPlanterEntity);
}
if (entityRegistry.TryGetEntityById(rightPlanterId, out PlanterEntity rightPlanterEntity))
{
entityRegistry.CleanChildren(rightPlanterEntity);
}
}
public bool ReplaceBaseByGhost(BaseDeconstructed baseDeconstructed)
{
if (!entityRegistry.TryGetEntityById(baseDeconstructed.FormerBaseId, out BuildEntity _))
{
Log.Error($"Trying to replace a non-registered build (BaseId: {baseDeconstructed.FormerBaseId})");
return false;
}
worldEntityManager.RemoveGlobalRootEntity(baseDeconstructed.FormerBaseId);
worldEntityManager.AddOrUpdateGlobalRootEntity(baseDeconstructed.ReplacerGhost);
return true;
}
public bool ReplacePieceByGhost(Player player, PieceDeconstructed pieceDeconstructed, out Entity removedEntity, out int operationId)
{
if (!entityRegistry.TryGetEntityById(pieceDeconstructed.BaseId, out BuildEntity buildEntity))
{
Log.Error($"Trying to replace a non-registered build (BaseId: {pieceDeconstructed.BaseId})");
removedEntity = null;
operationId = -1;
return false;
}
if (entityRegistry.TryGetEntityById(pieceDeconstructed.PieceId, out GhostEntity _))
{
Log.Error($"Trying to add a ghost to a building but another ghost child with the same id already exists (GhostId: {pieceDeconstructed.PieceId})");
removedEntity = null;
operationId = -1;
return false;
}
int deltaOperations = buildEntity.OperationId + 1 - pieceDeconstructed.OperationId;
if (deltaOperations != 0 && config.SafeBuilding)
{
Log.Warn($"Ignoring a {nameof(PieceDeconstructed)} packet from [{player.Name}] which is {Math.Abs(deltaOperations) + (deltaOperations > 0 ? " operations ahead" : " operations late")}");
NotifyPlayerDesync(player);
removedEntity = null;
operationId = -1;
return false;
}
removedEntity = worldEntityManager.RemoveGlobalRootEntity(pieceDeconstructed.PieceId).Value;
GhostEntity ghostEntity = pieceDeconstructed.ReplacerGhost;
worldEntityManager.AddOrUpdateGlobalRootEntity(ghostEntity);
buildEntity.BaseData = pieceDeconstructed.BaseData;
buildEntity.OperationId++;
operationId = buildEntity.OperationId;
return true;
}
public bool CreateWaterParkPiece(WaterParkDeconstructed waterParkDeconstructed, Entity removedEntity)
{
if (!entityRegistry.TryGetEntityById(waterParkDeconstructed.BaseId, out BuildEntity buildEntity))
{
Log.Error($"Trying to create a WaterPark piece in a non-registered build ({waterParkDeconstructed.BaseId})");
return false;
}
InteriorPieceEntity newPiece = waterParkDeconstructed.NewWaterPark;
worldEntityManager.AddOrUpdateGlobalRootEntity(newPiece);
// This part doesn't need all the below register again code because in this very case
// (a water park split between a below and an above one) the below water park is still the same
foreach (NitroxId childId in waterParkDeconstructed.MovedChildrenIds)
{
entityRegistry.ReparentEntity(childId, newPiece);
}
if (removedEntity != null && waterParkDeconstructed.Transfer)
{
// Children are all removed by BuildingManager.ReplacePieceByGhost which is called before
// so we need to re-register them
// NB: must copy the list before because the below functions do modify newPiece.ChildEntities
List<Entity> childEntities = [.. removedEntity.ChildEntities.Where(e => e is not PlanterEntity)];
foreach (Entity childEntity in childEntities)
{
entityRegistry.RemoveFromParent(childEntity);
childEntity.ParentId = newPiece.Id;
if (childEntity is GlobalRootEntity childGlobalRootEntity)
{
worldEntityManager.AddOrUpdateGlobalRootEntity(childGlobalRootEntity);
}
else
{
entityRegistry.AddOrUpdate(childEntity);
}
}
}
return true;
}
public bool SeparateChildrenToWaterParks(LargeWaterParkDeconstructed packet)
{
if (packet.MovedChildrenIdsByNewHostId.Count == 0)
{
return true;
}
if (packet.PieceId == null || !entityRegistry.TryGetEntityById(packet.PieceId, out InteriorPieceEntity deconstructedPiece))
{
Log.Error($"Could not find {nameof(InteriorPieceEntity)} with id {packet.PieceId} to be deconstructed");
return false;
}
foreach (KeyValuePair<NitroxId, List<NitroxId>> entry in packet.MovedChildrenIdsByNewHostId)
{
if (!entityRegistry.TryGetEntityById(entry.Key, out InteriorPieceEntity waterPark))
{
Log.Error($"Could not find {nameof(InteriorPieceEntity)} id {entry.Key} to move {entry.Value} entities to");
continue;
}
foreach (NitroxId entityId in entry.Value)
{
entityRegistry.ReparentEntity(entityId, waterPark);
}
}
return true;
}
private void NotifyPlayerDesync(Player player)
{
Dictionary<NitroxId, int> operations = GetEntitiesOperations(worldEntityManager.GetGlobalRootEntities(true));
player.SendPacket(new BuildingDesyncWarning(operations));
}
public static Dictionary<NitroxId, int> GetEntitiesOperations(List<GlobalRootEntity> entities)
{
return entities.OfType<BuildEntity>().ToDictionary(entity => entity.Id, entity => entity.OperationId);
}
}