diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/Vehicle_TorpedoShot_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/Vehicle_TorpedoShot_PatchTest.cs new file mode 100644 index 0000000000..abedf74690 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/Vehicle_TorpedoShot_PatchTest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using HarmonyLib; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NitroxTest.Patcher; + +namespace NitroxPatcher.Patches.Dynamic; + +[TestClass] +public class Vehicle_TorpedoShot_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(Vehicle_TorpedoShot_Patch.TARGET_METHOD); + IEnumerable transformedIl = Vehicle_TorpedoShot_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count() + 3); + } +} diff --git a/NitroxClient/GameLogic/AI.cs b/NitroxClient/GameLogic/AI.cs index d934272ff7..df6e259115 100644 --- a/NitroxClient/GameLogic/AI.cs +++ b/NitroxClient/GameLogic/AI.cs @@ -25,6 +25,15 @@ public class AI typeof(AttackLastTarget), typeof(AttackCyclops) ]; + /// + /// In the future, ensure all creatures are synced. We want each of them to be individually + /// checked (that all their actions are synced) before marking them as synced. + /// + private readonly HashSet syncedCreatureWhitelist = + [ + typeof(ReaperLeviathan), typeof(SeaDragon) + ]; + public AI(IPacketSender packetSender) { this.packetSender = packetSender; @@ -36,7 +45,7 @@ public AI(IPacketSender packetSender) public void BroadcastNewAction(NitroxId creatureId, Creature creature, CreatureAction newAction) { - if (creature is not ReaperLeviathan) + if (!syncedCreatureWhitelist.Contains(creature.GetType())) { return; } @@ -119,4 +128,9 @@ public bool IsCreatureActionWhitelisted(CreatureAction creatureAction) { return creatureActionWhitelist.Contains(creatureAction.GetType()); } + + public bool IsCreatureWhitelisted(Creature creature) + { + return syncedCreatureWhitelist.Contains(creature.GetType()); + } } diff --git a/NitroxClient/GameLogic/BulletManager.cs b/NitroxClient/GameLogic/BulletManager.cs index 01659b6df2..34f58d7ae6 100644 --- a/NitroxClient/GameLogic/BulletManager.cs +++ b/NitroxClient/GameLogic/BulletManager.cs @@ -8,6 +8,10 @@ namespace NitroxClient.GameLogic; +/// +/// Registers one stasis sphere per connected remote player, and syncs their behaviour.
+/// Also syncs remote torpedo (of all types) shots and hits. +///
public class BulletManager { private readonly PlayerManager playerManager; @@ -77,10 +81,7 @@ public void TorpedoTargetAcquired(NitroxId bulletId, NitroxId targetId, Vector3 public void ShootStasisSphere(ushort playerId, Vector3 position, Quaternion rotation, float speed, float lifeTime, float chargeNormalized) { - if (!stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere cloneSphere) || !cloneSphere) - { - cloneSphere = EnsurePlayerHasSphere(playerId); - } + StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId); cloneSphere.Shoot(position, rotation, speed, lifeTime, chargeNormalized); } @@ -89,7 +90,7 @@ public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotati { StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId); - // Setup the sphere in case it the shot was sent earlier + // Setup the sphere in case the shot was sent earlier cloneSphere.Shoot(position, rotation, 0, 0, chargeNormalized); // We override this field (set by .Shoot) with the right data cloneSphere._consumption = consumption; @@ -97,7 +98,7 @@ public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotati // Code from Bullet.Update when finding an object to hit cloneSphere._visible = true; cloneSphere.OnMadeVisible(); - cloneSphere.OnHit(default); + cloneSphere.EnableField(); cloneSphere.Deactivate(); } diff --git a/NitroxClient/GameLogic/RemotePlayer.cs b/NitroxClient/GameLogic/RemotePlayer.cs index b3d66d5840..c9e8798cf0 100644 --- a/NitroxClient/GameLogic/RemotePlayer.cs +++ b/NitroxClient/GameLogic/RemotePlayer.cs @@ -421,7 +421,7 @@ static void CopyEmitter(FMOD_CustomEmitter src, FMOD_CustomEmitter dst) private void SetupMixins() { InfectedMixin = Body.AddComponent(); - InfectedMixin.shaderKeyWord = "UWE_PLAYERINFECTION"; + InfectedMixin.shaderKeyWord = InfectedMixin.uwe_playerinfection; Renderer renderer = PlayerModel.transform.Find("male_geo/diveSuit/diveSuit_hands_geo").GetComponent(); InfectedMixin.renderers = [renderer]; @@ -432,7 +432,7 @@ private void SetupMixins() broadcastKillOnDeath = false }; LiveMixin.health = 100; - // We set the remote player to invincible because we only want this component to detectable but not to work + // We set the remote player to invincible because we only want this component to be detectable but not to work LiveMixin.invincible = true; } diff --git a/NitroxPatcher/Patches/Dynamic/AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch.cs b/NitroxPatcher/Patches/Dynamic/AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch.cs index d3141f82eb..12e472acc9 100644 --- a/NitroxPatcher/Patches/Dynamic/AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch.cs @@ -23,7 +23,6 @@ public static bool Prefix(AggressiveWhenSeeTarget __instance) return true; } - return false; } @@ -47,6 +46,11 @@ public static IEnumerable Transpiler(IEnumerable().IsCreatureWhitelisted(aggressiveWhenSeeTarget.creature)) + { + return; + } + // If the function was called to this point, either it'll return because it doesn't have an id or it'll be evident that we have ownership over the aggressive creature LastTarget lastTarget = aggressiveWhenSeeTarget.lastTarget; // If there's already (likely another) locked target, we get its id over aggressionTarget diff --git a/NitroxPatcher/Patches/Dynamic/AttackCyclops_OnCollisionEnter_Patch.cs b/NitroxPatcher/Patches/Dynamic/AttackCyclops_OnCollisionEnter_Patch.cs index 0c348abe2c..cd03feb8ba 100644 --- a/NitroxPatcher/Patches/Dynamic/AttackCyclops_OnCollisionEnter_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/AttackCyclops_OnCollisionEnter_Patch.cs @@ -17,6 +17,12 @@ public sealed partial class AttackCyclops_OnCollisionEnter_Patch : NitroxPatch, { public static readonly MethodInfo TARGET_METHOD = Reflect.Method((AttackCyclops t) => t.OnCollisionEnter(default)); + public static bool Prefix(AttackCyclops __instance) + { + return !__instance.TryGetNitroxId(out NitroxId creatureId) || + Resolve().HasAnyLockType(creatureId); + } + /* * REPLACE: * if (Player.main != null && Player.main.currentSub != null && Player.main.currentSub.isCyclops && Player.main.currentSub.gameObject == collision.gameObject) @@ -41,10 +47,4 @@ public static bool ShouldCollisionAnnoyCreature(Collision collision) { return AttackCyclops_UpdateAggression_Patch.IsTargetAValidInhabitedCyclops(collision.gameObject); } - - public static bool Prefix(AttackCyclops __instance) - { - return !__instance.TryGetNitroxId(out NitroxId creatureId) || - Resolve().HasAnyLockType(creatureId); - } } diff --git a/NitroxPatcher/Patches/Dynamic/AttackCyclops_UpdateAggression_Patch.cs b/NitroxPatcher/Patches/Dynamic/AttackCyclops_UpdateAggression_Patch.cs index d396283b7c..81ebde5da8 100644 --- a/NitroxPatcher/Patches/Dynamic/AttackCyclops_UpdateAggression_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/AttackCyclops_UpdateAggression_Patch.cs @@ -88,13 +88,13 @@ public static bool IsTargetAValidInhabitedCyclops(IEcoTarget target) public static bool IsTargetAValidInhabitedCyclops(GameObject targetObject) { // Is a Cyclops - if (!targetObject.GetComponent()) + if (!targetObject.TryGetComponent(out SubRoot subRoot) || !subRoot.isCyclops) { return false; } // Has the local player inside - if (Player.main && Player.main.currentSub && Player.main.currentSub.gameObject == targetObject) + if (Player.main && Player.main.currentSub == subRoot) { return true; } diff --git a/NitroxPatcher/Patches/Dynamic/Creature_ChooseBestAction_Patch.cs b/NitroxPatcher/Patches/Dynamic/Creature_ChooseBestAction_Patch.cs index 69c6a88f78..8c131b0e03 100644 --- a/NitroxPatcher/Patches/Dynamic/Creature_ChooseBestAction_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Creature_ChooseBestAction_Patch.cs @@ -14,7 +14,6 @@ public static bool Prefix(Creature __instance, out NitroxId __state, ref Creatur { if (!__instance.TryGetIdOrWarn(out __state, true)) { - Log.WarnOnce($"[{nameof(Creature_ChooseBestAction_Patch)}] Couldn't find an id on {__instance.GetFullHierarchyPath()}"); return true; } if (Resolve().HasAnyLockType(__state)) diff --git a/NitroxPatcher/Patches/Dynamic/SeamothTorpedoWhirlpool_Register_Patch.cs b/NitroxPatcher/Patches/Dynamic/SeamothTorpedoWhirlpool_Register_Patch.cs index 692fab1ab8..d235fb920d 100644 --- a/NitroxPatcher/Patches/Dynamic/SeamothTorpedoWhirlpool_Register_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/SeamothTorpedoWhirlpool_Register_Patch.cs @@ -12,7 +12,7 @@ public sealed partial class SeamothTorpedoWhirlpool_Register_Patch : NitroxPatch { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SeamothTorpedoWhirlpool t) => t.Register(default, ref Reflect.Ref.Field)); - public static bool Prefix(Collider other, ref bool __result) + public static bool Prefix(Collider other) { return !other.GetComponentInParent(true); } diff --git a/NitroxPatcher/Patches/Dynamic/StasisSphere_OnHit_Patch.cs b/NitroxPatcher/Patches/Dynamic/StasisSphere_OnHit_Patch.cs index 967e73d86b..ec541907f9 100644 --- a/NitroxPatcher/Patches/Dynamic/StasisSphere_OnHit_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/StasisSphere_OnHit_Patch.cs @@ -9,27 +9,27 @@ namespace NitroxPatcher.Patches.Dynamic; /// -/// Prevents remote stasis sphere from triggering hit effets (they're triggered with packets) +/// Prevents remote stasis sphere from triggering hit effets (they're triggered with packets). +/// Broadcasts local player's stasis sphere hits /// public sealed partial class StasisSphere_OnHit_Patch : NitroxPatch, IDynamicPatch { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((StasisSphere t) => t.OnHit(default)); - private static ushort LocalPlayerId => Resolve().PlayerId; - - public static void Prefix(StasisSphere __instance) + public static bool Prefix(StasisSphere __instance) { if (__instance.GetComponent()) { - return; + return false; } + ushort localPlayerId = Resolve().PlayerId; NitroxVector3 position = __instance.tr.position.ToDto(); NitroxQuaternion rotation = __instance.tr.rotation.ToDto(); // Calculate the chargeNormalized value which was passed to StasisSphere.Shoot float chargeNormalized = Mathf.Unlerp(__instance.minRadius, __instance.maxRadius, __instance.radius); - Resolve().Send(new StasisSphereHit(LocalPlayerId, position, rotation, chargeNormalized, __instance._consumption)); - return; + Resolve().Send(new StasisSphereHit(localPlayerId, position, rotation, chargeNormalized, __instance._consumption)); + return true; } } diff --git a/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs b/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs index f70f2f85fa..d161abdf26 100644 --- a/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs @@ -1,7 +1,11 @@ +using System.Collections.Generic; using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures; +using NitroxModel.DataStructures.Unity; using NitroxModel.Helper; using NitroxModel.Packets; using NitroxModel_Subnautica.DataStructures; @@ -11,31 +15,53 @@ namespace NitroxPatcher.Patches.Dynamic; public sealed partial class Vehicle_TorpedoShot_Patch : NitroxPatch, IDynamicPatch { - private static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => Vehicle.TorpedoShot(default, default, default)); + internal static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => Vehicle.TorpedoShot(default, default, default)); - public static bool Prefix(Vehicle __instance, ref bool __result, ItemsContainer container, TorpedoType torpedoType, Transform muzzle) + /* + * Inserts a DUP instruction after the SeamothTorpedo object is pushed onto the stack so we can use it later on + * in the inserted callback. + * NB: Ldarg_0 refers to the 1st parameter (ItemsContainer) and Ldarg_1 refers to the 2nd parameter (TorpedoType) as this method is static + * + * MODIFICATION: + * component.Shoot(muzzle.position, aimingTransform.rotation, num, -1f); + * Vehicle_TorpedoShot_Patch.TorpedoShotCallback(dupped object, torpedoType); <--- INSERTED LINE + * return true; + * + * "dupped object" is the SeamothTorpedo object from the line: + * gameObject.GetComponent(); + */ + public static IEnumerable Transpiler(IEnumerable instructions) { - // (almost) Exact code copy from Vehicle.TorpedoShot because it's impossible to make a transpiler for it without modifying most of the instructions - // (the transpiler wouldn't be readable at all) so the best possibility is to copy the exact and out the created torpedo GameObject - if (torpedoType == null || !container.DestroyItem(torpedoType.techType)) - { - return false; - } - GameObject gameObject = GameObject.Instantiate(torpedoType.prefab); - Bullet component = gameObject.GetComponent(); - Transform aimingTransform = Player.main.camRoot.GetAimingTransform(); - Rigidbody componentInParent = muzzle.GetComponentInParent(); - Vector3 vector = componentInParent ? componentInParent.velocity : Vector3.zero; - float num = Vector3.Dot(aimingTransform.forward, vector); - component.Shoot(muzzle.position, aimingTransform.rotation, num, -1f); - __result = true; + // Always match for one more instruction after the searched one because the cursor will be moved right before it + return new CodeMatcher(instructions).MatchEndForward([ + new CodeMatch(OpCodes.Pop), + new CodeMatch(OpCodes.Callvirt, Reflect.Method((GameObject t) => t.GetComponent())), + new CodeMatch(OpCodes.Call), + ]) + .InsertAndAdvance([ + new CodeInstruction(OpCodes.Dup), + ]) + .MatchEndForward([ + new CodeMatch(OpCodes.Callvirt, Reflect.Method((Bullet t) => t.Shoot(default, default, default, default))), + new CodeMatch(OpCodes.Ldc_I4_1), + ]) + .InsertAndAdvance([ + new CodeInstruction(OpCodes.Ldarg_1), + new CodeInstruction(OpCodes.Call, Reflect.Method(() => TorpedoShotCallback(default, default))) + ]) + .InstructionEnumeration(); + } + + public static void TorpedoShotCallback(SeamothTorpedo seamothTorpedo, TorpedoType torpedoType) + { + NitroxId bulletId = NitroxEntity.GenerateNewId(seamothTorpedo.gameObject); - // Broadcast code + NitroxVector3 position = seamothTorpedo.transform.position.ToDto(); + NitroxQuaternion rotation = seamothTorpedo.transform.rotation.ToDto(); - NitroxId bulletId = new(); - NitroxEntity.SetNewId(gameObject, bulletId); + // In Bullet.Shoot, _consumption = f(lifeTime), lifeTime = g(_consumption), this is g: + float lifeTime = seamothTorpedo._consumption > 0 ? 1f / seamothTorpedo._consumption : 0f; - Resolve().Send(new TorpedoShot(bulletId, torpedoType.techType.ToDto(), muzzle.position.ToDto(), aimingTransform.rotation.ToDto(), num, -1)); - return false; + Resolve().Send(new TorpedoShot(bulletId, torpedoType.techType.ToDto(), position, rotation, seamothTorpedo.speed, lifeTime)); } }