Skip to content

Commit

Permalink
Add basic sync for PvP (#2107)
Browse files Browse the repository at this point in the history
  • Loading branch information
tornac1234 authored Jan 5, 2025
1 parent b4a2fda commit 7d2885f
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 6 deletions.
1 change: 1 addition & 0 deletions Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class PatchesTranspilerTest
[typeof(IngameMenu_QuitSubscreen_Patch), -24],
[typeof(Inventory_LoseItems_Patch), -2],
[typeof(ItemsContainer_DestroyItem_Patch), 2],
[typeof(Knife_OnToolUseAnim_Patch), 0],
[typeof(LargeWorldEntity_UpdateCell_Patch), 1],
[typeof(LaunchRocket_OnHandClick_Patch), -9],
[typeof(LeakingRadiation_Update_Patch), 0],
Expand Down
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)
{
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
}
}
38 changes: 38 additions & 0 deletions NitroxPatcher/Patches/Dynamic/Knife_OnToolUseAnim_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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);
*
* WITH:
* 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();
}
}
67 changes: 61 additions & 6 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,19 +29,71 @@ 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>())
if (!__runOriginal)
{
return;
}

// IsRemoteHealthChanging means we're replicating an action from the server and BaseCell is managed by BaseLeakManager
if (Resolve<LiveMixinManager>().IsRemoteHealthChanging || __instance.GetComponent<BaseCell>())
{
return;
}

// PvP damage is always 0 so we need to check for it before the regular case
if (HandlePvP(__instance, dealer, originalDamage))
{
return;
}

// At this point, if the victim didn't take damage, there's no point in broadcasting it
if (__state != __instance.health)
{
return;
}

BroadcastDefaultTookDamage(__instance);
}

private static bool HandlePvP(LiveMixin liveMixin, GameObject dealer, float damage)
{
if (!liveMixin.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier))
{
return false;
}

// Dealer must be the local player, and we need to know about the item they're holding
if (dealer != Player.mainObject || !Inventory.main.GetHeldObject())
{
return false;
}

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 false;
}

Resolve<IPacketSender>().Send(new PvPAttack(remotePlayerIdentifier.RemotePlayer.PlayerId, damage, attackType));
return true;
}

private static void BroadcastDefaultTookDamage(LiveMixin liveMixin)
{
// Let others know if we have a lock on this entity
if (__instance.TryGetIdOrWarn(out NitroxId id) && Resolve<SimulationOwnership>().HasAnyLockType(id))
if (liveMixin.TryGetIdOrWarn(out NitroxId id) && Resolve<SimulationOwnership>().HasAnyLockType(id))
{
Optional<EntityMetadata> metadata = Resolve<EntityMetadataManager>().Extract(__instance.gameObject);
Optional<EntityMetadata> metadata = Resolve<EntityMetadataManager>().Extract(liveMixin.gameObject);

if (metadata.HasValue)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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)
{
return;
}
if (!playerManager.TryGetPlayerById(packet.TargetPlayerId, out Player targetPlayer))
{
return;
}
if (!damageMultiplierByType.TryGetValue(packet.Type, out float multiplier))
{
return;
}

packet.Damage *= multiplier;
targetPlayer.SendPacket(packet);
}
}
52 changes: 52 additions & 0 deletions NitroxServer/ConsoleCommands/PvpCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.IO;
using NitroxModel.DataStructures.GameLogic;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
using NitroxServer.Serialization;
using NitroxServer.Serialization.World;

namespace NitroxServer.ConsoleCommands;

public class PvpCommand : Command
{
private readonly ServerConfig serverConfig;

public PvpCommand(ServerConfig serverConfig) : base("pvp", Perms.ADMIN, "Enables/Disables PvP")
{
AddParameter(new TypeString("state", true, "on/off"));

this.serverConfig = serverConfig;
}

protected override void Execute(CallArgs args)
{
string state = args.Get<string>(0).ToLower();

bool pvpEnabled = false;
switch (state)
{
case "on":
pvpEnabled = true;
break;
case "off":
break;
default:
SendMessage(args.Sender, "Parameter must be \"on\" or \"off\"");
return;
}


using (serverConfig.Update(Path.Combine(WorldManager.SavesFolderDir, serverConfig.SaveName)))
{
if (serverConfig.PvPEnabled == pvpEnabled)
{
SendMessage(args.Sender, $"PvP is already {state}");
return;
}

serverConfig.PvPEnabled = pvpEnabled;

SendMessageToAllPlayers($"PvP is now {state}");
}
}
}
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;
}
}

0 comments on commit 7d2885f

Please sign in to comment.