411 lines
16 KiB
C#
411 lines
16 KiB
C#
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"));
|
|
}
|
|
}
|