diff --git a/Nitrox.Test/Patcher/Patches/Dynamic/Knife_OnToolUseAnim_PatchTest.cs b/Nitrox.Test/Patcher/Patches/Dynamic/Knife_OnToolUseAnim_PatchTest.cs new file mode 100644 index 0000000000..445aaa95f9 --- /dev/null +++ b/Nitrox.Test/Patcher/Patches/Dynamic/Knife_OnToolUseAnim_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 Knife_OnToolUseAnim_PatchTest +{ + [TestMethod] + public void Sanity() + { + IEnumerable originalIl = PatchTestHelper.GetInstructionsFromMethod(Knife_OnToolUseAnim_Patch.TARGET_METHOD); + IEnumerable transformedIl = Knife_OnToolUseAnim_Patch.Transpiler(originalIl); + transformedIl.Count().Should().Be(originalIl.Count()); + } +} diff --git a/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs b/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs new file mode 100644 index 0000000000..5112d193f5 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs @@ -0,0 +1,15 @@ +using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxModel.Packets; + +namespace NitroxClient.Communication.Packets.Processors; + +public class PvPAttackProcessor : ClientPacketProcessor +{ + public override void Process(PvPAttack packet) + { + if (Player.main && Player.main.liveMixin) + { + Player.main.liveMixin.TakeDamage(packet.Damage); + } + } +} diff --git a/NitroxModel/Packets/PvPAttack.cs b/NitroxModel/Packets/PvPAttack.cs new file mode 100644 index 0000000000..9318249d17 --- /dev/null +++ b/NitroxModel/Packets/PvPAttack.cs @@ -0,0 +1,24 @@ +using System; + +namespace NitroxModel.Packets; + +[Serializable] +public class PvPAttack : Packet +{ + public string TargetPlayerName { get; } + public float Damage { get; set; } + public AttackType Type { get; } + + public PvPAttack(string targetPlayerName, float damage, AttackType type) + { + TargetPlayerName = targetPlayerName; + Damage = damage; + Type = type; + } + + public enum AttackType + { + KnifeHit, + HeatbladeHit + } +} diff --git a/NitroxPatcher/Patches/Dynamic/Knife_OnToolUseAnim_Patch.cs b/NitroxPatcher/Patches/Dynamic/Knife_OnToolUseAnim_Patch.cs new file mode 100644 index 0000000000..b5800f5199 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/Knife_OnToolUseAnim_Patch.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxModel.Helper; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Registers knife hits's dealer as the main Player object +/// +public sealed partial class Knife_OnToolUseAnim_Patch : NitroxPatch, IDynamicPatch +{ + public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Knife t) => t.OnToolUseAnim(default)); + + /* + * + * bool flag = liveMixin.IsAlive(); + * REPLACE below line + * liveMixin.TakeDamage(this.damage, vector, this.damageType, null); + * BY: + * liveMixin.TakeDamage(this.damage, vector, this.damageType, Player.mainObject); + * this.GiveResourceOnDamage(gameObject, liveMixin.IsAlive(), flag); + */ + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions).MatchEndForward([ + new CodeMatch(OpCodes.Ldloc_0), + new CodeMatch(OpCodes.Ldarg_0), + new CodeMatch(OpCodes.Ldfld), + new CodeMatch(OpCodes.Ldnull) + ]) + .Set(OpCodes.Ldsfld, Reflect.Field(() => Player.mainObject)) + .InstructionEnumeration(); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/LiveMixin_TakeDamage_Patch.cs b/NitroxPatcher/Patches/Dynamic/LiveMixin_TakeDamage_Patch.cs index 0b384a633a..4dd15ada64 100644 --- a/NitroxPatcher/Patches/Dynamic/LiveMixin_TakeDamage_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/LiveMixin_TakeDamage_Patch.cs @@ -1,10 +1,13 @@ using System.Reflection; +using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; +using NitroxClient.GameLogic.PlayerLogic; using NitroxClient.GameLogic.Spawning.Metadata; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic.Entities.Metadata; using NitroxModel.DataStructures.Util; using NitroxModel.Helper; +using NitroxModel.Packets; using UnityEngine; namespace NitroxPatcher.Patches.Dynamic; @@ -13,9 +16,10 @@ public sealed partial class LiveMixin_TakeDamage_Patch : NitroxPatch, IDynamicPa { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((LiveMixin t) => t.TakeDamage(default(float), default(Vector3), default(DamageType), default(GameObject))); - public static bool Prefix(out float? __state, LiveMixin __instance, GameObject dealer) + public static bool Prefix(out float __state, LiveMixin __instance, GameObject dealer) { - __state = null; + // Persist the previous health value + __state = __instance.health; if (!Resolve().IsWhitelistedUpdateType(__instance)) { @@ -28,10 +32,10 @@ public static bool Prefix(out float? __state, LiveMixin __instance, GameObject d return Resolve().ShouldApplyNextHealthUpdate(__instance, dealer); } - public static void Postfix(float? __state, LiveMixin __instance, float originalDamage, Vector3 position, DamageType type, GameObject dealer) + public static void Postfix(float __state, LiveMixin __instance, float originalDamage, GameObject dealer, bool __runOriginal) { - // Did we realize a change in health? - if (!__state.HasValue || __state.Value == __instance.health) + bool healthChanged = __state != __instance.health; + if (!__runOriginal || !ShouldBroadcastDamage(__instance, dealer, originalDamage, healthChanged)) { return; } @@ -47,4 +51,40 @@ public static void Postfix(float? __state, LiveMixin __instance, float originalD } } } + + private static bool ShouldBroadcastDamage(LiveMixin victim, GameObject dealer, float damage, bool healthChanged) + { + foreach (MonoBehaviour monoBehaviour in victim.GetComponents()) + { + switch (monoBehaviour) + { + case RemotePlayerIdentifier remotePlayerIdentifier: + // Handle it internally + HandlePvP(remotePlayerIdentifier.RemotePlayer, dealer, damage); + return false; + } + } + return healthChanged; + } + + private static void HandlePvP(RemotePlayer remotePlayer, GameObject dealer, float damage) + { + if (dealer == Player.mainObject && Inventory.main.GetHeldObject()) + { + PvPAttack.AttackType attackType; + switch (Inventory.main.GetHeldTool()) + { + case HeatBlade: + attackType = PvPAttack.AttackType.HeatbladeHit; + break; + case Knife: + attackType = PvPAttack.AttackType.KnifeHit; + break; + default: + // We don't want to send non-registered attacks + return; + } + Resolve().Send(new PvPAttack(remotePlayer.PlayerName, damage, attackType)); + } + } } diff --git a/NitroxServer/Communication/Packets/Processors/PvPAttackProcessor.cs b/NitroxServer/Communication/Packets/Processors/PvPAttackProcessor.cs new file mode 100644 index 0000000000..57e33d8df6 --- /dev/null +++ b/NitroxServer/Communication/Packets/Processors/PvPAttackProcessor.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using NitroxModel.Packets; +using NitroxServer.Communication.Packets.Processors.Abstract; +using NitroxServer.GameLogic; +using NitroxServer.Serialization; + +namespace NitroxServer.Communication.Packets.Processors; + +public class PvPAttackProcessor : AuthenticatedPacketProcessor +{ + private readonly ServerConfig serverConfig; + private readonly PlayerManager playerManager; + + // TODO: In the future, do a whole config for damage sources + private static readonly Dictionary DamageMultiplierByType = new() + { + { PvPAttack.AttackType.KnifeHit, 0.5f }, + { PvPAttack.AttackType.HeatbladeHit, 1f } + }; + + public PvPAttackProcessor(ServerConfig serverConfig, PlayerManager playerManager) + { + this.serverConfig = serverConfig; + this.playerManager = playerManager; + } + + public override void Process(PvPAttack packet, Player player) + { + if (serverConfig.PvPEnabled && + playerManager.TryGetPlayerByName(packet.TargetPlayerName, out Player targetPlayer) && + DamageMultiplierByType.TryGetValue(packet.Type, out float multiplier)) + { + packet.Damage *= multiplier; + targetPlayer.SendPacket(packet); + } + } +} diff --git a/NitroxServer/Serialization/ServerConfig.cs b/NitroxServer/Serialization/ServerConfig.cs index cab3e60f65..3ebdf7d0ca 100644 --- a/NitroxServer/Serialization/ServerConfig.cs +++ b/NitroxServer/Serialization/ServerConfig.cs @@ -117,5 +117,8 @@ public string SaveName [PropertyDescription("When true, will reject any build actions detected as desynced")] public bool SafeBuilding { get; set; } = true; + + [PropertyDescription("Activates/Deactivates Player versus Player damage/interactions")] + public bool PvPEnabled { get; set; } = true; } }