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

Add basic sync for PvP #2107

Merged
merged 4 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -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<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(Knife_OnToolUseAnim_Patch.TARGET_METHOD);
IEnumerable<CodeInstruction> transformedIl = Knife_OnToolUseAnim_Patch.Transpiler(originalIl);
transformedIl.Count().Should().Be(originalIl.Count());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;

namespace NitroxClient.Communication.Packets.Processors;

public class PvPAttackProcessor : ClientPacketProcessor<PvPAttack>
{
public override void Process(PvPAttack packet)
{
if (Player.main && Player.main.liveMixin)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would log not finding this as an error bc the damage is disappearing without the player sending a health update back to the server.

Copy link
Collaborator Author

@tornac1234 tornac1234 Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If local player isn't instantiated, or its live mixin isn't found, then it can only mean that the game is in an early loading state/in an game closing unloading state in which case it's not necessary to log the failure

{
Player.main.liveMixin.TakeDamage(packet.Damage);
}
}
}
24 changes: 24 additions & 0 deletions NitroxModel/Packets/PvPAttack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace NitroxModel.Packets;

[Serializable]
public class PvPAttack : Packet
{
public ushort TargetPlayerId { get; }
public float Damage { get; set; }
public AttackType Type { get; }

public PvPAttack(ushort targetPlayerId, float damage, AttackType type)
{
TargetPlayerId = targetPlayerId;
Damage = damage;
Type = type;
}

public enum AttackType : byte
{
KnifeHit,
HeatbladeHit
}
}
36 changes: 36 additions & 0 deletions NitroxPatcher/Patches/Dynamic/Knife_OnToolUseAnim_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxModel.Helper;

namespace NitroxPatcher.Patches.Dynamic;

/// <summary>
/// Registers knife hits's dealer as the main Player object
/// </summary>
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<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> 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();
}
}
49 changes: 45 additions & 4 deletions NitroxPatcher/Patches/Dynamic/LiveMixin_TakeDamage_Patch.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,11 +29,10 @@ public static bool Prefix(out float __state, LiveMixin __instance, GameObject de
return Resolve<LiveMixinManager>().ShouldApplyNextHealthUpdate(__instance, dealer);
}

public static void Postfix(float __state, LiveMixin __instance, bool __runOriginal)
public static void Postfix(float __state, LiveMixin __instance, float originalDamage, GameObject dealer, bool __runOriginal)
{
// Did we realize a change in health?
if (!__runOriginal || __state == __instance.health || Resolve<LiveMixinManager>().IsRemoteHealthChanging ||
__instance.GetComponent<BaseCell>())
bool healthChanged = __state != __instance.health;
if (!__runOriginal || !ShouldBroadcastDamage(__instance, dealer, originalDamage, healthChanged))
{
return;
}
Expand All @@ -46,4 +48,43 @@ public static void Postfix(float __state, LiveMixin __instance, bool __runOrigin
}
}
}

private static bool ShouldBroadcastDamage(LiveMixin victim, GameObject dealer, float damage, bool healthChanged)
{
if (Resolve<LiveMixinManager>().IsRemoteHealthChanging || victim.GetComponent<BaseCell>())
{
return false;
}

if (victim.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier))
{
// Handle it internally
HandlePvP(remotePlayerIdentifier.RemotePlayer, dealer, damage);
return false;
}

// The health change check must happen after the PvP one
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<IPacketSender>().Send(new PvPAttack(remotePlayer.PlayerId, damage, attackType));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PvPAttack>
{
private readonly ServerConfig serverConfig;
private readonly PlayerManager playerManager;

// TODO: In the future, do a whole config for damage sources
private static readonly Dictionary<PvPAttack.AttackType, float> 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.TryGetPlayerById(packet.TargetPlayerId, out Player targetPlayer) &&
damageMultiplierByType.TryGetValue(packet.Type, out float multiplier))
{
packet.Damage *= multiplier;
targetPlayer.SendPacket(packet);
}
}
}
9 changes: 9 additions & 0 deletions NitroxServer/GameLogic/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace NitroxServer.GameLogic
public class PlayerManager
{
private readonly ThreadSafeDictionary<string, Player> allPlayersByName;
private readonly ThreadSafeDictionary<ushort, Player> connectedPlayersById = [];
private readonly ThreadSafeDictionary<INitroxConnection, ConnectionAssets> assetsByConnection = new();
private readonly ThreadSafeDictionary<string, PlayerContext> reservations = new();
private readonly ThreadSafeSet<string> reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user
Expand Down Expand Up @@ -215,6 +216,8 @@ public Player PlayerConnected(INitroxConnection connection, string reservationKe
allPlayersByName[playerContext.PlayerName] = player;
}

connectedPlayersById.Add(playerContext.PlayerId, player);

// TODO: make a ConnectedPlayer wrapper so this is not stateful
player.PlayerContext = playerContext;
player.Connection = connection;
Expand Down Expand Up @@ -248,6 +251,7 @@ public void PlayerDisconnected(INitroxConnection connection)
{
Player player = assetPackage.Player;
reservedPlayerNames.Remove(player.Name);
connectedPlayersById.Remove(player.Id);
}

assetsByConnection.Remove(connection);
Expand Down Expand Up @@ -301,6 +305,11 @@ public bool TryGetPlayerByName(string playerName, out Player foundPlayer)
return false;
}

public bool TryGetPlayerById(ushort playerId, out Player player)
{
return connectedPlayersById.TryGetValue(playerId, out player);
}

public Player GetPlayer(INitroxConnection connection)
{
if (!assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage))
Expand Down
3 changes: 3 additions & 0 deletions NitroxServer/Serialization/ServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}