Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve vehicle destruction sync #2197

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ public override CyclopsMetadata Extract(CyclopsGameObject cyclops)
LiveMixin liveMixin = gameObject.RequireComponentInChildren<LiveMixin>();
float health = liveMixin.health;

return new(silentRunning.active, shieldOn, sonarOn, engineOn, (int)motorMode, health);
SubRoot subRoot = gameObject.RequireComponentInChildren<SubRoot>();
bool isDestroyed = subRoot.subDestroyed || health <= 0f;

return new(silentRunning.active, shieldOn, sonarOn, engineOn, (int)motorMode, health, isDestroyed);
}

public struct CyclopsGameObject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public override void ProcessMetadata(GameObject cyclops, CyclopsMetadata metadat
ChangeSonarMode(cyclops, metadata.SonarOn);
SetEngineState(cyclops, metadata.EngineOn);
SetHealth(cyclops, metadata.Health);
SetDestroyed(cyclops, metadata.IsDestroyed);
}
}

Expand Down Expand Up @@ -149,4 +150,25 @@ private void SetHealth(GameObject gameObject, float health)
LiveMixin liveMixin = gameObject.RequireComponentInChildren<LiveMixin>(true);
liveMixinManager.SyncRemoteHealth(liveMixin, health);
}

private void SetDestroyed(GameObject gameObject, bool isDestroyed)
{
CyclopsDestructionEvent destructionEvent = gameObject.RequireComponentInChildren<CyclopsDestructionEvent>(true);

// Don't play VFX and SFX if the Cyclops is already destroyed or was spawned in as destroyed
if (destructionEvent.subRoot.subDestroyed == isDestroyed) return;

if (isDestroyed)
{
// Use packet suppressor as sentinel so the patch callback knows not to spawn loot
using (PacketSuppressor<EntitySpawnedByClient>.Suppress())
{
destructionEvent.DestroyCyclops();
}
}
else
{
destructionEvent.RestoreCyclops();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using NitroxClient.MonoBehaviours.CinematicController;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
Expand Down Expand Up @@ -99,6 +100,14 @@ private IEnumerator SpawnInWorld(VehicleWorldEntity vehicleEntity, TaskResult<Op

NitroxEntity.SetNewId(gameObject, vehicleEntity.Id);

if (vehicleEntity.Metadata is CyclopsMetadata cyclopsMetadata && cyclopsMetadata.IsDestroyed)
{
// Swap to destroyed look without triggering animations / effects
gameObject.BroadcastMessage("SwapToDamagedModels");
gameObject.BroadcastMessage("OnKill");
gameObject.BroadcastMessage("CyclopsDeathEvent", SendMessageOptions.DontRequireReceiver);
}

if (parent.HasValue)
{
DockVehicle(gameObject, parent.Value);
Expand Down
26 changes: 23 additions & 3 deletions NitroxClient/GameLogic/Vehicles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
using NitroxClient.Communication;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Helper;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
SpaceMonkeyy86 marked this conversation as resolved.
Show resolved Hide resolved
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
Expand All @@ -18,14 +22,18 @@ public class Vehicles
private readonly IPacketSender packetSender;
private readonly IMultiplayerSession multiplayerSession;
private readonly PlayerManager playerManager;
private readonly EntityMetadataManager entityMetadataManager;
private readonly Entities entities;

private readonly Dictionary<TechType, string> pilotingChairByTechType = [];

public Vehicles(IPacketSender packetSender, IMultiplayerSession multiplayerSession, PlayerManager playerManager)
public Vehicles(IPacketSender packetSender, IMultiplayerSession multiplayerSession, PlayerManager playerManager, EntityMetadataManager entityMetadataManager, Entities entities)
{
this.packetSender = packetSender;
this.multiplayerSession = multiplayerSession;
this.playerManager = playerManager;
this.entityMetadataManager = entityMetadataManager;
this.entities = entities;
}

private PilotingChair FindPilotingChairWithCache(GameObject parent, TechType techType)
Expand Down Expand Up @@ -54,8 +62,20 @@ public void BroadcastDestroyedVehicle(NitroxId id)
{
using (PacketSuppressor<VehicleOnPilotModeChanged>.Suppress())
{
VehicleDestroyed vehicleDestroyed = new(id);
packetSender.Send(vehicleDestroyed);
EntityDestroyed entityDestroyed = new(id);
packetSender.Send(entityDestroyed);
}
}

public void BroadcastDestroyedCyclops(GameObject cyclops, NitroxId id)
{
CyclopsMetadataExtractor.CyclopsGameObject cyclopsGameObject = new() { GameObject = cyclops };
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(cyclopsGameObject);

if (metadata.HasValue && metadata.Value is CyclopsMetadata cyclopsMetadata)
{
cyclopsMetadata.IsDestroyed = true;
entities.BroadcastMetadataUpdate(id, cyclopsMetadata);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,28 @@ public class CyclopsMetadata : EntityMetadata
[DataMember(Order = 6)]
public float Health { get; set; }

[DataMember(Order = 7)]
public bool IsDestroyed { get; set; }

[IgnoreConstructor]
protected CyclopsMetadata()
{
// Constructor for serialization. Has to be "protected" for json serialization.
}

public CyclopsMetadata(bool silentRunningOn, bool shieldOn, bool sonarOn, bool engineOn, int engineMode, float health)
public CyclopsMetadata(bool silentRunningOn, bool shieldOn, bool sonarOn, bool engineOn, int engineMode, float health, bool isDestroyed)
{
SilentRunningOn = silentRunningOn;
ShieldOn = shieldOn;
SonarOn = sonarOn;
EngineOn = engineOn;
EngineMode = engineMode;
Health = health;
IsDestroyed = isDestroyed;
}

public override string ToString()
{
return $"[CyclopsMetadata SilentRunningOn: {SilentRunningOn}, ShieldOn: {ShieldOn}, SonarOn: {SonarOn}, EngineOn: {EngineOn}, EngineMode: {EngineMode}, Health: {Health}]";
return $"[CyclopsMetadata SilentRunningOn: {SilentRunningOn}, ShieldOn: {ShieldOn}, SonarOn: {SonarOn}, EngineOn: {EngineOn}, EngineMode: {EngineMode}, Health: {Health}, IsDestroyed: {IsDestroyed}]";
}
}
15 changes: 0 additions & 15 deletions NitroxModel/Packets/VehicleDestroyed.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,11 @@ public static bool PrefixDestroy(CyclopsDestructionEvent __instance, out bool __
return __state;
}

/// <remarks>
/// This must happen at postfix so that the SubRootChanged packet are sent before the destroyed vehicle one,
/// thus saving player entities from deletion.
/// </remarks>
public static void PostfixDestroy(CyclopsDestructionEvent __instance, bool __state)
{
if (__state && __instance.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Vehicles>().BroadcastDestroyedVehicle(id);
Resolve<Vehicles>().BroadcastDestroyedCyclops(__instance.gameObject, id);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using NitroxPatcher.PatternMatching;
using UnityEngine;

namespace NitroxPatcher.Patches.Dynamic;

public sealed partial class CyclopsDestructionEvent_SpawnLootAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((CyclopsDestructionEvent t) => t.SpawnLootAsync()));

// Matches twice, once for scrap metal and once for computer chips
public static readonly InstructionsPattern PATTERN = new(expectedMatches: 2)
SpaceMonkeyy86 marked this conversation as resolved.
Show resolved Hide resolved
{
{ Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), "SpawnObject" }
};

public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(OpCodes.Switch))
.InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => PrefixCallback(default))))
.InstructionEnumeration()
.InsertAfterMarker(PATTERN, "SpawnObject", [
new(OpCodes.Dup),
new(OpCodes.Ldloc_1),
new(OpCodes.Call, ((Action<GameObject, CyclopsDestructionEvent>)SpawnObjectCallback).Method)
]);
}

public static void SpawnObjectCallback(GameObject gameObject, CyclopsDestructionEvent __instance)
{
NitroxId lootId = NitroxEntity.GenerateNewId(gameObject);

LargeWorldEntity largeWorldEntity = gameObject.GetComponent<LargeWorldEntity>();
PrefabIdentifier prefabIdentifier = gameObject.GetComponent<PrefabIdentifier>();
Pickupable pickupable = gameObject.GetComponent<Pickupable>();

WorldEntity lootEntity = new(gameObject.transform.ToWorldDto(), (int)largeWorldEntity.cellLevel, prefabIdentifier.classId, false, lootId, pickupable.GetTechType().ToDto(), null, null, []);
Resolve<Entities>().BroadcastEntitySpawnedByClient(lootEntity);
}

public static int PrefixCallback(int originalIndex)
{
// Immediately return from iterator block if called from within CyclopsMetadataProcessor
return PacketSuppressor<EntitySpawnedByClient>.IsSuppressed ? int.MaxValue : originalIndex;
}
}
4 changes: 2 additions & 2 deletions NitroxPatcher/Patches/Dynamic/SubRoot_OnTakeDamage_Patch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public static bool Prefix(SubRoot __instance, DamageInfo damageInfo)

public static void Postfix(bool __runOriginal, SubRoot __instance, DamageInfo damageInfo)
{
// If we have lock on it, we'll notify the server that this cyclops must be destroyed
// If we have lock on it, we'll notify the server that this cyclops took damage
if (__runOriginal && __instance.live.health <= 0f &&
damageInfo.type != EntityDestroyedProcessor.DAMAGE_TYPE_RUN_ORIGINAL &&
__instance.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Vehicles>().BroadcastDestroyedVehicle(id);
Resolve<Vehicles>().BroadcastDestroyedCyclops(__instance.gameObject, id);
}
}
}
33 changes: 33 additions & 0 deletions NitroxPatcher/Patches/Dynamic/SubRoot_SetCyclopsUpgrades_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Reflection;
using NitroxModel.Helper;

namespace NitroxPatcher.Patches.Dynamic;

/// <summary>
/// The way Subnautica handles modules in Cyclops wrecks is pretty weird. if any module is added/removed (and when spawning
/// the entity during joining), they are all instantly disabled, which deletes creature decoys in any slot except
/// the first one. We want to keep the creature decoys around if the module is inserted, so we allow use of that
/// module even when the Cyclops has been destroyed.
/// </summary>
public sealed partial class SubRoot_SetCyclopsUpgrades_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SubRoot t) => t.SetCyclopsUpgrades());

public static void Postfix(SubRoot __instance)
{
if (__instance.upgradeConsole == null) return;

__instance.decoyTubeSizeIncreaseUpgrade = false;

Equipment modules = __instance.upgradeConsole.modules;
foreach (string slot in SubRoot.slotNames)
{
TechType techTypeInSlot = modules.GetTechTypeInSlot(slot);
if (techTypeInSlot == TechType.CyclopsDecoyModule)
{
__instance.decoyTubeSizeIncreaseUpgrade = true;
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
Expand Down Expand Up @@ -26,6 +27,11 @@ public override void Process(EntityDestroyed packet, Player destroyingPlayer)

if (worldEntityManager.TryDestroyEntity(packet.Id, out Entity entity))
{
if (entity is VehicleWorldEntity vehicleWorldEntity)
{
worldEntityManager.MovePlayerChildrenToRoot(vehicleWorldEntity);
}

foreach (Player player in playerManager.GetConnectedPlayers())
{
bool isOtherPlayer = player != destroyingPlayer;
Expand Down

This file was deleted.

16 changes: 16 additions & 0 deletions NitroxServer/GameLogic/Entities/WorldEntityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Unity;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
Expand Down Expand Up @@ -67,6 +68,21 @@ public List<T> GetGlobalRootEntities<T>() where T : GlobalRootEntity
}
}

public List<GlobalRootEntity> GetPersistentGlobalRootEntities()
{
// TODO: refactor if there are more entities that should not be persisted
return GetGlobalRootEntities(true).Where(entity =>
{
if (entity.Metadata is CyclopsMetadata cyclopsMetadata)
{
// Do not save cyclops wrecks
return !cyclopsMetadata.IsDestroyed;
}

return true;
}).ToList();
}

public List<WorldEntity> GetEntities(AbsoluteEntityCell cell)
{
lock (worldEntitiesLock)
Expand Down
Loading