diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/CyclopsMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/CyclopsMetadataExtractor.cs index 0f8d62493a..b4f18cdc66 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/CyclopsMetadataExtractor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/CyclopsMetadataExtractor.cs @@ -28,7 +28,10 @@ public override CyclopsMetadata Extract(CyclopsGameObject cyclops) LiveMixin liveMixin = gameObject.RequireComponentInChildren(); float health = liveMixin.health; - return new(silentRunning.active, shieldOn, sonarOn, engineOn, (int)motorMode, health); + SubRoot subRoot = gameObject.RequireComponentInChildren(); + bool isDestroyed = subRoot.subDestroyed || health <= 0f; + + return new(silentRunning.active, shieldOn, sonarOn, engineOn, (int)motorMode, health, isDestroyed); } public struct CyclopsGameObject diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs index eb75f98a99..c79954095e 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs @@ -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); } } @@ -149,4 +150,25 @@ private void SetHealth(GameObject gameObject, float health) LiveMixin liveMixin = gameObject.RequireComponentInChildren(true); liveMixinManager.SyncRemoteHealth(liveMixin, health); } + + private void SetDestroyed(GameObject gameObject, bool isDestroyed) + { + CyclopsDestructionEvent destructionEvent = gameObject.RequireComponentInChildren(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.Suppress()) + { + destructionEvent.DestroyCyclops(); + } + } + else + { + destructionEvent.RestoreCyclops(); + } + } } diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleWorldEntitySpawner.cs index d548c99a51..a483d503d5 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleWorldEntitySpawner.cs @@ -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; @@ -99,6 +100,14 @@ private IEnumerator SpawnInWorld(VehicleWorldEntity vehicleEntity, TaskResult 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) @@ -54,8 +62,20 @@ public void BroadcastDestroyedVehicle(NitroxId id) { using (PacketSuppressor.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 metadata = entityMetadataManager.Extract(cyclopsGameObject); + + if (metadata.HasValue && metadata.Value is CyclopsMetadata cyclopsMetadata) + { + cyclopsMetadata.IsDestroyed = true; + entities.BroadcastMetadataUpdate(id, cyclopsMetadata); } } diff --git a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/CyclopsMetadata.cs b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/CyclopsMetadata.cs index 8ad9a67e52..1e6466c732 100644 --- a/NitroxModel/DataStructures/GameLogic/Entities/Metadata/CyclopsMetadata.cs +++ b/NitroxModel/DataStructures/GameLogic/Entities/Metadata/CyclopsMetadata.cs @@ -26,13 +26,16 @@ 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; @@ -40,10 +43,11 @@ public CyclopsMetadata(bool silentRunningOn, bool shieldOn, bool sonarOn, bool e 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}]"; } } diff --git a/NitroxModel/Packets/VehicleDestroyed.cs b/NitroxModel/Packets/VehicleDestroyed.cs deleted file mode 100644 index 3a8f64b8e5..0000000000 --- a/NitroxModel/Packets/VehicleDestroyed.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using NitroxModel.DataStructures; - -namespace NitroxModel.Packets; - -[Serializable] -public class VehicleDestroyed : Packet -{ - public NitroxId Id { get; } - - public VehicleDestroyed(NitroxId id) - { - Id = id; - } -} diff --git a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_DestroyCyclops_Patch.cs b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_DestroyCyclops_Patch.cs index 35c6bb9d46..8d321e670f 100644 --- a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_DestroyCyclops_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_DestroyCyclops_Patch.cs @@ -8,7 +8,7 @@ namespace NitroxPatcher.Patches.Dynamic; /// -/// Broadcasts the cyclops destruction by calling , and safely remove every player from it. +/// Broadcasts the cyclops destruction, and safely removes every player from it. /// public sealed partial class CyclopsDestructionEvent_DestroyCyclops_Patch : NitroxPatch, IDynamicPatch { @@ -38,4 +38,12 @@ public static void Prefix(CyclopsDestructionEvent __instance) __instance.subLiveMixin.Kill(); } + + public static void Postfix(CyclopsDestructionEvent __instance) + { + if (__instance.TryGetNitroxId(out NitroxId nitroxId)) + { + Resolve().BroadcastDestroyedCyclops(__instance.gameObject, nitroxId); + } + } } diff --git a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_OnConsoleCommand_Patch.cs b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_OnConsoleCommand_Patch.cs index 80ca4dc681..8097fce342 100644 --- a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_OnConsoleCommand_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_OnConsoleCommand_Patch.cs @@ -1,8 +1,6 @@ using System; using System.Reflection; using HarmonyLib; -using NitroxClient.GameLogic; -using NitroxModel.DataStructures; using NitroxModel.Helper; using static NitroxModel.Helper.Reflect; @@ -15,6 +13,7 @@ public sealed class CyclopsDestructionEvent_OnConsoleCommand_Patch : NitroxPatch public static bool PrefixRestore() { + // TODO: add support for "restorecyclops" command Log.InGame(Language.main.Get("Nitrox_CommandNotAvailable")); return false; } @@ -26,24 +25,11 @@ public static bool PrefixDestroy(CyclopsDestructionEvent __instance, out bool __ return __state; } - /// - /// This must happen at postfix so that the SubRootChanged packet are sent before the destroyed vehicle one, - /// thus saving player entities from deletion. - /// - public static void PostfixDestroy(CyclopsDestructionEvent __instance, bool __state) - { - if (__state && __instance.TryGetIdOrWarn(out NitroxId id)) - { - Resolve().BroadcastDestroyedVehicle(id); - } - } - public override void Patch(Harmony harmony) { MethodInfo destroyPrefixInfo = Method(() => PrefixDestroy(default, out Ref.Field)); PatchPrefix(harmony, TARGET_METHOD_RESTORE, ((Func)PrefixRestore).Method); PatchPrefix(harmony, TARGET_METHOD_DESTROY, destroyPrefixInfo); - PatchPostfix(harmony, TARGET_METHOD_DESTROY, ((Action)PostfixDestroy).Method); } } diff --git a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs new file mode 100644 index 0000000000..d4a1fd35c8 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs @@ -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) + { + { Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), "SpawnObject" } + }; + + public static IEnumerable Transpiler(MethodBase original, IEnumerable 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)SpawnObjectCallback).Method) + ]); + } + + public static void SpawnObjectCallback(GameObject gameObject, CyclopsDestructionEvent __instance) + { + NitroxId lootId = NitroxEntity.GenerateNewId(gameObject); + + LargeWorldEntity largeWorldEntity = gameObject.GetComponent(); + PrefabIdentifier prefabIdentifier = gameObject.GetComponent(); + Pickupable pickupable = gameObject.GetComponent(); + + WorldEntity lootEntity = new(gameObject.transform.ToWorldDto(), (int)largeWorldEntity.cellLevel, prefabIdentifier.classId, false, lootId, pickupable.GetTechType().ToDto(), null, null, []); + Resolve().BroadcastEntitySpawnedByClient(lootEntity); + } + + public static int PrefixCallback(int originalIndex) + { + // Immediately return from iterator block if called from within CyclopsMetadataProcessor + return PacketSuppressor.IsSuppressed ? int.MaxValue : originalIndex; + } +} diff --git a/NitroxPatcher/Patches/Dynamic/SubRoot_OnTakeDamage_Patch.cs b/NitroxPatcher/Patches/Dynamic/SubRoot_OnTakeDamage_Patch.cs index 6d50e163bb..90e08fe06a 100644 --- a/NitroxPatcher/Patches/Dynamic/SubRoot_OnTakeDamage_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/SubRoot_OnTakeDamage_Patch.cs @@ -30,15 +30,4 @@ public static bool Prefix(SubRoot __instance, DamageInfo damageInfo) return Resolve().HasAnyLockType(id); } - - 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 (__runOriginal && __instance.live.health <= 0f && - damageInfo.type != EntityDestroyedProcessor.DAMAGE_TYPE_RUN_ORIGINAL && - __instance.TryGetIdOrWarn(out NitroxId id)) - { - Resolve().BroadcastDestroyedVehicle(id); - } - } } diff --git a/NitroxPatcher/Patches/Dynamic/SubRoot_SetCyclopsUpgrades_Patch.cs b/NitroxPatcher/Patches/Dynamic/SubRoot_SetCyclopsUpgrades_Patch.cs new file mode 100644 index 0000000000..dd9896e906 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/SubRoot_SetCyclopsUpgrades_Patch.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using NitroxModel.Helper; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// 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. +/// +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; + } + } + } +} diff --git a/NitroxServer/Communication/Packets/Processors/EntityDestroyedPacketProcessor.cs b/NitroxServer/Communication/Packets/Processors/EntityDestroyedPacketProcessor.cs index 26f8f71178..682bbf91e4 100644 --- a/NitroxServer/Communication/Packets/Processors/EntityDestroyedPacketProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/EntityDestroyedPacketProcessor.cs @@ -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; @@ -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; diff --git a/NitroxServer/Communication/Packets/Processors/VehicleDestroyedPacketProcessor.cs b/NitroxServer/Communication/Packets/Processors/VehicleDestroyedPacketProcessor.cs deleted file mode 100644 index 4562db50e2..0000000000 --- a/NitroxServer/Communication/Packets/Processors/VehicleDestroyedPacketProcessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using NitroxModel.DataStructures.GameLogic; -using NitroxModel.DataStructures.GameLogic.Entities; -using NitroxModel.Packets; -using NitroxServer.Communication.Packets.Processors.Abstract; -using NitroxServer.GameLogic; -using NitroxServer.GameLogic.Entities; - -namespace NitroxServer.Communication.Packets.Processors; - -public class VehicleDestroyedPacketProcessor : AuthenticatedPacketProcessor -{ - private readonly PlayerManager playerManager; - private readonly EntitySimulation entitySimulation; - private readonly WorldEntityManager worldEntityManager; - - public VehicleDestroyedPacketProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.entitySimulation = entitySimulation; - } - - public override void Process(VehicleDestroyed packet, Player destroyingPlayer) - { - entitySimulation.EntityDestroyed(packet.Id); - - 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; - if (isOtherPlayer && player.CanSee(entity)) - { - player.SendPacket(packet); - } - } - } - } -} diff --git a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs index 7bcab59c4d..98147a6d64 100644 --- a/NitroxServer/GameLogic/Entities/WorldEntityManager.cs +++ b/NitroxServer/GameLogic/Entities/WorldEntityManager.cs @@ -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; @@ -67,6 +68,21 @@ public List GetGlobalRootEntities() where T : GlobalRootEntity } } + public List 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 GetEntities(AbsoluteEntityCell cell) { lock (worldEntitiesLock) diff --git a/NitroxServer/Serialization/World/PersistedWorldData.cs b/NitroxServer/Serialization/World/PersistedWorldData.cs index a7b0462322..72137f71f4 100644 --- a/NitroxServer/Serialization/World/PersistedWorldData.cs +++ b/NitroxServer/Serialization/World/PersistedWorldData.cs @@ -31,7 +31,7 @@ public static PersistedWorldData From(World world) Seed = world.Seed }, PlayerData = PlayerData.From(world.PlayerManager.GetAllPlayers()), - GlobalRootData = GlobalRootData.From(world.WorldEntityManager.GetGlobalRootEntities(true)), + GlobalRootData = GlobalRootData.From(world.WorldEntityManager.GetPersistentGlobalRootEntities()), EntityData = EntityData.From(world.EntityRegistry.GetAllEntities(true)) }; }