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 ea239e3626..5d2f454c3a 100644 --- a/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs +++ b/Nitrox.Model.Subnautica/Packets/InitialPlayerSync.cs @@ -42,6 +42,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, @@ -68,7 +69,8 @@ public InitialPlayerSync(NitroxId playerGameObjectId, bool keepInventoryOnDeath, SessionSettings sessionSettings, bool inPrecursor, - bool displaySurfaceWater) + bool displaySurfaceWater, + bool markDeathPointsWithBeacon) { AssignedEscapePodId = assignedEscapePodId; PlayerGameObjectId = playerGameObjectId; @@ -96,6 +98,7 @@ public InitialPlayerSync(NitroxId playerGameObjectId, SessionSettings = sessionSettings; InPrecursor = inPrecursor; DisplaySurfaceWater = displaySurfaceWater; + MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; } /// Used for deserialization @@ -125,7 +128,8 @@ public InitialPlayerSync( bool keepInventoryOnDeath, SessionSettings sessionSettings, bool inPrecursor, - bool displaySurfaceWater) + bool displaySurfaceWater, + bool markDeathPointsWithBeacon) { AssignedEscapePodId = assignedEscapePodId; PlayerGameObjectId = playerGameObjectId; @@ -153,6 +157,7 @@ public InitialPlayerSync( SessionSettings = sessionSettings; InPrecursor = inPrecursor; DisplaySurfaceWater = displaySurfaceWater; + MarkDeathPointsWithBeacon = markDeathPointsWithBeacon; } } } diff --git a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs index 8051c2e963..25cac951b1 100644 --- a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs +++ b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs @@ -70,6 +70,9 @@ public sealed partial class SubnauticaServerOptions public bool DisableConsole { get; set; } public string? AdminPassword { get; set; } = ""; public bool KeepInventoryOnDeath { get; set; } + + [PropertyDescription("Places a beacon where players die")] + public bool MarkDeathPointsWithBeacon { get; set; } public bool PvpEnabled { get; set; } = true; public bool AutoSave { get; set; } = true; diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs index feca5be245..05449b7356 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs @@ -82,4 +82,6 @@ public async ValueTask SendAsync(SessionId sessionId, T data) break; } } + + public override string ToString() => $"'{OriginName}' #{OriginId}"; } diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs index bc0143e7b5..a83b578283 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs @@ -77,4 +77,6 @@ public async ValueTask SendToOthersAsync(T data) break; } } + + public override string ToString() => $"'{OriginName}' #{OriginId}"; } diff --git a/Nitrox.Server.Subnautica/Models/Commands/DeathMarkerCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/DeathMarkerCommand.cs new file mode 100644 index 0000000000..4d12071825 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/DeathMarkerCommand.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using NitroxModel.Packets; + +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class DeathMarkerCommand(IOptions options) : ICommandHandler +{ + private readonly IOptions options = options; + + [Description("Sets \"death markers\" setting to on/off. If \"on\", a beacon will be placed when a player dies at the location of the death.")] + public async Task Execute(ICommandContext context, [Description("The true/false state to set the death markers setting to")] bool newState) + { + if (options.Value.MarkDeathPointsWithBeacon == newState) + { + await context.ReplyAsync($"{nameof(options.Value.MarkDeathPointsWithBeacon)} already set to {newState}"); + return; + } + options.Value.MarkDeathPointsWithBeacon = newState; + await context.SendToAllAsync(new DeathMarkersChanged(newState)); + await context.SendToAllAsync($"{nameof(options.Value.MarkDeathPointsWithBeacon)} changed to \"{newState}\" by {context}"); + } +} diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs index 2173d6f497..5387493a1d 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs @@ -206,7 +206,8 @@ private async Task SendInitialSyncAsync(SessionId sessionId, string reservationK options.Value.KeepInventoryOnDeath, sessionSettings, player.InPrecursor, - player.DisplaySurfaceWater + player.DisplaySurfaceWater, + options.Value.MarkDeathPointsWithBeacon ); await packetSender.SendPacketAsync(initialPlayerSync, player.SessionId); 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..a297700f01 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/DeathMarkersChangedProcessor.cs @@ -0,0 +1,16 @@ +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; +using NitroxModel.Packets; + +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class DeathMarkersChangedProcessor(LocalPlayer localPlayer) : IClientPacketProcessor +{ + private readonly LocalPlayer localPlayer = localPlayer; + + public Task Process(ClientProcessorContext context, DeathMarkersChanged packet) + { + localPlayer.MarkDeathPointsWithBeacon = packet.MarkDeathPointsWithBeacon; + return Task.CompletedTask; + } +} diff --git a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs index dc41599701..c4dc86dbf0 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs @@ -1,4 +1,4 @@ -using Nitrox.Model.Helper; +using Nitrox.Model.Helper; using Nitrox.Model.Subnautica.Packets; using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; diff --git a/NitroxClient/Extensions/GameObjectExtensions.cs b/NitroxClient/Extensions/GameObjectExtensions.cs index 3bf48ea712..6f7baced46 100644 --- a/NitroxClient/Extensions/GameObjectExtensions.cs +++ b/NitroxClient/Extensions/GameObjectExtensions.cs @@ -10,10 +10,13 @@ namespace NitroxClient.Extensions; public static class GameObjectExtensions { - /// - /// Returns true if game object is the local player, playing on the executing machine. - /// - public static bool IsLocalPlayer(this GameObject gameObject) => gameObject == Player.main.gameObject; + extension(GameObject self) + { + /// + /// Returns true if game object is the local player, playing on the executing machine. + /// + public bool IsLocalPlayer => self == Player.main.gameObject; + } public static bool TryGetComponentInChildren(this GameObject go, out T component, bool includeInactive = false) where T : Component { diff --git a/NitroxClient/Extensions/PingInstanceExtensions.cs b/NitroxClient/Extensions/PingInstanceExtensions.cs new file mode 100644 index 0000000000..72c4bbcff5 --- /dev/null +++ b/NitroxClient/Extensions/PingInstanceExtensions.cs @@ -0,0 +1,16 @@ +namespace NitroxClient.Extensions; + +internal static class PingInstanceExtensions +{ + extension(PingInstance self) + { + /// + /// If true, ping instance should not be synchronized to remote players. + /// + public bool IsLocalOnly + { + set => self._id = "local"; + get => self._id == "local"; + } + } +} diff --git a/NitroxClient/GameLogic/Helper/EquipmentHelper.cs b/NitroxClient/GameLogic/Helper/EquipmentHelper.cs index 6e44cf2426..befc47168b 100644 --- a/NitroxClient/GameLogic/Helper/EquipmentHelper.cs +++ b/NitroxClient/GameLogic/Helper/EquipmentHelper.cs @@ -17,7 +17,7 @@ public class EquipmentHelper o => o.GetComponent().AliveOrNull()?.modules, o => o.GetComponent().AliveOrNull()?.modules, o => o.GetComponent().AliveOrNull()?.equipment, - o => o.IsLocalPlayer() ? Inventory.main.equipment : null + o => o.IsLocalPlayer ? Inventory.main.equipment : null }; public static Optional FindEquipmentComponent(GameObject owner) diff --git a/NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs b/NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs index 4eef6e4ce1..24a7087457 100644 --- a/NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs +++ b/NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs @@ -17,7 +17,7 @@ public class InventoryContainerHelper /// public static Optional TryGetContainerByOwner(GameObject owner) { - if (owner.IsLocalPlayer()) + if (owner.IsLocalPlayer) { return Optional.Of(Inventory.main.container); } diff --git a/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs index 1f0cd8b60b..1dd7538e6c 100644 --- a/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs @@ -37,7 +37,7 @@ public PlayerInitialSyncProcessor(Items item, ItemContainers itemContainers, Loc AddStep(sync => SetPlayerStats(sync.PlayerStatsData)); AddStep(sync => SetUsedItems(sync.UsedItems)); 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) @@ -149,9 +149,10 @@ private static void SetPlayerGameMode(SubnauticaGameMode 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) @@ -170,4 +171,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/InitialSync/PlayerPreferencesInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs index aa676c97d2..f2dfb7356f 100644 --- a/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs @@ -100,6 +100,11 @@ private static void RefreshPingEntryInPDA(PingInstance pingInstance) public static bool TryGetKeyForPingInstance(PingInstance pingInstance, out string pingKey, out bool isRemotePlayerPing, Action failCallback = null) { isRemotePlayerPing = false; + if (pingInstance.IsLocalOnly) + { + pingKey = string.Empty; + return false; + } if (pingInstance.TryGetComponent(out SignalPing signalPing)) { pingKey = signalPing.descriptionKey; @@ -129,7 +134,7 @@ public static bool TryGetKeyForPingInstance(PingInstance pingInstance, out strin return false; } - Log.Warn($"Couldn't find PingInstance identifier for {pingInstance.name} under {pingInstance.transform.parent}"); + Log.Warn($"Couldn't find {nameof(PingInstance)} identifier for {pingInstance.name} under {pingInstance.transform.parent}"); pingKey = string.Empty; return false; } diff --git a/NitroxClient/GameLogic/LocalPlayer.cs b/NitroxClient/GameLogic/LocalPlayer.cs index caf235f376..65c925df10 100644 --- a/NitroxClient/GameLogic/LocalPlayer.cs +++ b/NitroxClient/GameLogic/LocalPlayer.cs @@ -37,10 +37,10 @@ public class LocalPlayer : ILocalNitroxPlayer /// public SessionId? SessionId => multiplayerSession.Reservation?.SessionId; 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) { @@ -53,6 +53,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..e51152f0f9 --- /dev/null +++ b/NitroxClient/MonoBehaviours/Gui/InGame/DeathBeacon.cs @@ -0,0 +1,42 @@ +using Nitrox.Model.DataStructures.Unity; +using UnityEngine; + +namespace NitroxClient.MonoBehaviours.Gui.InGame; + +/// +/// Related to DeathMarker server setting. +/// +internal sealed class DeathBeacon : MonoBehaviour +{ + private const float DESPAWN_DISTANCE = 20f; + + private void OnTriggerEnter(Collider collider) + { + if (!collider.gameObject.IsLocalPlayer) + { + return; + } + + Log.Debug($"{nameof(DeathBeacon)} '{name}' despawn trigger entered by {collider.name}"); + Destroy(gameObject); + } + + public static void SpawnDeathBeacon(NitroxVector3 location, string playerName) + { + GameObject beacon = new($"{playerName}{nameof(DeathBeacon)}"); + beacon.transform.position = location.ToUnity(); + beacon.layer = LayerID.Trigger | LayerID.OnlyVehicle; + beacon.AddComponent(); + PingInstance signal = beacon.AddComponent(); + signal.IsLocalOnly = true; + signal.pingType = PingType.Signal; + signal.origin = beacon.transform; + signal.minDist = DESPAWN_DISTANCE + 15f; + signal._label = Language.main.Get("Nitrox_PlayerDeathBeaconLabel").Replace("{PLAYER}", playerName); + signal.displayPingInManager = true; + signal.Initialize(); + SphereCollider collider = beacon.AddComponent(); + collider.radius = DESPAWN_DISTANCE; + collider.isTrigger = true; + } +} diff --git a/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs b/NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs index 32ff578210..ed49a7676d 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; @@ -11,16 +12,20 @@ public void Awake() { localPlayer = this.Resolve(); - Player.main.playerDeathEvent.AddHandler(this, PlayerDeath); + Player.main.playerDeathEvent.AddHandler(this, OnPlayerDeath); } - private void PlayerDeath(Player player) + private void OnPlayerDeath(Player player) { + if (localPlayer.MarkDeathPointsWithBeacon) + { + DeathBeacon.SpawnDeathBeacon(player.transform.position.ToDto(), localPlayer.PlayerName); + } localPlayer.BroadcastDeath(player.transform.position); } public void OnDestroy() { - Player.main.playerDeathEvent.RemoveHandler(this, PlayerDeath); + Player.main.playerDeathEvent.RemoveHandler(this, OnPlayerDeath); } } 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();