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();