diff --git a/Nitrox.Model.Subnautica/Packets/DeathMarkersChanged.cs b/Nitrox.Model.Subnautica/Packets/DeathMarkersChanged.cs new file mode 100644 index 0000000000..e74275feb6 --- /dev/null +++ b/Nitrox.Model.Subnautica/Packets/DeathMarkersChanged.cs @@ -0,0 +1,15 @@ +using System; +using Nitrox.Model.Packets; + +namespace NitroxModel.Packets; + +[Serializable] +public class DeathMarkersChanged : Packet +{ + public bool MarkDeathPointsWithBeacon { get; } + + public DeathMarkersChanged(bool markDeathPointsWithBeacon) + { + MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; + } +} diff --git a/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs b/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs index 38c2a7bf7c..390afaac12 100644 --- a/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs +++ b/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs @@ -41,6 +41,7 @@ public class InitialPlayerSync : Packet public SessionSettings SessionSettings { get; } public bool InPrecursor { get; } public bool DisplaySurfaceWater { get; } + public bool MarkDeathPointsWithBeacon { get; } public InitialPlayerSync(NitroxId playerGameObjectId, bool firstTimeConnecting, @@ -67,7 +68,8 @@ public InitialPlayerSync(NitroxId playerGameObjectId, bool keepInventoryOnDeath, SessionSettings sessionSettings, bool inPrecursor, - bool displaySurfaceWater) + bool displaySurfaceWater, + bool markDeathPointsWithBeacon) { AssignedEscapePodId = assignedEscapePodId; PlayerGameObjectId = playerGameObjectId; @@ -95,6 +97,7 @@ public InitialPlayerSync(NitroxId playerGameObjectId, SessionSettings = sessionSettings; InPrecursor = inPrecursor; DisplaySurfaceWater = displaySurfaceWater; + MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; } /// Used for deserialization @@ -124,7 +127,8 @@ public InitialPlayerSync( bool keepInventoryOnDeath, SessionSettings sessionSettings, bool inPrecursor, - bool displaySurfaceWater) + bool displaySurfaceWater, + bool markDeathPointsWithBeacon) { AssignedEscapePodId = assignedEscapePodId; PlayerGameObjectId = playerGameObjectId; @@ -152,6 +156,7 @@ public InitialPlayerSync( SessionSettings = sessionSettings; InPrecursor = inPrecursor; DisplaySurfaceWater = displaySurfaceWater; + MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; } } } diff --git a/Nitrox.Model/Serialization/SubnauticaServerConfig.cs b/Nitrox.Model/Serialization/SubnauticaServerConfig.cs index 21bbd63164..93f723ef01 100644 --- a/Nitrox.Model/Serialization/SubnauticaServerConfig.cs +++ b/Nitrox.Model/Serialization/SubnauticaServerConfig.cs @@ -30,6 +30,9 @@ public class SubnauticaServerConfig : NitroxConfig [PropertyDescription("Prevents players from losing items on death")] public bool KeepInventoryOnDeath { get; set; } = false; + [PropertyDescription("Places a beacon where players die")] + public bool MarkDeathPointsWithBeacon { get; set; } = true; + [PropertyDescription("Measured in milliseconds")] public int SaveInterval { diff --git a/Nitrox.Server.Subnautica/Models/Commands/SetDeathMarkersCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SetDeathMarkersCommand.cs new file mode 100644 index 0000000000..9fc3f333e7 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/SetDeathMarkersCommand.cs @@ -0,0 +1,42 @@ +using System.IO; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.Serialization; +using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.GameLogic; +using NitroxModel.Packets; + +namespace Nitrox.Server.Subnautica.Models.Commands; + +internal class SetDeathMarkersCommand : Command +{ + private readonly PlayerManager playerManager; + private readonly SubnauticaServerConfig serverConfig; + private readonly Server server; + + public SetDeathMarkersCommand(PlayerManager playerManager, SubnauticaServerConfig serverConfig, Server server) : base("deathmarkers", Perms.ADMIN, "Sets \"Death Markers\" setting to on/off. If \"on\", a beacon will appear at the location where a player dies.") + { + this.playerManager = playerManager; + this.serverConfig = serverConfig; + this.server = server; + AddParameter(new TypeBoolean("state", true, "on/off to enable/disable death markers")); + } + + protected override void Execute(CallArgs args) + { + bool newDeathMarkersState = args.Get(0); + using (serverConfig.Update(Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), server.Name))) + { + if (serverConfig.MarkDeathPointsWithBeacon != newDeathMarkersState) + { + serverConfig.MarkDeathPointsWithBeacon = newDeathMarkersState; + playerManager.SendPacketToAllPlayers(new DeathMarkersChanged(newDeathMarkersState)); + SendMessageToAllPlayers($"MarkDeathPointsWithBeacon changed to \"{newDeathMarkersState}\" by {args.SenderName}"); + } + else + { + SendMessage(args.Sender, $"MarkDeathPointsWithBeacon already set to {newDeathMarkersState}"); + } + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs index 7bf0fc7095..d9a1d9acd7 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs @@ -185,7 +185,8 @@ private void SendInitialSync(INitroxConnection connection, string reservationKey serverConfig.KeepInventoryOnDeath, sessionSettings, player.InPrecursor, - player.DisplaySurfaceWater + player.DisplaySurfaceWater, + serverConfig.MarkDeathPointsWithBeacon ); player.SendPacket(initialPlayerSync); diff --git a/NitroxClient/Communication/MultiplayerSession/ConnectionState/CommunicatingState.cs b/NitroxClient/Communication/MultiplayerSession/ConnectionState/CommunicatingState.cs index d07d22a42b..2557c9e6d0 100644 --- a/NitroxClient/Communication/MultiplayerSession/ConnectionState/CommunicatingState.cs +++ b/NitroxClient/Communication/MultiplayerSession/ConnectionState/CommunicatingState.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using NitroxClient.Communication.Abstract; namespace NitroxClient.Communication.MultiplayerSession.ConnectionState diff --git a/NitroxClient/Communication/Packets/Processors/DeathMarkersChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/DeathMarkersChangedProcessor.cs new file mode 100644 index 0000000000..e2645c9edb --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/DeathMarkersChangedProcessor.cs @@ -0,0 +1,20 @@ +using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.GameLogic; +using NitroxModel.Packets; + +namespace NitroxClient.Communication.Packets.Processors; + +public class DeathMarkersChangedProcessor : ClientPacketProcessor +{ + private readonly LocalPlayer localPlayer; + + public DeathMarkersChangedProcessor(LocalPlayer localPlayer) + { + this.localPlayer = localPlayer; + } + + public override void Process(DeathMarkersChanged packet) + { + localPlayer.MarkDeathPointsWithBeacon = packet.MarkDeathPointsWithBeacon; + } +} diff --git a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs index e03151017b..e4e3bfd3ca 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs @@ -1,4 +1,4 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.GameLogic; using Nitrox.Model.Helper; using Nitrox.Model.Packets; @@ -21,7 +21,6 @@ public override void Process(PlayerDeathEvent playerDeath) Log.Debug($"{player.PlayerName} died"); Log.InGame(Language.main.Get("Nitrox_PlayerDied").Replace("{PLAYER}", player.PlayerName)); player.PlayerDeathEvent.Trigger(player); - // TODO: Add any death related triggers (i.e. scoreboard updates, rewards, etc.) } } diff --git a/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs index 912f05b818..562de22b3e 100644 --- a/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs @@ -36,7 +36,7 @@ public PlayerInitialSyncProcessor(Items item, ItemContainers itemContainers, Loc AddStep(sync => AddStartingItemsToPlayer(sync.FirstTimeConnecting)); AddStep(sync => SetPlayerStats(sync.PlayerStatsData)); AddStep(sync => SetPlayerGameMode(sync.GameMode)); - AddStep(sync => ApplySettings(sync.KeepInventoryOnDeath, sync.SessionSettings.FastHatch, sync.SessionSettings.FastGrow)); + AddStep(sync => ApplySettings(sync.KeepInventoryOnDeath, sync.SessionSettings.FastHatch, sync.SessionSettings.FastGrow, sync.MarkDeathPointsWithBeacon)); } private void SetPlayerPermissions(Perms permissions) @@ -139,9 +139,10 @@ private static void SetPlayerGameMode(NitroxGameMode gameMode) GameModeUtils.SetGameMode((GameModeOption)(int)gameMode, GameModeOption.None); } - private void ApplySettings(bool keepInventoryOnDeath, bool fastHatch, bool fastGrow) + private void ApplySettings(bool keepInventoryOnDeath, bool fastHatch, bool fastGrow, bool markDeathPointsWithBeacon) { localPlayer.KeepInventoryOnDeath = keepInventoryOnDeath; + localPlayer.MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; NoCostConsoleCommand.main.fastHatchCheat = fastHatch; NoCostConsoleCommand.main.fastGrowCheat = fastGrow; if (!fastHatch && !fastGrow) @@ -160,4 +161,9 @@ private void ApplySettings(bool keepInventoryOnDeath, bool fastHatch, bool fastG } Log.InGame(cheatsEnabled.ToString()); } + + private void SetPlayerMarkDeathPointsWithBeacon(bool markDeathPointsWithBeacon) + { + localPlayer.MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; + } } diff --git a/NitroxClient/GameLogic/LocalPlayer.cs b/NitroxClient/GameLogic/LocalPlayer.cs index 327f52a0c1..9edb6e9e36 100644 --- a/NitroxClient/GameLogic/LocalPlayer.cs +++ b/NitroxClient/GameLogic/LocalPlayer.cs @@ -39,10 +39,10 @@ public class LocalPlayer : ILocalNitroxPlayer /// public ushort? PlayerId => multiplayerSession?.Reservation?.PlayerId; public PlayerSettings PlayerSettings => multiplayerSession.PlayerSettings; - public Perms Permissions { get; set; } public IntroCinematicMode IntroCinematicMode { get; set; } public bool KeepInventoryOnDeath { get; set; } + public bool MarkDeathPointsWithBeacon { get; set; } public LocalPlayer(IMultiplayerSession multiplayerSession, IPacketSender packetSender, ThrottledPacketSender throttledPacketSender) { @@ -55,6 +55,7 @@ public LocalPlayer(IMultiplayerSession multiplayerSession, IPacketSender packetS Permissions = Perms.PLAYER; IntroCinematicMode = IntroCinematicMode.NONE; KeepInventoryOnDeath = false; + MarkDeathPointsWithBeacon = false; } public void BroadcastLocation(Vector3 location, Vector3 velocity, Quaternion bodyRotation, Quaternion aimingRotation) diff --git a/NitroxClient/MonoBehaviours/Gui/InGame/DeathBeacon.cs b/NitroxClient/MonoBehaviours/Gui/InGame/DeathBeacon.cs new file mode 100644 index 0000000000..1d7b800683 --- /dev/null +++ b/NitroxClient/MonoBehaviours/Gui/InGame/DeathBeacon.cs @@ -0,0 +1,38 @@ +using Nitrox.Model.DataStructures.Unity; +using UnityEngine; + +namespace NitroxClient.MonoBehaviours.Gui.InGame; + +public class DeathBeacon : MonoBehaviour +{ + private const float DESPAWN_DISTANCE = 20f; + private const float DESPAWN_DISTANCE_SQUARED = DESPAWN_DISTANCE * DESPAWN_DISTANCE; + private const float CHECK_RATE = 1.5f; // in seconds + + public static void SpawnDeathBeacon(NitroxVector3 location, string playerName) + { + GameObject beacon = new($"{playerName}DeathBeacon"); + beacon.transform.position = location.ToUnity(); + PingInstance signal = beacon.AddComponent(); + signal.pingType = PingType.Signal; + signal.origin = beacon.transform; + signal.minDist = DESPAWN_DISTANCE + 15f; + signal._label = Language.main.Get("Nitrox_PlayerDeathBeaconLabel").Replace("{PLAYER}", playerName); + beacon.AddComponent(); + signal.displayPingInManager = true; + signal.Initialize(); + } + + private void Start() + { + InvokeRepeating(nameof(CheckPlayerDistance), 0, CHECK_RATE); + } + + private void CheckPlayerDistance() + { + if ((Player.main.transform.position - transform.position).sqrMagnitude <= DESPAWN_DISTANCE_SQUARED) + { + Destroy(gameObject); + } + } +} diff --git a/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs b/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs index 32ff578210..fb3cbe8af6 100644 --- a/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs +++ b/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs @@ -1,4 +1,5 @@ using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours.Gui.InGame; using UnityEngine; namespace NitroxClient.MonoBehaviours; @@ -16,6 +17,10 @@ public void Awake() private void PlayerDeath(Player player) { + if (localPlayer.MarkDeathPointsWithBeacon) + { + DeathBeacon.SpawnDeathBeacon(player.transform.position.ToDto(), localPlayer.PlayerName); + } localPlayer.BroadcastDeath(player.transform.position); } diff --git a/NitroxPatcher/Patches/Dynamic/Player_OnKill_Patch.cs b/NitroxPatcher/Patches/Dynamic/Player_OnKill_Patch.cs index 59edf93284..12dce05e3d 100644 --- a/NitroxPatcher/Patches/Dynamic/Player_OnKill_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Player_OnKill_Patch.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -11,6 +11,7 @@ public sealed partial class Player_OnKill_Patch : NitroxPatch, IDynamicPatch private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Player t) => t.OnKill(default(DamageType))); private static readonly MethodInfo SKIP_METHOD = Reflect.Method(() => GameModeUtils.IsPermadeath()); + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) { List instructionList = instructions.ToList();