first commit
This commit is contained in:
316
NitroxClient/GameLogic/Bases/BuildUtils.cs
Normal file
316
NitroxClient/GameLogic/Bases/BuildUtils.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.Spawning.Bases;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
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.Bases;
|
||||
|
||||
public static class BuildUtils
|
||||
{
|
||||
public static bool TryGetIdentifier(BaseDeconstructable baseDeconstructable, out BuildPieceIdentifier identifier, BaseCell baseCell = null, Base.Face? baseFace = null)
|
||||
{
|
||||
// It is unimaginable to have a BaseDeconstructable that is not child of a BaseCell
|
||||
if (!baseCell && !baseDeconstructable.TryGetComponentInParent(out baseCell, true))
|
||||
{
|
||||
identifier = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
identifier = GetIdentifier(baseDeconstructable, baseCell, baseFace);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static BuildPieceIdentifier GetIdentifier(BaseDeconstructable baseDeconstructable, BaseCell baseCell, Base.Face? baseFace = null)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Recipe = baseDeconstructable.recipe.ToDto(),
|
||||
BaseFace = baseFace?.ToDto() ?? baseDeconstructable.face?.ToDto(),
|
||||
BaseCell = baseCell.cell.ToDto(),
|
||||
PiecePoint = baseDeconstructable.deconstructedBase.WorldToGrid(baseDeconstructable.transform.position).ToDto()
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetGhostFace(BaseGhost baseGhost, out Base.Face face)
|
||||
{
|
||||
// Copied code from BaseAddModuleGhost.Finish() and BaseAddFaceGhost.Finish() to obtain the face at which the module was spawned
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddModuleGhost moduleGhost:
|
||||
face = moduleGhost.anchoredFace.Value;
|
||||
face.cell += baseGhost.targetBase.GetAnchor();
|
||||
return true;
|
||||
case BaseAddFaceGhost faceGhost:
|
||||
if (faceGhost.anchoredFace.HasValue)
|
||||
{
|
||||
face = faceGhost.anchoredFace.Value;
|
||||
face.cell += faceGhost.targetBase.GetAnchor();
|
||||
return true;
|
||||
}
|
||||
if (BaseAddFaceGhost.FindFirstMaskedFace(faceGhost.ghostBase, out face))
|
||||
{
|
||||
Vector3 point = faceGhost.ghostBase.GridToWorld(Int3.zero);
|
||||
faceGhost.targetOffset = faceGhost.targetBase.WorldToGrid(point);
|
||||
face.cell += faceGhost.targetOffset;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case BaseAddWaterPark waterPark:
|
||||
if (waterPark.anchoredFace.HasValue)
|
||||
{
|
||||
face = waterPark.anchoredFace.Value;
|
||||
face.cell += waterPark.targetBase.GetAnchor();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case BaseAddMapRoomGhost:
|
||||
face = new(GetMapRoomFunctionalityCell(baseGhost), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
face = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Even if the corresponding module was found, in some cases (with WaterParks notably) we don't want to transfer the id.
|
||||
/// We then return false because the GameObject may have already been marked.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// Whether or not the id was successfully transferred
|
||||
/// </returns>
|
||||
public static bool TryTransferIdFromGhostToModule(BaseGhost baseGhost, NitroxId id, ConstructableBase constructableBase, out GameObject moduleObject)
|
||||
{
|
||||
// 1. Find the face of the target piece
|
||||
Base.Face? face = null;
|
||||
bool isWaterPark = baseGhost is BaseAddWaterPark;
|
||||
bool isMapRoomGhost = baseGhost is BaseAddMapRoomGhost;
|
||||
// Only four types of ghost which spawn a module
|
||||
if (baseGhost is BaseAddFaceGhost faceGhost && faceGhost.modulePrefab ||
|
||||
baseGhost is BaseAddModuleGhost moduleGhost && moduleGhost.modulePrefab ||
|
||||
isMapRoomGhost ||
|
||||
isWaterPark)
|
||||
{
|
||||
if (TryGetGhostFace(baseGhost, out Base.Face ghostFace))
|
||||
{
|
||||
face = ghostFace;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Couldn't find the module spawned by {baseGhost}");
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// If the ghost is under a BaseDeconstructable(Clone), it may have an associated module
|
||||
else if (IsBaseDeconstructable(constructableBase))
|
||||
{
|
||||
face = new(constructableBase.moduleFace.Value.cell + baseGhost.targetBase.GetAnchor(), constructableBase.moduleFace.Value.direction);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (constructableBase.techType)
|
||||
{
|
||||
case TechType.BaseWaterPark:
|
||||
// Edge case that happens when a Deconstructed WaterPark is built onto another deconstructed WaterPark that has its module
|
||||
// A new module will be created by the current Deconstructed WaterPark which is the one we'll be aiming at
|
||||
IBaseModuleGeometry baseModuleGeometry = constructableBase.GetComponentInChildren<IBaseModuleGeometry>(true);
|
||||
if (baseModuleGeometry != null)
|
||||
{
|
||||
face = baseModuleGeometry.geometryFace;
|
||||
}
|
||||
break;
|
||||
|
||||
case TechType.BaseMoonpool:
|
||||
// Moonpools are a very specific case, we tweak them to work as interior pieces (while they're not)
|
||||
Optional<GameObject> objectOptional = baseGhost.targetBase.gameObject.EnsureComponent<MoonpoolManager>().RegisterMoonpool(constructableBase.transform, id);
|
||||
moduleObject = objectOptional.Value;
|
||||
return moduleObject;
|
||||
|
||||
case TechType.BaseMapRoom:
|
||||
// In the case the ghost is under a BaseDeconstructable, this is a good way to identify a MapRoom
|
||||
face = new(GetMapRoomFunctionalityCell(baseGhost), 0);
|
||||
isMapRoomGhost = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!face.HasValue)
|
||||
{
|
||||
if (constructableBase.techType != TechType.BaseWaterPark)
|
||||
{
|
||||
Log.Error($"No face could be found for ghost {baseGhost}");
|
||||
}
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Use that face to find the newly created piece and set its id to the desired one
|
||||
if (isMapRoomGhost)
|
||||
{
|
||||
MapRoomFunctionality mapRoomFunctionality = baseGhost.targetBase.GetMapRoomFunctionalityForCell(face.Value.cell);
|
||||
if (mapRoomFunctionality)
|
||||
{
|
||||
// As MapRooms can be built as the first piece of a base, we need to make sure that they receive a new id if they're not in a base
|
||||
if (constructableBase.GetComponentInParent<Base>(true))
|
||||
{
|
||||
NitroxEntity.SetNewId(mapRoomFunctionality.gameObject, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
NitroxEntity.SetNewId(mapRoomFunctionality.gameObject, id.Increment());
|
||||
}
|
||||
moduleObject = mapRoomFunctionality.gameObject;
|
||||
return true;
|
||||
}
|
||||
Log.Error($"Couldn't find MapRoomFunctionality of built MapRoom (cell: {face.Value.cell})");
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
IBaseModule module = baseGhost.targetBase.GetModule(face.Value);
|
||||
if (module != null)
|
||||
{
|
||||
// If the WaterPark is higher than one, it means that the newly built WaterPark will be merged with one that already has a NitroxEntity
|
||||
if (module is WaterPark waterPark && waterPark.height > 1)
|
||||
{
|
||||
// as the WaterPark is necessarily merged, we won't need to do anything about it
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
moduleObject = (module as Component).gameObject;
|
||||
NitroxEntity.SetNewId(moduleObject, id);
|
||||
return true;
|
||||
}
|
||||
// When a WaterPark is merged with another one, we won't find its module but we don't care about that
|
||||
if (!isWaterPark)
|
||||
{
|
||||
Log.Error("Couldn't find the module's GameObject of built interior piece when transferring its NitroxEntity to the module.");
|
||||
}
|
||||
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The criteria to make sure that a ConstructableBase is one of a BaseDeconstructable is if it has a moduleFace
|
||||
/// because this field is only filled for the base deconstruction (<see cref="BaseDeconstructable.Deconstruct"/>, <seealso cref="ConstructableBase.LinkModule(Base.Face?)"/>).
|
||||
/// </remarks>
|
||||
public static bool IsBaseDeconstructable(ConstructableBase constructableBase)
|
||||
{
|
||||
return constructableBase.moduleFace.HasValue;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A BaseDeconstructable's ghost component is a simple BaseGhost so we need to identify it by the parent ConstructableBase instead.
|
||||
/// </remarks>
|
||||
/// <param name="faceAlreadyLinked">Whether <see cref="ConstructableBase.moduleFace"/> was already set or not</param>
|
||||
public static bool IsUnderBaseDeconstructable(BaseGhost baseGhost, bool faceAlreadyLinked)
|
||||
{
|
||||
return baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true) &&
|
||||
(IsBaseDeconstructable(constructableBase) || !faceAlreadyLinked);
|
||||
}
|
||||
|
||||
public static Int3 GetMapRoomFunctionalityCell(BaseGhost baseGhost)
|
||||
{
|
||||
// Code found from Base.GetMapRoomFunctionalityForCell
|
||||
return baseGhost.targetBase.NormalizeCell(baseGhost.targetBase.WorldToGrid(baseGhost.ghostBase.occupiedBounds.center));
|
||||
}
|
||||
|
||||
public static MapRoomEntity CreateMapRoomEntityFrom(MapRoomFunctionality mapRoomFunctionality, Base @base, NitroxId id, NitroxId parentId)
|
||||
{
|
||||
Int3 mapRoomCell = @base.NormalizeCell(@base.WorldToGrid(mapRoomFunctionality.transform.position));
|
||||
return new(id, parentId, mapRoomCell.ToDto());
|
||||
}
|
||||
|
||||
// TODO: Use this for a latter singleplayer save converter
|
||||
public static List<GlobalRootEntity> GetGlobalRootChildren(Transform globalRoot, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
List<GlobalRootEntity> entities = new();
|
||||
foreach (Transform child in globalRoot)
|
||||
{
|
||||
if (child.TryGetComponent(out Base @base))
|
||||
{
|
||||
entities.Add(BuildEntitySpawner.From(@base, entityMetadataManager));
|
||||
}
|
||||
else if (child.TryGetComponent(out Constructable constructable))
|
||||
{
|
||||
if (constructable is ConstructableBase constructableBase)
|
||||
{
|
||||
entities.Add(GhostEntitySpawner.From(constructableBase));
|
||||
continue;
|
||||
}
|
||||
entities.Add(ModuleEntitySpawner.From(constructable));
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static List<Entity> GetChildEntities(Base targetBase, NitroxId baseId, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
List<Entity> childEntities = new();
|
||||
void AddChild(Entity childEntity)
|
||||
{
|
||||
// Making sure that childEntities are correctly parented
|
||||
childEntity.ParentId = baseId;
|
||||
childEntities.Add(childEntity);
|
||||
}
|
||||
|
||||
foreach (Transform transform in targetBase.transform)
|
||||
{
|
||||
if (transform.TryGetComponent(out MapRoomFunctionality mapRoomFunctionality))
|
||||
{
|
||||
if (!mapRoomFunctionality.TryGetNitroxId(out NitroxId mapRoomId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AddChild(CreateMapRoomEntityFrom(mapRoomFunctionality, targetBase, mapRoomId, baseId));
|
||||
}
|
||||
else if (transform.TryGetComponent(out IBaseModule baseModule))
|
||||
{
|
||||
// IBaseModules without a NitroxEntity are related to BaseDeconstructable and are saved with their ghost
|
||||
if (!(baseModule as MonoBehaviour).GetComponent<NitroxEntity>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MonoBehaviour moduleMB = baseModule as MonoBehaviour;
|
||||
AddChild(InteriorPieceEntitySpawner.From(baseModule, entityMetadataManager));
|
||||
}
|
||||
else if (transform.TryGetComponent(out Constructable constructable))
|
||||
{
|
||||
if (constructable is ConstructableBase constructableBase)
|
||||
{
|
||||
AddChild(GhostEntitySpawner.From(constructableBase));
|
||||
continue;
|
||||
}
|
||||
AddChild(ModuleEntitySpawner.From(constructable));
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBase.TryGetComponent(out MoonpoolManager nitroxMoonpool))
|
||||
{
|
||||
nitroxMoonpool.GetSavedMoonpools().ForEach(AddChild);
|
||||
}
|
||||
|
||||
return childEntities;
|
||||
}
|
||||
|
||||
public static Component AliveOrNull(this IBaseModule baseModule)
|
||||
{
|
||||
return (baseModule as Component).AliveOrNull();
|
||||
}
|
||||
}
|
410
NitroxClient/GameLogic/Bases/BuildingHandler.cs
Normal file
410
NitroxClient/GameLogic/Bases/BuildingHandler.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Spawning.Bases;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Bases;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public partial class BuildingHandler : MonoBehaviour
|
||||
{
|
||||
public static BuildingHandler Main;
|
||||
|
||||
public Queue<Packet> BuildQueue;
|
||||
private bool working;
|
||||
|
||||
public Dictionary<NitroxId, DateTimeOffset> BasesCooldown;
|
||||
|
||||
/// <summary>
|
||||
/// When processing deconstruction-related packets, it's required to pass part of their data to the patches
|
||||
/// so that they can work accordingly (mainly to differentiate local actions from remotely issued ones).
|
||||
/// </summary>
|
||||
public TemporaryBuildData Temp;
|
||||
|
||||
/// <summary>
|
||||
/// TimeSpan before which local player can build on a base that was modified by another player
|
||||
/// </summary>
|
||||
private static readonly TimeSpan MultiplayerBuildCooldown = TimeSpan.FromSeconds(2);
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Main)
|
||||
{
|
||||
Log.Error($"Another instance of {nameof(BuildingHandler)} is already running. Deleting the current one.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Main = this;
|
||||
BuildQueue = new();
|
||||
LatestResyncRequestTimeOffset = DateTimeOffset.UtcNow;
|
||||
BasesCooldown = new();
|
||||
Temp = new();
|
||||
Operations = new();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
CleanCooldowns();
|
||||
if (BuildQueue.Count > 0 && !working && !Resyncing)
|
||||
{
|
||||
working = true;
|
||||
StartCoroutine(SafelyTreatNextBuildCommand());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SafelyTreatNextBuildCommand()
|
||||
{
|
||||
Packet packet = BuildQueue.Dequeue();
|
||||
yield return TreatBuildCommand(packet).OnYieldError(exception => Log.Error(exception, $"An error happened when processing build command {packet}"));
|
||||
working = false;
|
||||
}
|
||||
|
||||
private IEnumerator TreatBuildCommand(Packet buildCommand)
|
||||
{
|
||||
switch (buildCommand)
|
||||
{
|
||||
case PlaceGhost placeGhost:
|
||||
yield return BuildGhost(placeGhost);
|
||||
break;
|
||||
case PlaceModule placeModule:
|
||||
yield return BuildModule(placeModule);
|
||||
break;
|
||||
case ModifyConstructedAmount modifyConstructedAmount:
|
||||
PeekNextModifyCommands(ref modifyConstructedAmount);
|
||||
yield return ProgressConstruction(modifyConstructedAmount);
|
||||
break;
|
||||
case UpdateBase updateBase:
|
||||
yield return UpdatePlacedBase(updateBase);
|
||||
break;
|
||||
case PlaceBase placeBase:
|
||||
yield return BuildBase(placeBase);
|
||||
break;
|
||||
case BaseDeconstructed baseDeconstructed:
|
||||
yield return DeconstructBase(baseDeconstructed);
|
||||
break;
|
||||
case PieceDeconstructed pieceDeconstructed:
|
||||
yield return DeconstructPiece(pieceDeconstructed);
|
||||
break;
|
||||
default:
|
||||
Log.Error($"Found an unhandled build command packet: {buildCommand}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the next build command is also a ModifyConstructedAmount applied on the same object, we'll just skip the current one to apply the new one.
|
||||
/// </summary>
|
||||
private void PeekNextModifyCommands(ref ModifyConstructedAmount currentCommand)
|
||||
{
|
||||
while (BuildQueue.Count > 0 && BuildQueue.Peek() is ModifyConstructedAmount nextCommand && nextCommand.GhostId.Equals(currentCommand.GhostId))
|
||||
{
|
||||
BuildQueue.Dequeue();
|
||||
currentCommand = nextCommand;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator BuildGhost(PlaceGhost placeGhost)
|
||||
{
|
||||
GhostEntity ghostEntity = placeGhost.GhostEntity;
|
||||
Transform parent = GetParentOrGlobalRoot(ghostEntity.ParentId);
|
||||
yield return GhostEntitySpawner.RestoreGhost(parent, ghostEntity);
|
||||
BasesCooldown[ghostEntity.ParentId ?? ghostEntity.Id] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator BuildModule(PlaceModule placeModule)
|
||||
{
|
||||
ModuleEntity moduleEntity = placeModule.ModuleEntity;
|
||||
Transform parent = GetParentOrGlobalRoot(moduleEntity.ParentId);
|
||||
TaskResult<Optional<GameObject>> result = new();
|
||||
yield return ModuleEntitySpawner.RestoreModule(parent, moduleEntity, result);
|
||||
if (result.value.HasValue)
|
||||
{
|
||||
this.Resolve<EntityMetadataManager>().ApplyMetadata(result.value.Value.gameObject, moduleEntity.Metadata);
|
||||
}
|
||||
BasesCooldown[moduleEntity.ParentId ?? moduleEntity.Id] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator ProgressConstruction(ModifyConstructedAmount modifyConstructedAmount)
|
||||
{
|
||||
if (NitroxEntity.TryGetComponentFrom(modifyConstructedAmount.GhostId, out Constructable constructable))
|
||||
{
|
||||
BasesCooldown[modifyConstructedAmount.GhostId] = DateTimeOffset.UtcNow;
|
||||
if (modifyConstructedAmount.ConstructedAmount == 0f)
|
||||
{
|
||||
constructable.constructedAmount = 0f;
|
||||
yield return constructable.ProgressDeconstruction();
|
||||
Destroy(constructable.gameObject);
|
||||
BasesCooldown.Remove(modifyConstructedAmount.GhostId);
|
||||
yield break;
|
||||
}
|
||||
if (modifyConstructedAmount.ConstructedAmount >= 1f)
|
||||
{
|
||||
constructable.SetState(true, true);
|
||||
yield return BuildingPostSpawner.ApplyPostSpawner(gameObject, modifyConstructedAmount.GhostId);
|
||||
yield break;
|
||||
}
|
||||
constructable.SetState(false, false);
|
||||
constructable.constructedAmount = modifyConstructedAmount.ConstructedAmount;
|
||||
yield return constructable.ProgressDeconstruction();
|
||||
constructable.UpdateMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator BuildBase(PlaceBase placeBase)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(placeBase.FormerGhostId, out ConstructableBase constructableBase))
|
||||
{
|
||||
FailedOperations++;
|
||||
yield break;
|
||||
}
|
||||
BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
|
||||
constructableBase.SetState(true, true);
|
||||
NitroxEntity.SetNewId(baseGhost.targetBase.gameObject, placeBase.FormerGhostId);
|
||||
|
||||
// Specific case : just a moonpool built as a base
|
||||
if (constructableBase.techType == TechType.BaseMoonpool)
|
||||
{
|
||||
// For a new base, the moonpool will be the only cell which is 0, 0, 0
|
||||
Int3 absoluteCell = new(0, 0, 0);
|
||||
// Deterministic id, see MoonpoolManager.LateAssignNitroxEntity
|
||||
NitroxId moonpoolId = placeBase.FormerGhostId.Increment();
|
||||
baseGhost.targetBase.gameObject.EnsureComponent<MoonpoolManager>().RegisterMoonpool(absoluteCell, moonpoolId);
|
||||
}
|
||||
|
||||
BasesCooldown[placeBase.FormerGhostId] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator UpdatePlacedBase(UpdateBase updateBase)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom<Base>(updateBase.BaseId, out _))
|
||||
{
|
||||
Log.Error($"Couldn't find base with id: {updateBase.BaseId} when processing packet: {updateBase}");
|
||||
FailedOperations++;
|
||||
yield break;
|
||||
}
|
||||
|
||||
OperationTracker tracker = EnsureTracker(updateBase.BaseId);
|
||||
tracker.RegisterOperation(updateBase.OperationId);
|
||||
|
||||
if (!NitroxEntity.TryGetComponentFrom(updateBase.FormerGhostId, out ConstructableBase constructableBase))
|
||||
{
|
||||
tracker.FailedOperations++;
|
||||
Log.Error($"Couldn't find ghost with id: {updateBase.FormerGhostId} when processing packet: {updateBase}");
|
||||
yield break;
|
||||
}
|
||||
Temp.ChildrenTransfer = updateBase.ChildrenTransfer;
|
||||
|
||||
BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
|
||||
constructableBase.SetState(true, true);
|
||||
BasesCooldown[updateBase.BaseId] = DateTimeOffset.UtcNow;
|
||||
// In the case the built piece was an interior piece, we'll want to transfer the id to it.
|
||||
if (BuildUtils.TryTransferIdFromGhostToModule(baseGhost, updateBase.FormerGhostId, constructableBase, out GameObject moduleObject))
|
||||
{
|
||||
yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, updateBase.FormerGhostId);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator DeconstructBase(BaseDeconstructed baseDeconstructed)
|
||||
{
|
||||
if (!NitroxEntity.TryGetObjectFrom(baseDeconstructed.FormerBaseId, out GameObject baseObject))
|
||||
{
|
||||
FailedOperations++;
|
||||
Log.Error($"Couldn't find base with id: {baseDeconstructed.FormerBaseId} when processing packet: {baseDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
BaseDeconstructable[] deconstructableChildren = baseObject.GetComponentsInChildren<BaseDeconstructable>(true);
|
||||
|
||||
if (deconstructableChildren.Length == 1 && deconstructableChildren[0])
|
||||
{
|
||||
using (PacketSuppressor<BaseDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<PieceDeconstructed>.Suppress())
|
||||
{
|
||||
deconstructableChildren[0].Deconstruct();
|
||||
}
|
||||
BasesCooldown[baseDeconstructed.FormerBaseId] = DateTimeOffset.UtcNow;
|
||||
yield break;
|
||||
}
|
||||
Log.Error($"Found multiple {nameof(BaseDeconstructable)} under base {baseObject} while there should be only one");
|
||||
EnsureTracker(baseDeconstructed.FormerBaseId).FailedOperations++;
|
||||
}
|
||||
|
||||
public IEnumerator DeconstructPiece(PieceDeconstructed pieceDeconstructed)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(pieceDeconstructed.BaseId, out Base @base))
|
||||
{
|
||||
FailedOperations++;
|
||||
Log.Error($"Couldn't find base with id: {pieceDeconstructed.BaseId} when processing packet: {pieceDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
OperationTracker tracker = EnsureTracker(pieceDeconstructed.BaseId);
|
||||
|
||||
BuildPieceIdentifier pieceIdentifier = pieceDeconstructed.BuildPieceIdentifier;
|
||||
Transform cellObject = @base.GetCellObject(pieceIdentifier.BaseCell.ToUnity());
|
||||
if (!cellObject)
|
||||
{
|
||||
Log.Error($"Couldn't find cell object {pieceIdentifier.BaseCell} when destructing piece {pieceDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
BaseDeconstructable[] deconstructableChildren = cellObject.GetComponentsInChildren<BaseDeconstructable>(true);
|
||||
foreach (BaseDeconstructable baseDeconstructable in deconstructableChildren)
|
||||
{
|
||||
if (!BuildUtils.TryGetIdentifier(baseDeconstructable, out BuildPieceIdentifier identifier) || !identifier.Equals(pieceIdentifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using (PacketSuppressor<BaseDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<PieceDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<WaterParkDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<LargeWaterParkDeconstructed>.Suppress())
|
||||
using (Temp.Fill(pieceDeconstructed))
|
||||
{
|
||||
baseDeconstructable.Deconstruct();
|
||||
}
|
||||
tracker.RegisterOperation(pieceDeconstructed.OperationId);
|
||||
BasesCooldown[pieceDeconstructed.BaseId] = DateTimeOffset.UtcNow;
|
||||
yield break;
|
||||
}
|
||||
Log.Error($"Couldn't find the right BaseDeconstructable to be destructed under {pieceDeconstructed.BaseId}");
|
||||
tracker.FailedOperations++;
|
||||
}
|
||||
|
||||
public static Transform GetParentOrGlobalRoot(NitroxId id)
|
||||
{
|
||||
if (id != null && NitroxEntity.TryGetObjectFrom(id, out GameObject parentObject))
|
||||
{
|
||||
return parentObject.transform;
|
||||
}
|
||||
return LargeWorldStreamer.main.globalRoot.transform;
|
||||
}
|
||||
|
||||
private void CleanCooldowns()
|
||||
{
|
||||
BasesCooldown.RemoveWhere(DateTimeOffset.UtcNow, (time, curr) => (curr - time) >= MultiplayerBuildCooldown);
|
||||
}
|
||||
|
||||
public class TemporaryBuildData : IDisposable
|
||||
{
|
||||
public NitroxId Id;
|
||||
public InteriorPieceEntity NewWaterPark;
|
||||
public List<NitroxId> MovedChildrenIds;
|
||||
public (NitroxId, NitroxId) ChildrenTransfer;
|
||||
public bool Transfer;
|
||||
public Dictionary<NitroxId, List<NitroxId>> MovedChildrenIdsByNewHostId;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Id = null;
|
||||
NewWaterPark = null;
|
||||
MovedChildrenIds = null;
|
||||
ChildrenTransfer = (null, null);
|
||||
Transfer = false;
|
||||
MovedChildrenIdsByNewHostId = null;
|
||||
}
|
||||
|
||||
public TemporaryBuildData Fill(PieceDeconstructed pieceDeconstructed)
|
||||
{
|
||||
Id = pieceDeconstructed.PieceId;
|
||||
if (pieceDeconstructed is WaterParkDeconstructed waterParkDeconstructed)
|
||||
{
|
||||
NewWaterPark = waterParkDeconstructed.NewWaterPark;
|
||||
MovedChildrenIds = waterParkDeconstructed.MovedChildrenIds;
|
||||
Transfer = waterParkDeconstructed.Transfer;
|
||||
return this;
|
||||
}
|
||||
if (pieceDeconstructed is LargeWaterParkDeconstructed largeWaterParkDeconstructed)
|
||||
{
|
||||
MovedChildrenIdsByNewHostId = largeWaterParkDeconstructed.MovedChildrenIdsByNewHostId;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Building resync-related part of <see cref="BuildingHandler"/>.
|
||||
/// </summary>
|
||||
public partial class BuildingHandler
|
||||
{
|
||||
private static readonly TimeSpan ResyncRequestCooldown = TimeSpan.FromSeconds(10);
|
||||
private DateTimeOffset LatestResyncRequestTimeOffset;
|
||||
|
||||
private Dictionary<NitroxId, OperationTracker> Operations;
|
||||
// TODO: Should be used to track total fails when more stuff is built towards resyncing
|
||||
public int FailedOperations;
|
||||
|
||||
public bool Resyncing;
|
||||
|
||||
public OperationTracker EnsureTracker(NitroxId baseId)
|
||||
{
|
||||
if (!Operations.TryGetValue(baseId, out OperationTracker tracker))
|
||||
{
|
||||
Operations[baseId] = tracker = new();
|
||||
}
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public int GetCurrentOperationIdOrDefault(NitroxId baseId)
|
||||
{
|
||||
if (baseId != null && Operations.TryGetValue(baseId, out OperationTracker tracker))
|
||||
{
|
||||
return tracker.LastOperationId + tracker.LocalOperations;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void StartResync<T>(Dictionary<T, int> entities) where T : Entity
|
||||
{
|
||||
Resyncing = true;
|
||||
FailedOperations = 0;
|
||||
BuildQueue.Clear();
|
||||
working = true;
|
||||
InitializeOperations(entities.ToDictionary(pair => pair.Key.Id, pair => pair.Value));
|
||||
}
|
||||
|
||||
public void StopResync()
|
||||
{
|
||||
working = false;
|
||||
Resyncing = false;
|
||||
}
|
||||
|
||||
public void InitializeOperations(Dictionary<NitroxId, int> operations)
|
||||
{
|
||||
foreach (KeyValuePair<NitroxId, int> pair in operations)
|
||||
{
|
||||
EnsureTracker(pair.Key).ResetToId(pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AskForResync()
|
||||
{
|
||||
if (!Multiplayer.Main || !Multiplayer.Main.InitialSyncCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TimeSpan deltaTime = DateTimeOffset.UtcNow - LatestResyncRequestTimeOffset;
|
||||
if (deltaTime < ResyncRequestCooldown)
|
||||
{
|
||||
double timeLeft = ResyncRequestCooldown.TotalSeconds - deltaTime.TotalSeconds;
|
||||
Log.InGame(Language.main.Get("Nitrox_ResyncOnCooldown").Replace("{TIME_LEFT}", string.Format("{0:N2}", timeLeft)));
|
||||
return;
|
||||
}
|
||||
LatestResyncRequestTimeOffset = DateTimeOffset.UtcNow;
|
||||
|
||||
this.Resolve<IPacketSender>().Send(new BuildingResyncRequest());
|
||||
Log.InGame(Language.main.Get("Nitrox_ResyncRequested"));
|
||||
}
|
||||
}
|
142
NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs
Normal file
142
NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public static class GhostMetadataApplier
|
||||
{
|
||||
public static void ApplyBasicMetadataTo(this GhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
baseGhost.targetOffset = ghostMetadata.TargetOffset.ToUnity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the given metadata to a ghost depending on their types.
|
||||
/// </summary>
|
||||
/// <returns>An extra instruction set to yield for BaseDeconstructable ghosts or null if unrequired.</returns>
|
||||
public static IEnumerator ApplyMetadataToGhost(BaseGhost baseGhost, EntityMetadata entityMetadata, Base @base)
|
||||
{
|
||||
if (entityMetadata is not GhostMetadata ghostMetadata)
|
||||
{
|
||||
Log.Error($"Trying to apply metadata to a ghost that is not of type {nameof(GhostMetadata)} : [{entityMetadata.GetType()}]");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (BuildUtils.IsUnderBaseDeconstructable(baseGhost, false) &&
|
||||
entityMetadata is BaseDeconstructableGhostMetadata deconstructableMetadata)
|
||||
{
|
||||
return deconstructableMetadata.ApplyBaseDeconstructableMetadataTo(baseGhost, @base);
|
||||
}
|
||||
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddWaterPark:
|
||||
case BaseAddPartitionDoorGhost:
|
||||
case BaseAddModuleGhost:
|
||||
case BaseAddFaceGhost:
|
||||
if (ghostMetadata is BaseAnchoredFaceGhostMetadata faceMetadata)
|
||||
{
|
||||
faceMetadata.ApplyBaseAnchoredFaceMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
case BaseAddPartitionGhost:
|
||||
if (ghostMetadata is BaseAnchoredCellGhostMetadata cellMetadata)
|
||||
{
|
||||
cellMetadata.ApplyBaseAnchoredCellMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ghostMetadata.ApplyBasicMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
Log.Error($"[{nameof(GhostMetadataApplier)}] Metadata of type {entityMetadata.GetType()} can't be applied to ghost of type {baseGhost.GetType()}");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void ApplyBaseAnchoredCellMetadataTo(this BaseAnchoredCellGhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
ApplyBasicMetadataTo(ghostMetadata, baseGhost);
|
||||
if (ghostMetadata.AnchoredCell.HasValue && baseGhost is BaseAddPartitionGhost ghost)
|
||||
{
|
||||
ghost.anchoredCell = ghostMetadata.AnchoredCell.Value.ToUnity();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyBaseAnchoredFaceMetadataTo(this BaseAnchoredFaceGhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
ApplyBasicMetadataTo(ghostMetadata, baseGhost);
|
||||
if (ghostMetadata.AnchoredFace.HasValue)
|
||||
{
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddWaterPark ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddPartitionDoorGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddModuleGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddFaceGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerator ApplyBaseDeconstructableMetadataTo(this BaseDeconstructableGhostMetadata ghostMetadata, BaseGhost baseGhost, Base @base)
|
||||
{
|
||||
ghostMetadata.ApplyBasicMetadataTo(baseGhost);
|
||||
baseGhost.DisableGhostModelScripts();
|
||||
if (ghostMetadata.ModuleFace.HasValue)
|
||||
{
|
||||
if (!baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true))
|
||||
{
|
||||
Log.Error($"Couldn't find an interior piece's parent ConstructableBase to apply a {nameof(BaseDeconstructableGhostMetadata)} to.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
constructableBase.moduleFace = ghostMetadata.ModuleFace.Value.ToUnity();
|
||||
|
||||
IPrefabRequest request = PrefabDatabase.GetPrefabAsync(ghostMetadata.ClassId);
|
||||
yield return request;
|
||||
if (!request.TryGetPrefab(out GameObject prefab))
|
||||
{
|
||||
// Without its module, the ghost will be useless, so we delete it (like in base game)
|
||||
Object.Destroy(constructableBase.gameObject);
|
||||
Log.Error($"Couldn't find a prefab for module of interior piece of ClassId: {ghostMetadata.ClassId}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!baseGhost.targetBase)
|
||||
{
|
||||
baseGhost.targetBase = @base;
|
||||
}
|
||||
|
||||
Base.Face face = ghostMetadata.ModuleFace.Value.ToUnity();
|
||||
face.cell += baseGhost.targetBase.GetAnchor();
|
||||
GameObject moduleObject = baseGhost.targetBase.SpawnModule(prefab, face);
|
||||
if (!moduleObject)
|
||||
{
|
||||
Object.Destroy(constructableBase.gameObject);
|
||||
Log.Error("Module couldn't be spawned for interior piece");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (moduleObject.TryGetComponent(out IBaseModule baseModule))
|
||||
{
|
||||
baseModule.constructed = constructableBase.constructedAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs
Normal file
82
NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
/// <summary>
|
||||
/// Because of the multiple possible types for Ghost components, the retrieving of their metadata is inappropriate for the MetadataExtractor system
|
||||
/// </summary>
|
||||
public static class GhostMetadataRetriever
|
||||
{
|
||||
public static GhostMetadata GetMetadataForGhost(BaseGhost baseGhost)
|
||||
{
|
||||
// Specific case in which a piece was deconstructed and resulted in a BaseDeconstructable with a normal BaseGhost
|
||||
if (BuildUtils.IsUnderBaseDeconstructable(baseGhost, true))
|
||||
{
|
||||
return GetBaseDeconstructableMetadata(baseGhost);
|
||||
}
|
||||
|
||||
GhostMetadata metadata = baseGhost switch
|
||||
{
|
||||
BaseAddWaterPark or BaseAddPartitionDoorGhost or BaseAddModuleGhost or BaseAddFaceGhost => baseGhost.GetBaseAnchoredFaceMetadata(),
|
||||
BaseAddPartitionGhost => baseGhost.GetBaseAnchoredCellMetadata(),
|
||||
_ => baseGhost.GetMetadata<GhostMetadata>(),
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static T GetMetadata<T>(this BaseGhost baseGhost) where T : GhostMetadata, new()
|
||||
{
|
||||
T metadata = new()
|
||||
{
|
||||
TargetOffset = baseGhost.targetOffset.ToDto()
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseDeconstructableGhostMetadata GetBaseDeconstructableMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseDeconstructableGhostMetadata metadata = baseGhost.GetMetadata<BaseDeconstructableGhostMetadata>();
|
||||
if (baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true) && constructableBase.moduleFace.HasValue)
|
||||
{
|
||||
Base.Face moduleFace = constructableBase.moduleFace.Value;
|
||||
metadata.ModuleFace = moduleFace.ToDto();
|
||||
moduleFace.cell += baseGhost.targetBase.GetAnchor();
|
||||
Component baseModule = baseGhost.targetBase.GetModule(moduleFace).AliveOrNull();
|
||||
if (baseModule && baseModule.TryGetComponent(out PrefabIdentifier identifier))
|
||||
{
|
||||
metadata.ClassId = identifier.ClassId;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseAnchoredFaceGhostMetadata GetBaseAnchoredFaceMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseAnchoredFaceGhostMetadata metadata = baseGhost.GetMetadata<BaseAnchoredFaceGhostMetadata>();
|
||||
metadata.AnchoredFace = baseGhost switch
|
||||
{
|
||||
BaseAddWaterPark ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddPartitionDoorGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddModuleGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddFaceGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseAnchoredCellGhostMetadata GetBaseAnchoredCellMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseAnchoredCellGhostMetadata metadata = baseGhost.GetMetadata<BaseAnchoredCellGhostMetadata>();
|
||||
if (baseGhost is BaseAddPartitionGhost ghost && ghost.anchoredCell.HasValue)
|
||||
{
|
||||
metadata.AnchoredCell = ghost.anchoredCell.Value.ToDto();
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
45
NitroxClient/GameLogic/Bases/OperationTracker.cs
Normal file
45
NitroxClient/GameLogic/Bases/OperationTracker.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public class OperationTracker
|
||||
{
|
||||
// TODO: Add the target's Id in here if future works requires it
|
||||
public int LastOperationId = -1;
|
||||
/// <summary>
|
||||
/// Accounts for locally-issued build actions
|
||||
/// </summary>
|
||||
public int LocalOperations;
|
||||
/// <summary>
|
||||
/// Calculated number of missed build actions
|
||||
/// </summary>
|
||||
public int MissedOperations;
|
||||
/// <summary>
|
||||
/// Number of detected issues when trying to apply actions remotely
|
||||
/// </summary>
|
||||
public int FailedOperations;
|
||||
|
||||
public void RegisterOperation(int newOperationId)
|
||||
{
|
||||
// If the progress was never registered, we don't need to account for missed operations
|
||||
if (LastOperationId != -1)
|
||||
{
|
||||
MissedOperations += Math.Max(newOperationId - (LastOperationId + LocalOperations) - 1, 0);
|
||||
}
|
||||
LastOperationId = newOperationId;
|
||||
LocalOperations = 0;
|
||||
}
|
||||
|
||||
public void ResetToId(int operationId = 0)
|
||||
{
|
||||
LastOperationId = operationId;
|
||||
LocalOperations = 0;
|
||||
MissedOperations = 0;
|
||||
FailedOperations = 0;
|
||||
}
|
||||
|
||||
public bool IsDesynced()
|
||||
{
|
||||
return MissedOperations + FailedOperations > 0;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user