From 2cbf209a1c02cb5ac36c460b9db4d2897b6f5709 Mon Sep 17 00:00:00 2001 From: MistaOmega Date: Sun, 25 Jan 2026 00:34:09 +0000 Subject: [PATCH 01/59] base method for monobehaviours (to impl) added some event cleanups for PlayerCinematics.cs and StoryGoalInitialSyncProcessor.cs removed application.quit from IngameMenu_QuitGame_Patch.cs --- .../StoryGoalInitialSyncProcessor.cs | 21 +++++++++++++++- .../GameLogic/PlayerLogic/PlayerCinematics.cs | 25 +++++++++++++++++-- .../MonoBehaviours/NitroxSessionBehaviour.cs | 22 ++++++++++++++++ .../Dynamic/IngameMenu_QuitGame_Patch.cs | 2 +- 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs diff --git a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs index 03282d6dd0..66de3e3577 100644 --- a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs @@ -12,6 +12,8 @@ namespace NitroxClient.GameLogic.InitialSync; public sealed class StoryGoalInitialSyncProcessor : InitialSyncProcessor { + private static bool storyGoalHandlerRegistered; + private readonly TimeManager timeManager; public StoryGoalInitialSyncProcessor(TimeManager timeManager) @@ -152,7 +154,12 @@ private static void SetScheduledGoals(InitialPlayerSync packet) // We don't want any scheduled goal we add now to be executed before initial sync has finished, else they might not get broadcasted StoryGoalScheduler.main.paused = true; - Multiplayer.OnLoadingComplete += () => StoryGoalScheduler.main.paused = false; + if (!storyGoalHandlerRegistered) + { + Multiplayer.OnLoadingComplete += OnLoadingCompleteUnpauseStoryGoals; + Multiplayer.OnAfterMultiplayerEnd += CleanupStoryGoalHandler; + storyGoalHandlerRegistered = true; + } foreach (NitroxScheduledGoal scheduledGoal in scheduledGoals) { @@ -195,4 +202,16 @@ private void SetTimeData(InitialPlayerSync packet) timeManager.InitRealTimeElapsed(packet.TimeData.TimePacket.RealTimeElapsed, packet.TimeData.TimePacket.UpdateTime, packet.IsFirstPlayer); timeManager.AuroraRealExplosionTime = packet.TimeData.AuroraEventData.AuroraRealExplosionTime; } + + private static void OnLoadingCompleteUnpauseStoryGoals() + { + StoryGoalScheduler.main.paused = false; + } + + private static void CleanupStoryGoalHandler() + { + Multiplayer.OnLoadingComplete -= OnLoadingCompleteUnpauseStoryGoals; + Multiplayer.OnAfterMultiplayerEnd -= CleanupStoryGoalHandler; + storyGoalHandlerRegistered = false; + } } diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs index 9928c52182..abeb09a177 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs @@ -14,6 +14,7 @@ public class PlayerCinematics private readonly LocalPlayer localPlayer; private IntroCinematicMode lastModeToSend = IntroCinematicMode.NONE; + private bool cinematicsHandlerRegistered; public ushort? IntroCinematicPartnerId = null; @@ -26,6 +27,9 @@ public PlayerCinematics(IPacketSender packetSender, LocalPlayer localPlayer) { this.packetSender = packetSender; this.localPlayer = localPlayer; + + // Register for cleanup when session ends + Multiplayer.OnAfterMultiplayerEnd += CleanupCinematicsHandler; } public void StartCinematicMode(ushort playerId, NitroxId controllerID, int controllerNameHash, string key) @@ -66,11 +70,28 @@ public void SetLocalIntroCinematicMode(IntroCinematicMode introCinematicMode) return; } - if (lastModeToSend == IntroCinematicMode.NONE) + if (lastModeToSend == IntroCinematicMode.NONE && !cinematicsHandlerRegistered) { - Multiplayer.OnLoadingComplete += () => SetLocalIntroCinematicMode(lastModeToSend); + Multiplayer.OnLoadingComplete += OnLoadingCompleteSendCinematicMode; + cinematicsHandlerRegistered = true; } lastModeToSend = introCinematicMode; } + + private void OnLoadingCompleteSendCinematicMode() + { + SetLocalIntroCinematicMode(lastModeToSend); + } + + private void CleanupCinematicsHandler() + { + if (cinematicsHandlerRegistered) + { + Multiplayer.OnLoadingComplete -= OnLoadingCompleteSendCinematicMode; + cinematicsHandlerRegistered = false; + } + Multiplayer.OnAfterMultiplayerEnd -= CleanupCinematicsHandler; + lastModeToSend = IntroCinematicMode.NONE; + } } diff --git a/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs b/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs new file mode 100644 index 0000000000..4a14fbd002 --- /dev/null +++ b/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace NitroxClient.MonoBehaviours; + +/// +/// Base class for Monos that exist only during a multiplayer session +/// This auto-handles cleanup +/// +public abstract class NitroxSessionBehaviour : MonoBehaviour +{ + protected virtual void Awake() + { + Multiplayer.OnAfterMultiplayerEnd += OnSessionEnd; + } + + protected virtual void OnDestroy() + { + Multiplayer.OnAfterMultiplayerEnd += OnSessionEnd; + } + + protected virtual void OnSessionEnd(){ } +} diff --git a/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs b/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs index 32a47971b9..79f143f6e8 100644 --- a/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs @@ -10,7 +10,7 @@ public sealed partial class IngameMenu_QuitGame_Patch : NitroxPatch, IDynamicPat public static bool Prefix() { // TODO: Remove this patch after fixing that no MP resources are left on disconnect. So that we can return to main menu. - Application.Quit(); + //Application.Quit(); return false; } } From e95936e1e217541db95531b2dd2a05d5668951eb Mon Sep 17 00:00:00 2001 From: MistaOmega Date: Sun, 25 Jan 2026 19:28:39 +0000 Subject: [PATCH 02/59] session cleanup logic and resource management for exit to main menu - Properly unhook events for multiplayer session lifecycle. - Add `OnSessionEnd` handling to session-scoped behaviours for automatic cleanup. - Clear registries and caches on session end. - Fix issues w/ stale references in service lifetimes. --- Nitrox.Model/Core/NitroxServiceLocator.cs | 9 +++++- Nitrox.Model/GameLogic/FMOD/FMODWhitelist.cs | 5 ++- .../GameLogic/Bases/BuildingHandler.cs | 13 +++++++- NitroxClient/GameLogic/PlayerManager.cs | 11 +++++++ .../MonoBehaviours/Cyclops/VirtualCyclops.cs | 29 ++++++++++++++--- .../EntityPositionBroadcaster.cs | 11 +++++-- .../MonoBehaviours/MovementBroadcaster.cs | 17 ++++++---- NitroxClient/MonoBehaviours/Multiplayer.cs | 31 +++++++++++++++++++ NitroxClient/MonoBehaviours/NitroxEntity.cs | 8 +++++ .../MonoBehaviours/NitroxSessionBehaviour.cs | 2 +- .../PlayerMovementBroadcaster.cs | 5 +-- .../Dynamic/IngameMenu_QuitGame_Patch.cs | 3 +- 12 files changed, 124 insertions(+), 20 deletions(-) diff --git a/Nitrox.Model/Core/NitroxServiceLocator.cs b/Nitrox.Model/Core/NitroxServiceLocator.cs index 9929d8b94d..d31fb7a5e3 100644 --- a/Nitrox.Model/Core/NitroxServiceLocator.cs +++ b/Nitrox.Model/Core/NitroxServiceLocator.cs @@ -53,7 +53,14 @@ public static void BeginNewLifetimeScope() throw new InvalidOperationException("You must install an Autofac container before initializing a new lifetime scope."); } - CurrentLifetimeScope?.Dispose(); + // If there's an existing scope, invalidate caches before disposing + // Should allow us to handle instances of stale refs for issue 2545 + if (CurrentLifetimeScope != null) + { + OnLifetimeScopeEnded(); + CurrentLifetimeScope.Dispose(); + } + CurrentLifetimeScope = DependencyContainer.BeginLifetimeScope(); } diff --git a/Nitrox.Model/GameLogic/FMOD/FMODWhitelist.cs b/Nitrox.Model/GameLogic/FMOD/FMODWhitelist.cs index 0c41b41f69..0d57c76e6c 100644 --- a/Nitrox.Model/GameLogic/FMOD/FMODWhitelist.cs +++ b/Nitrox.Model/GameLogic/FMOD/FMODWhitelist.cs @@ -96,7 +96,10 @@ public ReadOnlyDictionary GetWhitelist() public void Dispose() { - ThrowIfDisposed(); + if (isDisposed) + { + return; + } isDisposed = true; soundsWhitelist.Clear(); whitelistedPaths.Clear(); diff --git a/NitroxClient/GameLogic/Bases/BuildingHandler.cs b/NitroxClient/GameLogic/Bases/BuildingHandler.cs index 4e99650514..20a11f2b2d 100644 --- a/NitroxClient/GameLogic/Bases/BuildingHandler.cs +++ b/NitroxClient/GameLogic/Bases/BuildingHandler.cs @@ -18,7 +18,7 @@ namespace NitroxClient.GameLogic.Bases; -public partial class BuildingHandler : MonoBehaviour +public partial class BuildingHandler : NitroxSessionBehaviour { public static BuildingHandler Main; @@ -409,4 +409,15 @@ public void AskForResync() this.Resolve().Send(new BuildingResyncRequest()); Log.InGame(Language.main.Get("Nitrox_ResyncRequested")); } + + protected override void OnSessionEnd() + { + if (Main == this) + { + Main = null; // todo: this valid? + } + BuildQueue?.Clear(); + BasesCooldown?.Clear(); + Operations?.Clear(); + } } diff --git a/NitroxClient/GameLogic/PlayerManager.cs b/NitroxClient/GameLogic/PlayerManager.cs index 6feb95f25b..ab8118a9d8 100644 --- a/NitroxClient/GameLogic/PlayerManager.cs +++ b/NitroxClient/GameLogic/PlayerManager.cs @@ -88,6 +88,17 @@ public void RemovePlayer(ushort playerId) } } + /// + /// Removes all remote players + /// + public void RemoveAllPlayers() + { + foreach (ushort playerId in playersById.Keys.ToList()) + { + RemovePlayer(playerId); + } + } + /// Remote players + You => X + 1 public int GetTotalPlayerCount() => playersById.Count + 1; diff --git a/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs b/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs index 2fb6f1b4cd..ca68d0a47a 100644 --- a/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs +++ b/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs @@ -19,6 +19,7 @@ public class VirtualCyclops : MonoBehaviour public const string NAME = "VirtualCyclops"; private static readonly Dictionary cacheColliderCopy = []; + private static bool isDisposed; private readonly Dictionary virtualOpenableByName = []; private readonly Dictionary realOpenableByName = []; private readonly Dictionary virtualConstructableByRealGameObject = []; @@ -31,14 +32,30 @@ public class VirtualCyclops : MonoBehaviour public static void Initialize() { + isDisposed = false; CreateVirtualCyclops(); Multiplayer.OnAfterMultiplayerEnd += Dispose; } public static void Dispose() { - Destroy(Instance.gameObject); - Instance = null; + isDisposed = true; + + // Destroy cached collider copies from previous session + foreach (GameObject cachedCollider in cacheColliderCopy.Values) + { + if (cachedCollider) + { + Destroy(cachedCollider); + } + } + cacheColliderCopy.Clear(); + + if (Instance) + { + Destroy(Instance.gameObject); + Instance = null; + } Multiplayer.OnAfterMultiplayerEnd -= Dispose; } @@ -99,6 +116,10 @@ public static IEnumerator InitializeConstructablesCache() TaskResult result = new(); foreach (TechType techType in constructableTechTypes) { + if (isDisposed) + { + yield break; + } yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result); if (result.value && result.value.GetComponent()) { @@ -234,7 +255,7 @@ public void ReplicateConstructable(Constructable constructable) /// public static GameObject CreateColliderCopy(GameObject realObject, TechType techType) { - if (cacheColliderCopy.TryGetValue(techType, out GameObject colliderCopy)) + if (cacheColliderCopy.TryGetValue(techType, out GameObject colliderCopy) && colliderCopy) { return GameObject.Instantiate(colliderCopy); } @@ -276,7 +297,7 @@ public static GameObject CreateColliderCopy(GameObject realObject, TechType tech child.SetParent(colliderCopy.transform, false); } - cacheColliderCopy.Add(techType, colliderCopy); + cacheColliderCopy[techType] = colliderCopy; return GameObject.Instantiate(colliderCopy); } diff --git a/NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs b/NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs index 6fcdf2fd02..709b886845 100644 --- a/NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs +++ b/NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs @@ -12,7 +12,7 @@ namespace NitroxClient.MonoBehaviours; -public class EntityPositionBroadcaster : MonoBehaviour +public class EntityPositionBroadcaster : NitroxSessionBehaviour { public static readonly float BROADCAST_INTERVAL = 0.25f; @@ -24,11 +24,18 @@ public class EntityPositionBroadcaster : MonoBehaviour private float time; - public void Awake() + protected override void Awake() { + base.Awake(); packetSender = NitroxServiceLocator.LocateService(); } + protected override void OnSessionEnd() + { + watchingEntityIds.Clear(); + splineUpdatesById.Clear(); + } + public void Update() { time += Time.deltaTime; diff --git a/NitroxClient/MonoBehaviours/MovementBroadcaster.cs b/NitroxClient/MonoBehaviours/MovementBroadcaster.cs index ed3384e0e1..0fcaf245f9 100644 --- a/NitroxClient/MonoBehaviours/MovementBroadcaster.cs +++ b/NitroxClient/MonoBehaviours/MovementBroadcaster.cs @@ -9,7 +9,7 @@ namespace NitroxClient.MonoBehaviours; -public class MovementBroadcaster : MonoBehaviour +public class MovementBroadcaster : NitroxSessionBehaviour { public const int BROADCAST_FREQUENCY = 30; public const float BROADCAST_PERIOD = 1f / BROADCAST_FREQUENCY; @@ -31,11 +31,6 @@ public void Start() Instance = this; } - public void OnDestroy() - { - Instance = null; - } - public void Update() { float currentTime = (float)this.Resolve().RealTimeElapsed; @@ -106,4 +101,14 @@ public static void UnregisterReplicator(MovementReplicator movementReplicator) Instance.Replicators.Remove(movementReplicator.objectId); } } + protected override void OnDestroy() + { + base.OnDestroy(); + Instance = null; + } + + protected override void OnSessionEnd() + { + Replicators?.Clear(); + } } diff --git a/NitroxClient/MonoBehaviours/Multiplayer.cs b/NitroxClient/MonoBehaviours/Multiplayer.cs index 02a9f643fc..5cfa3b2643 100644 --- a/NitroxClient/MonoBehaviours/Multiplayer.cs +++ b/NitroxClient/MonoBehaviours/Multiplayer.cs @@ -191,9 +191,40 @@ public void InitMonoBehaviours() public void StopCurrentSession() { SceneManager.sceneLoaded -= SceneManager_sceneLoaded; + + // Destroy session-scoped Mono's before cleanup events + DestroySessionMonoBehaviours(); + + // Clear entity registry before invoking end event + NitroxEntity.ClearAll(); + + // clear remote players + PlayerManager remotePlayerManager = NitroxServiceLocator.LocateService(); + remotePlayerManager.RemoveAllPlayers(); + OnAfterMultiplayerEnd?.Invoke(); UnregisterConnectedDelegates(); + + // Reset state + InitialSyncCompleted = false; + } + + /// + /// Destroys session-scoped MonoBehaviours to clean up resources before multiplayer end. + /// todo: consider replacing manual Destroy calls by having components inherit + /// so cleanup happens automatically on session end. + /// + private void DestroySessionMonoBehaviours() + { + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); + Destroy(GetComponent()); } private static void SetLoadingComplete() diff --git a/NitroxClient/MonoBehaviours/NitroxEntity.cs b/NitroxClient/MonoBehaviours/NitroxEntity.cs index bfb232ee16..bc671c023a 100644 --- a/NitroxClient/MonoBehaviours/NitroxEntity.cs +++ b/NitroxClient/MonoBehaviours/NitroxEntity.cs @@ -135,6 +135,14 @@ public static void RemoveFrom(GameObject gameObject) } } + /// + /// Clears all tracked entity-to-GO mappings. + /// + public static void ClearAll() + { + gameObjectsById.Clear(); + } + /// /// Removes the from the global directory and set's its to null. /// diff --git a/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs b/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs index 4a14fbd002..80966390d3 100644 --- a/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs +++ b/NitroxClient/MonoBehaviours/NitroxSessionBehaviour.cs @@ -15,7 +15,7 @@ protected virtual void Awake() protected virtual void OnDestroy() { - Multiplayer.OnAfterMultiplayerEnd += OnSessionEnd; + Multiplayer.OnAfterMultiplayerEnd -= OnSessionEnd; } protected virtual void OnSessionEnd(){ } diff --git a/NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs b/NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs index 3e38d6fe5a..85600df44a 100644 --- a/NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs +++ b/NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs @@ -8,12 +8,13 @@ namespace NitroxClient.MonoBehaviours; -public class PlayerMovementBroadcaster : MonoBehaviour +public class PlayerMovementBroadcaster : NitroxSessionBehaviour { private LocalPlayer localPlayer; - public void Awake() + protected override void Awake() { + base.Awake(); localPlayer = this.Resolve(); } diff --git a/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs b/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs index 79f143f6e8..de9de5118d 100644 --- a/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/IngameMenu_QuitGame_Patch.cs @@ -10,7 +10,6 @@ public sealed partial class IngameMenu_QuitGame_Patch : NitroxPatch, IDynamicPat public static bool Prefix() { // TODO: Remove this patch after fixing that no MP resources are left on disconnect. So that we can return to main menu. - //Application.Quit(); - return false; + return true; } } From 91302572d2ee02fea13657d80c33baaed894cec8 Mon Sep 17 00:00:00 2001 From: MistaOmega Date: Mon, 26 Jan 2026 12:54:11 +0000 Subject: [PATCH 03/59] fix(movement): clear pawn controllers --- NitroxClient/GameLogic/CyclopsPawn.cs | 14 +++++++++++++- .../MonoBehaviours/Cyclops/VirtualCyclops.cs | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/NitroxClient/GameLogic/CyclopsPawn.cs b/NitroxClient/GameLogic/CyclopsPawn.cs index 923417f519..1bc227b8b0 100644 --- a/NitroxClient/GameLogic/CyclopsPawn.cs +++ b/NitroxClient/GameLogic/CyclopsPawn.cs @@ -87,11 +87,23 @@ public void RegisterController() { foreach (CharacterController controller in controllers) { - Physics.IgnoreCollision(controller, Controller); + // Just-in-case check if ClearController hasn't been called, or some stale controllers exist + if (controller && controller.GetComponent()) + { + Physics.IgnoreCollision(controller, Controller); + } } controllers.Add(Controller); } + /// + /// Clears the static controllers list + /// + public static void ClearControllers() + { + controllers.Clear(); + } + public void SetReference() { Handle.transform.localPosition = RealObject.transform.localPosition; diff --git a/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs b/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs index ca68d0a47a..4f3cea337f 100644 --- a/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs +++ b/NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NitroxClient.Communication; +using NitroxClient.GameLogic; using NitroxClient.GameLogic.Spawning.WorldEntities; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; @@ -51,6 +52,9 @@ public static void Dispose() } cacheColliderCopy.Clear(); + // Clear pawn controllers from previous session + CyclopsPawn.ClearControllers(); + if (Instance) { Destroy(Instance.gameObject); From 8dd2b1f731d060592c1fa6ffa19dee21bbd44f20 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 30 Jan 2026 14:17:21 +0100 Subject: [PATCH 04/59] [Weblate] Translations update from Hosted Weblate (#2660) --- .../LanguageFiles/cs.json | 3 +++ .../LanguageFiles/de.json | 3 +++ .../LanguageFiles/en.json | 2 ++ .../LanguageFiles/es.json | 10 +++++++++- .../LanguageFiles/ga.json | 3 +++ .../LanguageFiles/nl.json | 10 ++++++++-- .../LanguageFiles/pt-BR.json | 20 +++++++++++++------ .../LanguageFiles/uk.json | 1 + 8 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/cs.json b/Nitrox.Assets.Subnautica/LanguageFiles/cs.json index 5dfe18b165..cc484803f2 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/cs.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/cs.json @@ -79,11 +79,14 @@ "Nitrox_ServerEntry_DeleteWarning": "Jste si jistý, že chcete smazat tento server?", "Nitrox_ServerStopped": "Server byl zastaven", "Nitrox_Settings_Bandwidth": "Nastavení šířky pásma", + "Nitrox_Settings_ChatVisibilityDuration": "Doba viditelnosti chatu (s)", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Jak dlouho zůstane chat viditelný. Nastavte hodnotu 0, chcete-li zakázat vyskakovací okna chatu.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Zvyš hodnotu pro nestabilní připojení", "Nitrox_Settings_Keybind_FocusDiscord": "Zaměření na okno s pozvánkou na Discordu", "Nitrox_Settings_Keybind_OpenChat": "Otevřít chat", "Nitrox_Settings_LatencyUpdatePeriod": "Doba zpoždění aktualizace (s)", "Nitrox_Settings_OfflineClockSyncDuration": "Doba trvání procedury synchronizace hodin (s)", + "Nitrox_Settings_Privacy": "Soukromí", "Nitrox_Settings_SafetyLatencyMargin": "Bezpečnostní rozpětí zpoždění (ms)", "Nitrox_ShowPing": "Zobrazit odezvu", "Nitrox_SilenceChat": "Ztlumit chat", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/de.json b/Nitrox.Assets.Subnautica/LanguageFiles/de.json index 267c82d980..3c3c54975b 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/de.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/de.json @@ -79,11 +79,14 @@ "Nitrox_ServerEntry_DeleteWarning": "Willst du diesen Server wirklich löschen?", "Nitrox_ServerStopped": "Der Server wurde gestoppt", "Nitrox_Settings_Bandwidth": "Bandbreiteneinstellungen", + "Nitrox_Settings_ChatVisibilityDuration": "Sichtbarkeitsdauer des Chats (s)", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Die Dauer, für die der Chat sichtbar bleibt. Setzen Sie den Wert auf 0, um Chat-Popups zu deaktivieren.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Gib bei instabilen Verbindungen einen höheren Wert ein", "Nitrox_Settings_Keybind_FocusDiscord": "Das Discord Anfragefenster fokussieren", "Nitrox_Settings_Keybind_OpenChat": "Chat öffnen", "Nitrox_Settings_LatencyUpdatePeriod": "Latenzaktualisierungszeitraum (s)", "Nitrox_Settings_OfflineClockSyncDuration": "Dauer des Zeitsynchronisations-Prozesses (s)", + "Nitrox_Settings_Privacy": "Datenschutz", "Nitrox_Settings_SafetyLatencyMargin": "Sicherheitslatenzpuffer (ms)", "Nitrox_ShowPing": "Signal anzeigen", "Nitrox_SilenceChat": "Chat stumm schalten", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/en.json b/Nitrox.Assets.Subnautica/LanguageFiles/en.json index d258473e43..2b13c384b2 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/en.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/en.json @@ -79,6 +79,8 @@ "Nitrox_ServerEntry_DeleteWarning": "Are you sure you want to delete this server?", "Nitrox_ServerStopped": "The server has been stopped", "Nitrox_Settings_Bandwidth": "Bandwidth settings", + "Nitrox_Settings_ChatVisibilityDuration": "Chat visibility duration (s)", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "How long the chat stays visible. Set to 0 to disable chat popups.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Give a higher value for unstable connections", "Nitrox_Settings_Keybind_FocusDiscord": "Focus on the Discord invite window", "Nitrox_Settings_Keybind_OpenChat": "Open chat", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/es.json b/Nitrox.Assets.Subnautica/LanguageFiles/es.json index 731879918b..64ecb0e09c 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/es.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/es.json @@ -9,13 +9,14 @@ "Nitrox_AddServer_NamePlaceholder": "Introduce un nombre para el servidor", "Nitrox_AddServer_PortDescription": "Puerto:", "Nitrox_AddServer_PortPlaceholder": "Ingresa el puerto numérico del servidor", + "Nitrox_BedGetUp": "Levantarse", "Nitrox_BuildingDesyncDetected": "El servidor detectó una desincronización con un edificio del cliente local (Ve a las opciones de Nitrox para solicitar una resincronización)", "Nitrox_BuildingSettings": "Construcción de bases", "Nitrox_Cancel": "Cancelar", "Nitrox_CommandNotAvailable": "Este comando no está disponible con Nitrox", "Nitrox_Confirm": "Confirmar", "Nitrox_ConnectTo": "Conectarse a", - "Nitrox_DenyOwnershipHand": "Otro jugador esta interactiando con ese objeto", + "Nitrox_DenyOwnershipHand": "Otro jugador esta interactuando con ese objeto", "Nitrox_DisconnectedSession": "Desconectado del servidor", "Nitrox_DiscordAccept": "Aceptar", "Nitrox_DiscordDecline": "Cancelar", @@ -78,12 +79,19 @@ "Nitrox_ServerEntry_DeleteWarning": "¿Estás seguro de que deseas eliminar este servidor?", "Nitrox_ServerStopped": "El servidor se ha parado", "Nitrox_Settings_Bandwidth": "Configuración del ancho de banda", + "Nitrox_Settings_ChatVisibilityDuration": "Duración de la visibilidad del chat", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Cuánto tiempo permanece visible el chat. Establécelo en 0 para desactivar las ventanas emergentes del chat.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Dar un valor más alto para conexiones inestables", + "Nitrox_Settings_Keybind_FocusDiscord": "Centrarse en la ventana de invitación de Discord", + "Nitrox_Settings_Keybind_OpenChat": "Abrir chat", "Nitrox_Settings_LatencyUpdatePeriod": "Periodo(s) de actualización de la latencia", + "Nitrox_Settings_OfflineClockSyncDuration": "Duración del procedimiento de sincronización del reloj", + "Nitrox_Settings_Privacy": "Privacidad", "Nitrox_Settings_SafetyLatencyMargin": "Margen de latencia de seguridad (ms)", "Nitrox_ShowPing": "Mostrar señal", "Nitrox_SilenceChat": "Silenciar el chat", "Nitrox_SilencedChatNotif": "Se silencia el chat", + "Nitrox_SleepingPlayers": "[Durmiendo]/[Total]jugadores durmiendo", "Nitrox_StartServer": "Inicie su servidor para alcanzar a su mundo", "Nitrox_SyncingWorld": "Sincronización del mundo multijugador…", "Nitrox_TeleportTo": "Teleportar hacia {PLAYER}", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/ga.json b/Nitrox.Assets.Subnautica/LanguageFiles/ga.json index ff16c16477..991d286917 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/ga.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/ga.json @@ -79,11 +79,14 @@ "Nitrox_ServerEntry_DeleteWarning": "An bhfuil tú cinnte go dteastaíonn uait an freastalaí seo a scriosadh?", "Nitrox_ServerStopped": "Stopadh an freastalaí", "Nitrox_Settings_Bandwidth": "Socruithe bandaleithead", + "Nitrox_Settings_ChatVisibilityDuration": "Fad infheictheachta comhrá (s)", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Cé chomh fada is a fhanann an comhrá le feiceáil. Socraigh go 0 chun fuinneoga aníos comhrá a dhíchumasú.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Tabhair luach níos airde do naisc éagobhsaí", "Nitrox_Settings_Keybind_FocusDiscord": "Dírigh ar fhuinneog cuireadh Discord", "Nitrox_Settings_Keybind_OpenChat": "Oscail comhrá", "Nitrox_Settings_LatencyUpdatePeriod": "Tréimhse (nó) nuashonraithe moille", "Nitrox_Settings_OfflineClockSyncDuration": "Fad nós imeachta sioncrónaithe cloig (s)", + "Nitrox_Settings_Privacy": "Príobháideacht", "Nitrox_Settings_SafetyLatencyMargin": "Corrlach moille sábháilteachta (ms)", "Nitrox_ShowPing": "Taispeáin Ping", "Nitrox_SilenceChat": "Balbhaigh comhrá", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/nl.json b/Nitrox.Assets.Subnautica/LanguageFiles/nl.json index f543117be4..827a6aacb7 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/nl.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/nl.json @@ -9,6 +9,7 @@ "Nitrox_AddServer_NamePlaceholder": "Voer een naam in voor de server", "Nitrox_AddServer_PortDescription": "Poort:", "Nitrox_AddServer_PortPlaceholder": "Voer de numerieke poort van de server in", + "Nitrox_BedGetUp": "Sta op", "Nitrox_BuildingDesyncDetected": "Synchronisatie probleem gedetecteerd in basis gebouwen (ga naar Nitrox instellingen om basissen opnieuw in te laden)", "Nitrox_BuildingSettings": "Basis bouwen", "Nitrox_Cancel": "Annuleer", @@ -78,12 +79,17 @@ "Nitrox_ServerEntry_DeleteWarning": "Weet u zeker dat u deze server wilt verwijderen?", "Nitrox_ServerStopped": "Server is gestopt", "Nitrox_Settings_Bandwidth": "Bandbreedte-instellingen", + "Nitrox_Settings_ChatVisibilityDuration": "Chat zichtbaarheid duur (s)", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Hoelang de chat zichtbaar blijft. Zet op 0 op chat popups uit te zetten.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Geef een hogere waarde voor onstabiele verbindingen", - "Nitrox_Settings_LatencyUpdatePeriod": "Latentie-updateperiode (s)", - "Nitrox_Settings_SafetyLatencyMargin": "Veiligheidslatentiemarge (ms)", + "Nitrox_Settings_Keybind_OpenChat": "Open chat", + "Nitrox_Settings_LatencyUpdatePeriod": "Latentie-updateperiode(s)", + "Nitrox_Settings_Privacy": "Privacy", + "Nitrox_Settings_SafetyLatencyMargin": "Veiligheids latentie marge (ms)", "Nitrox_ShowPing": "Laat pin zien", "Nitrox_SilenceChat": "Chat dempen", "Nitrox_SilencedChatNotif": "Chat is gedempt", + "Nitrox_SleepingPlayers": "{SLEEPING}/{TOTAL} spelers zijn aan het slapen.", "Nitrox_StartServer": "Start je server eerst om mee toe doen met je eigen gehostte wereld", "Nitrox_SyncingWorld": "Multiplayer Wereld Synchroniseren…", "Nitrox_TeleportTo": "Teleporteer naar {PLAYER}", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/pt-BR.json b/Nitrox.Assets.Subnautica/LanguageFiles/pt-BR.json index 6855a510a7..c2bccdbb22 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/pt-BR.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/pt-BR.json @@ -9,6 +9,7 @@ "Nitrox_AddServer_NamePlaceholder": "Digite um nome para o servidor", "Nitrox_AddServer_PortDescription": "Porta:", "Nitrox_AddServer_PortPlaceholder": "Digite o número da porta do servidor", + "Nitrox_BedGetUp": "Levantar-se", "Nitrox_BuildingDesyncDetected": "O servidor detectou uma dessincronização com as construções do cliente local (vá para as configurações do Nitrox para solicitar uma ressincronização)", "Nitrox_BuildingSettings": "Construção de base", "Nitrox_Cancel": "Cancelar", @@ -28,8 +29,8 @@ "Nitrox_EnterName": "Nome do jogador", "Nitrox_ErrorDesyncDetected": "[Construção Segura] Esta base está atualmente dessincronizada, então você não pode modificá-la, a menos que sincronize novamente as construções (nas configurações do Nitrox)", "Nitrox_ErrorRecentBuildUpdate": "Não é possível modificar uma base que foi atualizada recentemente por outro jogador", - "Nitrox_Failure": "Um erro ocorreu", - "Nitrox_FinishedResyncRequest": "Levou {TIME}ms para ressincronizar {COUNT} entities", + "Nitrox_Failure": "Ocorreu um erro", + "Nitrox_FinishedResyncRequest": "Levou {TIME}ms para ressincronizar {COUNT} entidades", "Nitrox_FirewallInterfering": "As configurações do seu Firewall estão em conflito", "Nitrox_HideIp": "Ocultar endereço IP", "Nitrox_HidePing": "Ocultar Ping", @@ -39,7 +40,7 @@ "Nitrox_Join": "Entrar", "Nitrox_JoinServer": "Entrando:", "Nitrox_JoinServerPassword": "Senha:", - "Nitrox_JoinServerPasswordHeader": "O Servidor selecionado necessita de senha", + "Nitrox_JoinServerPasswordHeader": "Senha do servidor necessária", "Nitrox_JoinServerPlaceholder": "Por favor insira a senha do servidor", "Nitrox_JoiningSession": "Entrando na Sessão", "Nitrox_Kick": "Expulsar {PLAYER}", @@ -58,7 +59,7 @@ "Nitrox_OK": "Ok", "Nitrox_OutOfDateClient": "A sua instalação do Nitrox está desatualizada. Servidor: {serverVersion}, Sua: {localVersion}.", "Nitrox_OutOfDateServer": "O servidor está rodando em uma versão antiga do Nitrox. Peça para o administrador do servidor para atualizar ou desatualizar a sua instalação do Nitrox. Versão do Servidor: {serverVersion}, Sua Versão: {localVersion}.", - "Nitrox_PlayerDeathBeaconLabel": "{PLAYER}'s morto", + "Nitrox_PlayerDeathBeaconLabel": "{PLAYER} morto", "Nitrox_PlayerDied": "{PLAYER} morreu", "Nitrox_PlayerDisconnected": "{PLAYER} desconectou-se", "Nitrox_PlayerJoined": "{PLAYER} entrou no jogo.", @@ -76,21 +77,28 @@ "Nitrox_SafeBuildingLog": "Registro de construção segura", "Nitrox_ScannerRoomWarn": "Atenção: as salas de scanner não funcionam corretamente nesta versão do Nitrox. Espere muitos bugs.", "Nitrox_ServerEntry_DeleteWarning": "Tem certeza de que deseja excluir este servidor?", - "Nitrox_ServerStopped": "O servidor foi fechado", + "Nitrox_ServerStopped": "O servidor foi desligado", "Nitrox_Settings_Bandwidth": "Configurações de largura de banda", + "Nitrox_Settings_ChatVisibilityDuration": "Duração da visibilidade do chat", + "Nitrox_Settings_ChatVisibilityDuration_Tooltip": "Por quanto tempo o chat permanece visível. Defina como 0 para desativar os pop-ups do chat.", "Nitrox_Settings_HigherForUnstable_Tooltip": "Dê um valor maior para conexões instáveis", + "Nitrox_Settings_Keybind_FocusDiscord": "Foco na janela de convite do Discord", + "Nitrox_Settings_Keybind_OpenChat": "Abrir chat", "Nitrox_Settings_LatencyUpdatePeriod": "Período(s) de atualização(ões) de latência", + "Nitrox_Settings_OfflineClockSyncDuration": "Duração de procedimento de sinc. do relógio (s)", + "Nitrox_Settings_Privacy": "Privacidade", "Nitrox_Settings_SafetyLatencyMargin": "Margem de latência de segurança (ms)", "Nitrox_ShowPing": "Mostrar ping", "Nitrox_SilenceChat": "Silenciar a conversa", "Nitrox_SilencedChatNotif": "O chat agora está silenciado", + "Nitrox_SleepingPlayers": "{SLEEPING}/{TOTAL} jogadores dormindo.", "Nitrox_StartServer": "Inicie seu servidor primeiro para entrar na sua sessão", "Nitrox_SyncingWorld": "Sincronizando com Servidor Multiplayer…", "Nitrox_TeleportTo": "Teletransportar para {PLAYER}", "Nitrox_TeleportToMe": "Teletransportar {PLAYER} para mim", "Nitrox_TeleportToMeQuestion": "Teletransportar {PLAYER} para mim?", "Nitrox_TeleportToQuestion": "Teletransportar para {PLAYER}?", - "Nitrox_ThankForPlaying": "Obrigado por utilizar Nitrox !", + "Nitrox_ThankForPlaying": "Obrigado por utilizar Nitrox!", "Nitrox_UnableToConnect": "Não foi possível conectar com o Servidor remoto:", "Nitrox_Unmute": "Desmutar {PLAYER}", "Nitrox_UnmuteQuestion": "Desmutar {PLAYER}?", diff --git a/Nitrox.Assets.Subnautica/LanguageFiles/uk.json b/Nitrox.Assets.Subnautica/LanguageFiles/uk.json index bc28e561b7..3a01f853bd 100644 --- a/Nitrox.Assets.Subnautica/LanguageFiles/uk.json +++ b/Nitrox.Assets.Subnautica/LanguageFiles/uk.json @@ -9,6 +9,7 @@ "Nitrox_AddServer_NamePlaceholder": "Введіть ім'я серверу", "Nitrox_AddServer_PortDescription": "Порт:", "Nitrox_AddServer_PortPlaceholder": "Введіть числовий порт сервера", + "Nitrox_BedGetUp": "Вставай", "Nitrox_BuildingDesyncDetected": "Сервер виявив десинхронізацію з локальними клієнтськими будівлями (перейдіть до налаштувань Nitrox, щоб надіслати запит на повторну синхронізацію)", "Nitrox_BuildingSettings": "Налаштування будівель", "Nitrox_Cancel": "Скасувати", From bde6d8fc5f0b9edae29a0bf051db6f9674725e37 Mon Sep 17 00:00:00 2001 From: Meas <1107063+Measurity@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:05:09 +0100 Subject: [PATCH 05/59] Enabled CRC for packets and use native sockets (latter only server-side) (#2661) --- .../Models/Communication/LiteNetLibServer.cs | 5 ++++- .../NetworkingLayer/LiteNetLib/LiteNetLibClient.cs | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs index ff064222e9..d56ba2a925 100644 --- a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs +++ b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs @@ -1,7 +1,9 @@ using System.Buffers; using System.Collections.Generic; using LiteNetLib; +using LiteNetLib.Layers; using LiteNetLib.Utils; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; @@ -37,8 +39,9 @@ public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager this.options = options; this.logger = logger; listener = new EventBasedNetListener(); - server = new NetManager(listener) + server = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) { + UseNativeSockets = true, IPv6Enabled = true }; } diff --git a/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs b/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs index dc24917775..2970386ec5 100644 --- a/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs +++ b/NitroxClient/Communication/NetworkingLayer/LiteNetLib/LiteNetLibClient.cs @@ -4,14 +4,15 @@ using System.Threading; using System.Threading.Tasks; using LiteNetLib; +using LiteNetLib.Layers; using LiteNetLib.Utils; +using Nitrox.Model.Core; using NitroxClient.Communication.Abstract; using NitroxClient.Debuggers; using NitroxClient.MonoBehaviours; using NitroxClient.MonoBehaviours.Gui.Modals; using Nitrox.Model.Networking; using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.NetworkingLayer.LiteNetLib; @@ -47,7 +48,7 @@ public LiteNetLibClient(PacketReceiver packetReceiver, INetworkDebugger networkD }; - client = new NetManager(listener) + client = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) { UpdateTime = 15, ChannelsCount = (byte)typeof(Packet.UdpChannelId).GetEnumValues().Length, From 3f06268114f0ca01b517e7e87e073455bafbb553 Mon Sep 17 00:00:00 2001 From: MrBub <106004257+misterbubb@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:48:39 -0500 Subject: [PATCH 06/59] Sync DataBox (BlueprintHandTarget) state across players (#2644) Co-authored-by: dartasen <10561268+dartasen@users.noreply.github.com> --- .../Metadata/BlueprintHandTargetMetadata.cs | 29 +++++++++++ .../Entities/Metadata/EntityMetadata.cs | 1 + .../Server/Serialization/WorldServiceTest.cs | 3 ++ .../BlueprintHandTargetMetadataExtractor.cs | 15 ++++++ .../BlueprintHandTargetMetadataProcessor.cs | 51 +++++++++++++++++++ ...ueprintHandTarget_UnlockBlueprint_Patch.cs | 23 +++++++++ 6 files changed, 122 insertions(+) create mode 100644 Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/BlueprintHandTargetMetadata.cs create mode 100644 NitroxClient/GameLogic/Spawning/Metadata/Extractor/BlueprintHandTargetMetadataExtractor.cs create mode 100644 NitroxClient/GameLogic/Spawning/Metadata/Processor/BlueprintHandTargetMetadataProcessor.cs create mode 100644 NitroxPatcher/Patches/Dynamic/BlueprintHandTarget_UnlockBlueprint_Patch.cs diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/BlueprintHandTargetMetadata.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/BlueprintHandTargetMetadata.cs new file mode 100644 index 0000000000..81c063ed21 --- /dev/null +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/BlueprintHandTargetMetadata.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.Serialization; +using BinaryPack.Attributes; + +namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; + +[Serializable] +[DataContract] +public class BlueprintHandTargetMetadata : EntityMetadata +{ + [DataMember(Order = 1)] + public bool Used { get; } + + [IgnoreConstructor] + protected BlueprintHandTargetMetadata() + { + // Constructor for serialization. Has to be "protected" for json serialization. + } + + public BlueprintHandTargetMetadata(bool used) + { + Used = used; + } + + public override string ToString() + { + return $"[BlueprintHandTargetMetadata Used: {Used}]"; + } +} diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs index 22e02c6c2e..815bb037ce 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs @@ -43,6 +43,7 @@ namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata [ProtoInclude(84, typeof(DrillableMetadata))] [ProtoInclude(85, typeof(PrecursorComputerTerminalMetadata))] [ProtoInclude(86, typeof(GenericConsoleMetadata))] + [ProtoInclude(87, typeof(BlueprintHandTargetMetadata))] public abstract class EntityMetadata { } diff --git a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs index f5c3e13f34..5de23c6606 100644 --- a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs @@ -345,6 +345,9 @@ private static void EntityTest(Entity entity, Entity entityAfter) case GenericConsoleMetadata metadata when entityAfter.Metadata is GenericConsoleMetadata metadataAfter: Assert.AreEqual(metadata.GotUsed, metadataAfter.GotUsed); break; + case BlueprintHandTargetMetadata metadata when entityAfter.Metadata is BlueprintHandTargetMetadata metadataAfter: + Assert.AreEqual(metadata.Used, metadataAfter.Used); + break; default: Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {entity.Metadata?.GetType().Name} - {entityAfter.Metadata?.GetType().Name}"); break; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/BlueprintHandTargetMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/BlueprintHandTargetMetadataExtractor.cs new file mode 100644 index 0000000000..6991fee285 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/BlueprintHandTargetMetadataExtractor.cs @@ -0,0 +1,15 @@ +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; +using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; + +/// +/// Extracts the current state of a DataBox (BlueprintHandTarget) for syncing to new players. +/// +public class BlueprintHandTargetMetadataExtractor : EntityMetadataExtractor +{ + public override BlueprintHandTargetMetadata Extract(BlueprintHandTarget entity) + { + return new BlueprintHandTargetMetadata(entity.used); + } +} diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/BlueprintHandTargetMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/BlueprintHandTargetMetadataProcessor.cs new file mode 100644 index 0000000000..f379b12726 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/BlueprintHandTargetMetadataProcessor.cs @@ -0,0 +1,51 @@ +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; +using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; +using UnityEngine; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Processor; + +/// +/// Processes DataBox (BlueprintHandTarget) metadata updates from other players. +/// When another player opens a DataBox, this applies the visual state change locally. +/// +public sealed class BlueprintHandTargetMetadataProcessor : EntityMetadataProcessor +{ + public override void ProcessMetadata(GameObject gameObject, BlueprintHandTargetMetadata metadata) + { + BlueprintHandTarget blueprintHandTarget = gameObject.GetComponent(); + if (!blueprintHandTarget) + { + Log.Error($"[BlueprintHandTargetMetadataProcessor] No BlueprintHandTarget component found on {gameObject.name}"); + return; + } + + // Skip if already in the target state + if (blueprintHandTarget.used == metadata.Used) + { + return; + } + + blueprintHandTarget.used = metadata.Used; + + if (metadata.Used) + { + // Trigger the animation if the DataBox has an animator + if (!string.IsNullOrEmpty(blueprintHandTarget.animParam) && blueprintHandTarget.animator) + { + blueprintHandTarget.animator.SetBool(blueprintHandTarget.animParam, true); + } + + // Disable the visual game object (the lid/door of the DataBox) + if (blueprintHandTarget.disableGameObject) + { + blueprintHandTarget.disableGameObject.SetActive(false); + } + + // Unregister from resource tracker to remove the scanner ping + if (blueprintHandTarget.resourceTracker) + { + blueprintHandTarget.resourceTracker.OnBlueprintHandTargetUsed(); + } + } + } +} diff --git a/NitroxPatcher/Patches/Dynamic/BlueprintHandTarget_UnlockBlueprint_Patch.cs b/NitroxPatcher/Patches/Dynamic/BlueprintHandTarget_UnlockBlueprint_Patch.cs new file mode 100644 index 0000000000..c7339fc4ab --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/BlueprintHandTarget_UnlockBlueprint_Patch.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using NitroxClient.GameLogic; +using Nitrox.Model.DataStructures; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Syncs DataBox (BlueprintHandTarget) usage across players. +/// When a player opens a DataBox to unlock a blueprint, this broadcasts the state to other players. +/// +public sealed partial class BlueprintHandTarget_UnlockBlueprint_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((BlueprintHandTarget t) => t.UnlockBlueprint()); + + public static void Postfix(BlueprintHandTarget __instance) + { + if (__instance.used && __instance.TryGetIdOrWarn(out NitroxId id)) + { + Resolve().BroadcastMetadataUpdate(id, new BlueprintHandTargetMetadata(__instance.used)); + } + } +} From 0d1fa0cd814a5e242342a1e9be03495b571e2936 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:56:10 +0100 Subject: [PATCH 07/59] Improved Steam exe finding in launcher for Linux This caused proton to be launched without a proper Steam exe. --- .../InstallationFinders/SteamFinder.cs | 61 ++----------------- Nitrox.Model/Platforms/Store/Steam.cs | 61 ++++++++++++++++--- 2 files changed, 56 insertions(+), 66 deletions(-) diff --git a/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs b/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs index 59a5147bb0..f2b16aed17 100644 --- a/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs +++ b/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Nitrox.Model.Platforms.Discovery.InstallationFinders.Core; -using Nitrox.Model.Platforms.OS.Windows; +using Nitrox.Model.Platforms.Store; using static Nitrox.Model.Platforms.Discovery.InstallationFinders.Core.GameFinderResult; namespace Nitrox.Model.Platforms.Discovery.InstallationFinders; @@ -50,69 +50,18 @@ public GameFinderResult FindGame(GameInfo gameInfo) return Ok(path); } - private static string? GetSteamPath() + private static string GetSteamPath() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - string steamPath = RegistryEx.Read(@"Software\Valve\Steam\SteamPath"); - - if (string.IsNullOrWhiteSpace(steamPath)) - { - steamPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), - "Steam" - ); - } - - return Directory.Exists(steamPath) ? steamPath : null; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (string.IsNullOrWhiteSpace(homePath)) - { - homePath = Environment.GetEnvironmentVariable("HOME"); - } - - if (!Directory.Exists(homePath)) - { - return null; - } - - string[] commonSteamPath = - [ - // Default install location - // https://github.com/ValveSoftware/steam-for-linux - Path.Combine(homePath, ".local", "share", "Steam"), - // Those symlinks are often use as a backward-compatibility (Debian, Ubuntu, Fedora, ArchLinux) - // https://wiki.archlinux.org/title/steam, https://askubuntu.com/questions/227502/where-are-steam-games-installed - Path.Combine(homePath, ".steam", "steam"), - Path.Combine(homePath, ".steam", "root"), - // Flatpack install - // https://github.com/flathub/com.valvesoftware.Steam/wiki, https://flathub.org/apps/com.valvesoftware.Steam - Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"), - Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".steam", "steam"), - ]; - - foreach (string path in commonSteamPath) - { - if (Directory.Exists(path)) - { - return path; - } - } - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (string.IsNullOrWhiteSpace(homePath)) { homePath = Environment.GetEnvironmentVariable("HOME"); } - if (!Directory.Exists(homePath)) { - return null; + return ""; } // Steam should always be here @@ -123,7 +72,7 @@ public GameFinderResult FindGame(GameInfo gameInfo) } } - return null; + return Path.GetDirectoryName(Steam.GetExeFile()) ?? ""; } /// diff --git a/Nitrox.Model/Platforms/Store/Steam.cs b/Nitrox.Model/Platforms/Store/Steam.cs index 608c882218..f56c1c0c54 100644 --- a/Nitrox.Model/Platforms/Store/Steam.cs +++ b/Nitrox.Model/Platforms/Store/Steam.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; @@ -107,13 +108,23 @@ await RegistryEx.CompareWaitAsync(@"SOFTWARE\Valve\Steam\ActiveProcess\Acti return steam; } - private static string? GetExeFile() + public static string? GetExeFile() { string steamExecutable = ""; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - steamExecutable = Path.Combine(RegistryEx.Read(@"SOFTWARE\Valve\Steam\SteamPath", steamExecutable), "steam.exe"); + string steamPath = RegistryEx.Read(@"Software\Valve\Steam\SteamPath"); + + if (string.IsNullOrWhiteSpace(steamPath)) + { + steamPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + "Steam" + ); + } + + steamExecutable = Directory.Exists(steamPath) ? Path.Combine(steamPath, "steam.exe") : ""; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { @@ -121,20 +132,50 @@ await RegistryEx.CompareWaitAsync(@"SOFTWARE\Valve\Steam\ActiveProcess\Acti } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - string userHomePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (!Directory.Exists(userHomePath)) + string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrWhiteSpace(homePath)) + { + homePath = Environment.GetEnvironmentVariable("HOME"); + } + if (!Directory.Exists(homePath)) { return null; } - string steamPath = Path.Combine(userHomePath, ".steam", "steam"); - // support flatpak - if (!Directory.Exists(steamPath)) + string[] commonPaths = [ + // Default install location + // https://github.com/ValveSoftware/steam-for-linux + Path.Combine(homePath, ".local", "share", "Steam"), + // Those symlinks are often use as a backward-compatibility (Debian, Ubuntu, Fedora, ArchLinux) + // https://wiki.archlinux.org/title/steam, https://askubuntu.com/questions/227502/where-are-steam-games-installed + Path.Combine(homePath, ".steam", "steam"), + Path.Combine(homePath, ".steam", "root"), + // Flatpack install + // https://github.com/flathub/com.valvesoftware.Steam/wiki, https://flathub.org/apps/com.valvesoftware.Steam + Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"), + Path.Combine(homePath, ".var", "app", "com.valvesoftware.Steam", ".steam", "steam"), + ]; + + string steamPath = ""; + foreach (string path in commonPaths) + { + try + { + if (Directory.GetFileSystemEntries(path).Any()) + { + steamPath = path; + break; + } + } + catch + { + // ignored + } + } + if (!string.IsNullOrWhiteSpace(steamPath)) { - steamPath = Path.Combine(userHomePath, ".var", "app", "com.valvesoftware.Steam", "data", "Steam"); + steamExecutable = Path.Combine(steamPath, "steam.sh"); } - - steamExecutable = Path.Combine(steamPath, "steam.sh"); } return File.Exists(steamExecutable) ? Path.GetFullPath(steamExecutable) : null; From 4830234b431656a992e2be2fe6c67e17aac719a1 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:55:52 +0100 Subject: [PATCH 08/59] Added code comment to SteamFinder explaining OSX special case --- .../Platforms/Discovery/InstallationFinders/SteamFinder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs b/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs index f2b16aed17..3b6429d835 100644 --- a/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs +++ b/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs @@ -52,6 +52,7 @@ public GameFinderResult FindGame(GameInfo gameInfo) private static string GetSteamPath() { + // OSX: Steam dynamic data isn't near the steam exe. Because it can't (or isn't supposed to) write anything inside application bundle. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { string homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); From b00d56715e9e7fa378c4053664fe739d94fcfc9b Mon Sep 17 00:00:00 2001 From: Meas <1107063+Measurity@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:55:56 +0100 Subject: [PATCH 09/59] Fixed prefab resource cache not storing all required data (#2665) --- .../Parsers/EntityDistributionsResource.cs | 16 +-- .../PrefabPlaceholderGroupsResource.cs | 98 ++++++++++--------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs index c4c6c2ca84..ed37e5021a 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs @@ -12,13 +12,12 @@ internal class EntityDistributionsResource(SubnauticaAssetsManager assetsManager private readonly SubnauticaAssetsManager assetsManager = assetsManager; private readonly IOptions options = options; - private ValueTask lootDistribution; - public LootDistributionData LootDistribution => GetLootDistributionDataAsync().GetAwaiter().GetResult(); + private readonly TaskCompletionSource lootDistributionTcs = new(); + public LootDistributionData LootDistribution => lootDistributionTcs.Task.GetAwaiter().GetResult(); - public Task LoadAsync(CancellationToken cancellationToken) + public async Task LoadAsync(CancellationToken cancellationToken) { - lootDistribution = GetLootDistributionDataAsync(cancellationToken); - return Task.CompletedTask; + lootDistributionTcs.TrySetResult(await GetLootDistributionDataAsync(cancellationToken)); } public Task CleanupAsync() @@ -27,13 +26,8 @@ public Task CleanupAsync() return Task.CompletedTask; } - private async ValueTask GetLootDistributionDataAsync(CancellationToken cancellationToken = default) + private async Task GetLootDistributionDataAsync(CancellationToken cancellationToken = default) { - if (lootDistribution is { IsCompletedSuccessfully : true, Result: not null }) - { - return await lootDistribution; - } - // TODO: Do not depend on game code; use custom types to map to game JSON files. LootDictionary result = JsonSerializer.Deserialize(await GetJsonAsync(cancellationToken), new JsonSerializerOptions diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs index f606dc8d3c..7caf26f0be 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs @@ -10,6 +10,8 @@ using Nitrox.Server.Subnautica.Models.Helper; using Nitrox.Server.Subnautica.Models.Resources.AddressablesTools.Catalog; using Nitrox.Server.Subnautica.Models.Resources.Core; +using ClassIdByRuntimeKeyDictionary = System.Collections.Generic.Dictionary; +using AddressableCatalogDictionary = System.Collections.Generic.Dictionary; namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; @@ -23,18 +25,17 @@ internal sealed class PrefabPlaceholderGroupsResource(SubnauticaAssetsManager as /// the cache is rebuilt /// /// - private const int CACHE_VERSION = 3; + private const int CACHE_VERSION = 4; + private const string CACHE_FILENAME = "PrefabPlaceholdersGroupAssetsCache.json"; - private readonly ConcurrentDictionary addressableCatalog = new(); private readonly SubnauticaAssetsManager assetsManager = assetsManager; - private readonly ConcurrentDictionary classIdByRuntimeKey = new(); - private readonly ConcurrentDictionary groupsByClassId = new(); private readonly ILogger logger = logger; private readonly IOptions options = options; - private readonly ConcurrentDictionary placeholdersByClassId = []; private readonly TaskCompletionSource resourceLoadFinished = new(); private readonly JsonSerializer serializer = new() { TypeNameHandling = TypeNameHandling.Auto }; + private ConcurrentDictionary groupsByClassId = []; + private ConcurrentDictionary placeholdersByClassId = []; private ConcurrentDictionary randomPossibilitiesByClassId = []; public ConcurrentDictionary GroupsByClassId @@ -62,10 +63,6 @@ public ConcurrentDictionary RandomPossibilitiesByClassId resourceLoadFinished.Task.GetAwaiter().GetResult(); return randomPossibilitiesByClassId; } - private set - { - randomPossibilitiesByClassId = value; - } } public async Task LoadAsync(CancellationToken cancellationToken) @@ -89,7 +86,7 @@ public void PickRandomClassIdIfRequired(ref string classId) } } - private static Dictionary LoadPrefabDatabase(string fullFilename) + private static ClassIdByRuntimeKeyDictionary LoadPrefabDatabase(string fullFilename) { Dictionary prefabFiles = new(); if (!File.Exists(fullFilename)) @@ -122,40 +119,29 @@ private static void GetPrefabGameObjectInfoFromBundle(SubnauticaAssetsManager am prefabGameObjectInfo = assetFileInst.file.Metadata.GetAssetInfo(rootAssetPathId); } - private Task> LoadPrefabsAndSpawnPossibilitiesAsync(CancellationToken cancellationToken = default) + private async Task LoadPrefabsAndSpawnPossibilitiesAsync(CancellationToken cancellationToken = default) { - string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db"); - - // Get all prefab-classIds linked to the (partial) bundle path - Dictionary prefabDatabase = LoadPrefabDatabase(prefabDatabasePath); - cancellationToken.ThrowIfCancellationRequested(); - // Loading all prefabs by their classId and file paths (first the path to the prefab then the dependencies) - LoadAddressableCatalog(options.Value.GetSubnauticaAaResourcePath(), prefabDatabase); cancellationToken.ThrowIfCancellationRequested(); - Dictionary result = CreateOrLoadPrefabCache(options.Value.GetServerCachePath()); + await CreateOrLoadPrefabCacheAsync(options.Value.GetServerCachePath()); cancellationToken.ThrowIfCancellationRequested(); // Select only prefabs with a PrefabPlaceholdersGroups component in the root and link them with their dependencyPaths // Do not remove: the internal cache list is slowing down the process more than loading a few assets again. There maybe is a better way in the new AssetToolsNetVersion but, we need a byte to texture library bc ATNs sub-package is only for netstandard. assetsManager.UnloadAll(true); - // Clear private collections that were used temporarily to parse the files. - addressableCatalog.Clear(); - classIdByRuntimeKey.Clear(); // Get all needed data for the filtered PrefabPlaceholdersGroups to construct PrefabPlaceholdersGroupAssets and add them to the dictionary by classId Validate.IsFalse(randomPossibilitiesByClassId.IsEmpty); - return Task.FromResult(result); } - private Dictionary CreateOrLoadPrefabCache(string nitroxCachePath) + private async Task CreateOrLoadPrefabCacheAsync(string nitroxCachePath) { - Dictionary prefabPlaceholdersGroupPaths = null; + Dictionary prefabPlaceholdersGroupPaths; string cacheFilePath = Path.Combine(nitroxCachePath, CACHE_FILENAME); Cache? cache = null; try { - cache = Cache.Deserialize(serializer, cacheFilePath); + cache = await Cache.DeserializeAsync(serializer, cacheFilePath); } catch (Exception ex) { @@ -169,24 +155,33 @@ private Dictionary CreateOrLoadPrefabCache } prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths; randomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId; + groupsByClassId = cache.Value.GroupsByClassId; + placeholdersByClassId = cache.Value.PlaceholdersByClassId; logger.ZLogDebug($"Successfully loaded cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours."); } // Fallback solution - if (prefabPlaceholdersGroupPaths is null) + else { logger.ZLogInformation($"Building cache, this may take a while..."); - prefabPlaceholdersGroupPaths = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager))); - Cache.Serialize(serializer, new Cache(CACHE_VERSION, prefabPlaceholdersGroupPaths, randomPossibilitiesByClassId), cacheFilePath); + // Get all prefab-classIds linked to the (partial) bundle path + string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db"); + Dictionary prefabDatabase = LoadPrefabDatabase(prefabDatabasePath); + (AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) = LoadAddressableCatalog(options.Value.GetSubnauticaAaResourcePath(), prefabDatabase); + prefabPlaceholdersGroupPaths = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager, addressableCatalog, classIdByRuntimeKey), addressableCatalog, classIdByRuntimeKey)); + await Cache.SerializeAsync(serializer, new Cache(CACHE_VERSION, prefabPlaceholdersGroupPaths, randomPossibilitiesByClassId, groupsByClassId, placeholdersByClassId), cacheFilePath); logger.ZLogDebug( $"Successfully built cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours. Future server starts will take less time."); } Validate.IsTrue(prefabPlaceholdersGroupPaths.Count > 0); Validate.IsTrue(randomPossibilitiesByClassId.Count > 0); - return prefabPlaceholdersGroupPaths; + Validate.IsTrue(groupsByClassId.Count > 0); + Validate.IsTrue(placeholdersByClassId.Count > 0); } - private void LoadAddressableCatalog(string aaRootPath, Dictionary prefabDatabase) + private (AddressableCatalogDictionary, ClassIdByRuntimeKeyDictionary) LoadAddressableCatalog(string aaRootPath, Dictionary prefabDatabase) { + ClassIdByRuntimeKeyDictionary classIdByRuntimeKey = []; + AddressableCatalogDictionary addressableCatalog = []; ContentCatalogData ccd = ContentCatalogData.FromJson(File.ReadAllText(Path.Combine(aaRootPath, "catalog.json"))); Dictionary classIdByPath = prefabDatabase.ToDictionary(m => m.Value, m => m.Key); @@ -216,13 +211,15 @@ private void LoadAddressableCatalog(string aaRootPath, Dictionary /// Gathers bundle paths by class id for prefab placeholder groups. /// Also fills /// - private ConcurrentDictionary GetAllPrefabPlaceholdersGroupsFast(SubnauticaAssetsManager am) + private ConcurrentDictionary GetAllPrefabPlaceholdersGroupsFast(SubnauticaAssetsManager am, AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) { // First step is to find out about the hash of the types PrefabPlaceholdersGroup and SpawnRandom // to be able to recognize them easily later on @@ -313,7 +310,8 @@ private ConcurrentDictionary GetAllPrefabPlaceholdersGroupsFas return prefabPlaceholdersGroupPaths; } - private ConcurrentDictionary GetPrefabPlaceholderGroupAssetsByGroupClassId(SubnauticaAssetsManager am, ConcurrentDictionary prefabPlaceholdersGroupPaths) + private ConcurrentDictionary GetPrefabPlaceholderGroupAssetsByGroupClassId(SubnauticaAssetsManager am, ConcurrentDictionary prefabPlaceholdersGroupPaths, + AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) { ConcurrentDictionary prefabPlaceholderGroupsByGroupClassId = new(); @@ -323,7 +321,7 @@ private ConcurrentDictionary GetPrefabPlac SubnauticaAssetsManager amInnerClone = amClone.Clone(); AssetsFileInstance assetFileInst = amInnerClone.LoadBundleWithDependencies(keyValuePair.Value); - PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(amInnerClone, assetFileInst, keyValuePair.Key); + PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(amInnerClone, assetFileInst, keyValuePair.Key, addressableCatalog, classIdByRuntimeKey); amInnerClone.UnloadAll(); if (!prefabPlaceholderGroupsByGroupClassId.TryAdd(keyValuePair.Key, prefabPlaceholderGroup)) @@ -334,13 +332,15 @@ private ConcurrentDictionary GetPrefabPlac return prefabPlaceholderGroupsByGroupClassId; } - private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, string classId) + private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, string classId, AddressableCatalogDictionary addressableCatalog, + ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) { GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo); - return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId); + return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId, addressableCatalog, classIdByRuntimeKey); } - private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId) + private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(SubnauticaAssetsManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId, AddressableCatalogDictionary addressableCatalog, + ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) { if (!string.IsNullOrEmpty(classId) && groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup)) { @@ -368,7 +368,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub AssetTypeValueField gameObjectPtr = prefabPlaceholder["m_GameObject"]; AssetTypeValueField gameObjectField = amInst.GetExtAsset(assetFileInst, gameObjectPtr).baseField; - IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString); + IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString, addressableCatalog, classIdByRuntimeKey); bool isEntitySlotAsset = asset is PrefabPlaceholderAsset prefabPlaceholderAsset && prefabPlaceholderAsset.EntitySlot.HasValue; NitroxTransform transform = amInst.GetTransformFromGameObject(assetFileInst, gameObjectField, rootGameObjectName, isEntitySlotAsset); string prefabAssetClassId = prefabPlaceholder["prefabClassId"].AsString; @@ -388,7 +388,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub return prefabPlaceholdersGroup; } - private IPrefabAsset? GetAndCacheAsset(SubnauticaAssetsManager am, string classId) + private IPrefabAsset? GetAndCacheAsset(SubnauticaAssetsManager am, string classId, AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) { if (string.IsNullOrEmpty(classId)) { @@ -415,7 +415,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub AssetFileInfo placeholdersGroupInfo = am.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "PrefabPlaceholdersGroup"); if (placeholdersGroupInfo != null) { - PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(am, assetFileInst, classId); + PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(am, assetFileInst, classId, addressableCatalog, classIdByRuntimeKey); groupsByClassId[classId] = groupAsset; return groupAsset; } @@ -469,24 +469,30 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub return prefabPlaceholderAsset; } - private record struct Cache(int Version, Dictionary PrefabPlaceholdersGroupPaths, ConcurrentDictionary RandomPossibilitiesByClassId) + private record struct Cache( + int Version, + Dictionary PrefabPlaceholdersGroupPaths, + ConcurrentDictionary RandomPossibilitiesByClassId, + ConcurrentDictionary GroupsByClassId, + ConcurrentDictionary PlaceholdersByClassId + ) { - public static void Serialize(JsonSerializer serializer, Cache cache, string filePath) + public static async Task SerializeAsync(JsonSerializer serializer, Cache cache, string filePath) { Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? throw new Exception("Failed to get directory path from cache file path")); - using StreamWriter stream = File.CreateText(filePath); + await using StreamWriter stream = File.CreateText(filePath); serializer.Serialize(stream, cache); } - public static Cache? Deserialize(JsonSerializer serializer, string filePath) + public static Task DeserializeAsync(JsonSerializer serializer, string filePath) { if (!File.Exists(filePath)) { - return null; + return Task.FromResult(null); } Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? throw new Exception("Failed to get directory path from cache file path")); using StreamReader reader = File.OpenText(filePath); - return (Cache?)serializer.Deserialize(reader, typeof(Cache)); + return Task.FromResult((Cache?)serializer.Deserialize(reader, typeof(Cache))); } } } From fbd5cafc69e90d029a0bd2e2b0d8cc379c03a01f Mon Sep 17 00:00:00 2001 From: Meas <1107063+Measurity@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:56:35 +0100 Subject: [PATCH 10/59] Asyncify command and packet handling (#2656) Co-authored-by: Coding-Hen <8798074+Coding-Hen@users.noreply.github.com> --- Nitrox.Launcher/Models/Design/ServerEntry.cs | 3 +- .../Models/Design/StaticCommands.cs | 8 +- .../FileCacheDelegatingHandler.cs | 2 +- .../Models/Services/StorageService.cs | 6 +- .../Styles/Theme/RadioButtonGroupStyle.axaml | 8 +- .../ViewModels/ManageServerViewModel.cs | 2 +- .../Entities/EscapePodWorldEntity.cs | 5 +- .../GameLogic/Entities/GlobalRootEntity.cs | 2 +- .../GameLogic/Entities/IUwePrefabFactory.cs | 3 +- .../Entities/IUweWorldEntityFactory.cs | 6 - .../GameLogic/Entities/InventoryItemEntity.cs | 2 +- .../GameLogic/Entities/OxygenPipeEntity.cs | 2 +- .../GameLogic/Entities/PrefabChildEntity.cs | 2 +- .../Entities/SerializedWorldEntity.cs | 2 +- .../GameLogic/Entities/WorldEntity.cs | 4 +- .../DataStructures/GameLogic/Entity.cs | 2 +- .../GameLogic/RandomStartGenerator.cs | 45 +- .../Extensions/SunbeamEventExtensions.cs | 17 + .../MultiplayerSession/PlayerContext.cs | 15 +- .../Packets/AnimationChangeEvent.cs | 13 +- .../Packets/BenchChanged.cs | 7 +- .../Packets/BuildingResyncRequest.cs | 12 +- .../Packets/CellVisibilityChanged.cs | 7 +- .../Packets/ChatMessage.cs | 23 +- .../Packets/DebugStartMapPacket.cs | 16 +- Nitrox.Model.Subnautica/Packets/Disconnect.cs | 17 +- .../Packets/EscapePodChanged.cs | 7 +- .../Packets/FootstepPacket.cs | 7 +- .../Packets/GameModeChanged.cs | 15 +- .../Packets/KnownTechEntryAdd.cs | 4 +- Nitrox.Model.Subnautica/Packets/Movement.cs | 3 +- .../Packets/MultiplayerSessionReservation.cs | 38 +- Nitrox.Model.Subnautica/Packets/MutePlayer.cs | 7 +- .../Packets/PDAScanFinished.cs | 4 +- .../Packets/PieceDeconstructed.cs | 2 +- .../Packets/PlaySunbeamEvent.cs | 53 ++- .../Packets/PlayerCinematicControllerCall.cs | 7 +- .../Packets/PlayerDeathEvent.cs | 7 +- .../Packets/PlayerHeldItemChanged.cs | 44 +- .../Packets/PlayerInCyclopsMovement.cs | 7 +- .../Packets/PlayerMovement.cs | 38 +- .../Packets/PlayerStats.cs | 7 +- Nitrox.Model.Subnautica/Packets/PvPAttack.cs | 7 +- .../Packets/SetIntroCinematicMode.cs | 13 +- .../Packets/SimulationOwnershipChange.cs | 5 +- .../Packets/SimulationOwnershipRequest.cs | 26 +- .../Packets/SpawnEntities.cs | 2 +- .../Packets/StasisSphereHit.cs | 7 +- .../Packets/StasisSphereShot.cs | 7 +- .../Packets/SubRootChanged.cs | 22 +- .../Packets/VehicleDocking.cs | 16 +- .../Packets/VehicleOnPilotModeChanged.cs | 7 +- .../Packets/VehicleUndocking.cs | 7 +- .../Configuration/ServerStartOptions.cs | 1 - .../Configuration/SubnauticaServerOptions.cs | 1 + Nitrox.Model/Constants/NitroxConstants.cs | 1 + .../Constants/SubnauticaServerConstants.cs | 9 +- Nitrox.Model/Core/PeerId.cs | 27 ++ Nitrox.Model/Core/SessionId.cs | 40 ++ .../DataStructures/GameLogic/Perms.cs | 71 ++-- Nitrox.Model/DataStructures/NitroxId.cs | 6 +- Nitrox.Model/DataStructures/Optional.cs | 6 +- .../DataStructures/SimulatedEntity.cs | 48 +-- .../DataStructures/Unity/NitroxTransform.cs | 4 +- Nitrox.Model/Extensions/StringExtensions.cs | 20 +- Nitrox.Model/GlobalUsings.cs | 1 + Nitrox.Model/Helper/AsyncBarrier.cs | 52 --- Nitrox.Model/Helper/Validate.cs | 9 +- .../Packets/Core/IPacketProcessContext.cs | 11 + Nitrox.Model/Packets/Core/IPacketProcessor.cs | 20 + .../Packets/Core/PacketProcessorsInvoker.cs | 139 ++++++ Nitrox.Model/Packets/CorrelatedPacket.cs | 17 +- Nitrox.Model/Packets/Packet.cs | 20 +- .../Processors/Abstract/IProcessorContext.cs | 6 - .../Processors/Abstract/PacketProcessor.cs | 43 -- Nitrox.Model/Packets/TextAutoComplete.cs | 20 + .../Extensions/LoggerExtensions.cs | 53 ++- .../Extensions/LoggingBuilderExtensions.cs | 80 +++- .../Extensions/ServiceCollectionExtensions.cs | 396 +++++++++--------- .../Administration/Core/IAdminFeature.cs | 5 + .../Models/Administration/IKickPlayer.cs | 9 + .../Models/AppEvents/ISaveState.cs | 15 + .../Models/AppEvents/ISessionCleaner.cs | 12 + .../Models/AppEvents/Triggers/AsyncTrigger.cs | 27 ++ .../Models/Commands/Abstract/CallArgs.cs | 63 --- .../Models/Commands/Abstract/Command.cs | 139 ------ .../Models/Commands/Abstract/Parameter.cs | 41 -- .../Commands/Abstract/Type/TypeBoolean.cs | 39 -- .../Models/Commands/Abstract/Type/TypeEnum.cs | 26 -- .../Commands/Abstract/Type/TypeFloat.cs | 23 - .../Models/Commands/Abstract/Type/TypeInt.cs | 23 - .../Commands/Abstract/Type/TypeNitroxId.cs | 22 - .../Commands/Abstract/Type/TypePlayer.cs | 26 -- .../Commands/Abstract/Type/TypeString.cs | 18 - .../ArgConverters/Core/ConvertResult.cs | 17 + .../ArgConverters/Core/IArgConverter.cs | 16 + .../PlayerNameToPlayerArgConverter.cs | 21 + .../SessionIdToPlayerArgConverter.cs | 26 ++ .../ArgConverters/WordToBoolArgConverter.cs | 17 + .../Models/Commands/AuroraCommand.cs | 49 ++- .../Models/Commands/AutosaveCommand.cs | 37 +- .../Models/Commands/BackCommand.cs | 42 +- .../Models/Commands/BroadcastCommand.cs | 31 +- .../Commands/ChangeAdminPasswordCommand.cs | 39 +- .../Commands/ChangeServerGamemodeCommand.cs | 47 +-- .../Commands/ChangeServerPasswordCommand.cs | 41 +- .../Models/Commands/ConfigCommand.cs | 75 +--- .../Models/Commands/Core/AliasAttribute.cs | 7 + .../Commands/Core/CommandHandlerEntry.cs | 166 ++++++++ .../Models/Commands/Core/CommandOrigin.cs | 10 + .../Models/Commands/Core/CommandRegistry.cs | 261 ++++++++++++ .../Core/HostToServerCommandContext.cs | 85 ++++ .../Models/Commands/Core/ICommandContext.cs | 40 ++ .../Models/Commands/Core/ICommandHandler.cs | 28 ++ .../Models/Commands/Core/ICommandSubmit.cs | 12 + .../Core/PlayerToServerCommandContext.cs | 80 ++++ .../Models/Commands/Core/RequiresOrigin.cs | 10 + .../Core/RequiresPermissionAttribute.cs | 12 + .../Models/Commands/DebugStartMapCommand.cs | 36 -- .../Debugging/DebugStartMapCommand.cs | 28 ++ .../Commands/Debugging/LoadBatchCommand.cs | 26 ++ .../Commands/Debugging/PlayerCommand.cs | 59 +++ .../Models/Commands/Debugging/QueryCommand.cs | 46 ++ .../Debugging/SeedNearPositionCommand.cs | 73 ++++ .../Models/Commands/DeopCommand.cs | 48 ++- .../Models/Commands/DirectoryCommand.cs | 81 ++-- .../Models/Commands/FastCommand.cs | 73 ++-- .../Models/Commands/GameModeCommand.cs | 70 ++-- .../Models/Commands/HelpCommand.cs | 123 ++++-- .../Models/Commands/KickCommand.cs | 70 ++-- .../Models/Commands/ListCommand.cs | 34 +- .../Models/Commands/LoadBatchCommand.cs | 34 -- .../Models/Commands/LoginCommand.cs | 51 ++- .../Models/Commands/MuteCommand.cs | 63 ++- .../Models/Commands/OpCommand.cs | 33 +- .../Models/Commands/PlayerCommand.cs | 66 --- .../Processor/TextCommandProcessor.cs | 78 ---- .../Models/Commands/PromoteCommand.cs | 56 ++- .../Models/Commands/PvpCommand.cs | 43 +- .../Models/Commands/QueryCommand.cs | 53 --- .../Models/Commands/SaveCommand.cs | 30 +- .../Commands/SetKeepInventoryCommand.cs | 38 +- .../Models/Commands/StopCommand.cs | 13 +- .../Models/Commands/SummaryCommand.cs | 46 +- .../Models/Commands/SunbeamCommand.cs | 49 +-- .../Models/Commands/SwapSerializerCommand.cs | 38 -- .../Models/Commands/TeleportCommand.cs | 36 +- .../Models/Commands/TimeCommand.cs | 35 +- .../Models/Commands/UnmuteCommand.cs | 64 ++- .../Models/Commands/WarpCommand.cs | 51 +-- .../Models/Commands/WhisperCommand.cs | 32 +- .../Models/Commands/WhoisCommand.cs | 42 +- .../Communication/LiteNetLibConnection.cs | 79 ---- .../Models/Communication/LiteNetLibServer.cs | 351 ++++++++++++---- .../Models/Communication/NitroxConnection.cs | 13 - .../Models/Communication/SessionManager.cs | 125 ++++++ .../Models/Factories/RandomFactory.cs | 29 ++ .../Models/GameLogic/Bases/BuildingManager.cs | 10 +- .../GameLogic/Entities/EntityRegistry.cs | 13 +- .../GameLogic/Entities/EntitySimulation.cs | 112 ++--- .../Entities/Spawning/BatchEntitySpawner.cs | 120 +++--- .../Spawning/CrashHomeBootstrapper.cs | 2 +- .../Entities/Spawning/GeyserBootstrapper.cs | 6 +- .../Entities/Spawning/IEntityBootstrapper.cs | 2 +- .../Spawning/IEntityBootstrapperManager.cs | 2 +- .../Entities/Spawning/IEntitySpawner.cs | 11 - .../Entities/Spawning/ReefbackBootstrapper.cs | 30 +- .../StayAtLeashPositionBootstrapper.cs | 2 +- .../SubnauticaEntityBootstrapperManager.cs | 18 +- .../Entities/SubnauticaUwePrefabFactory.cs | 34 +- .../SubnauticaUweWorldEntityFactory.cs | 16 +- .../GameLogic/Entities/WorldEntityManager.cs | 15 +- .../Models/GameLogic/EscapePodManager.cs | 57 +-- .../Models/GameLogic/GameData.cs | 2 +- .../Models/GameLogic/JoiningManager.cs | 132 +++--- .../Models/GameLogic/PdaManager.cs | 184 ++++++-- .../Models/GameLogic/PlayerManager.cs | 177 ++++---- .../GameLogic/Players/PersistedPlayerData.cs | 7 +- .../Models/GameLogic/Players/PlayerData.cs | 2 +- .../Models/GameLogic/SimulationOwnership.cs | 11 +- .../Models/GameLogic/SleepManager.cs | 80 ++-- .../Models/GameLogic/StoryManager.cs | 13 +- .../Models/GameLogic/StoryScheduler.cs | 6 +- .../Models/GameLogic/TimeService.cs | 16 +- .../GameLogic/Unlockables/PdaStateData.cs | 39 +- .../Models/Helper/DeterministicGenerator.cs | 48 +-- .../Models/Helper/EasyPool.cs | 19 + .../Models/Helper/XorRandom.cs | 122 +++--- .../WriteRedactedLogLoggerMiddleware.cs | 2 +- .../Logging/Redaction/Core/IRedactor.cs | 2 +- .../Packets/Core/AnonProcessorContext.cs | 24 ++ .../Packets/Core/AuthProcessorContext.cs | 27 ++ .../Packets/Core/IAnonPacketProcessor.cs | 11 + .../Packets/Core/IAuthPacketProcessor.cs | 11 + .../Models/Packets/Core/IPacketSender.cs | 13 + .../Models/Packets/Core/NopPacketSender.cs | 12 + .../Models/Packets/PacketHandler.cs | 89 ---- ...AggressiveWhenSeeTargetChangedProcessor.cs | 13 +- .../AnimationChangeEventProcessor.cs | 19 +- .../AttackCyclopsTargetChangedProcessor.cs | 13 +- .../Processors/BaseDeconstructedProcessor.cs | 12 +- .../Packets/Processors/BedEnterProcessor.cs | 16 +- .../Packets/Processors/BedExitProcessor.cs | 16 +- .../Packets/Processors/BuildingProcessor.cs | 35 +- .../BuildingResyncRequestProcessor.cs | 18 +- .../CellVisibilityChangedProcessor.cs | 30 +- .../Processors/ChatMessageProcessor.cs | 34 +- .../Processors/CheatCommandProcessor.cs | 12 +- .../Processors/ClearPlanterProcessor.cs | 9 +- .../Core/AuthenticatedPacketProcessor.cs | 14 - .../Core/TransmitIfCanSeePacketProcessor.cs | 22 +- .../Core/UnauthenticatedPacketProcessor.cs | 15 - .../CreatureActionChangedProcessor.cs | 8 +- .../CreaturePoopPerformedProcessor.cs | 8 +- .../CyclopsDamagePointRepairedProcessor.cs | 21 +- .../Processors/CyclopsDamageProcessor.cs | 33 +- .../Processors/CyclopsFireCreatedProcessor.cs | 21 +- .../DefaultServerPacketProcessor.cs | 52 +-- .../Processors/DiscordRequestIPProcessor.cs | 24 +- .../EntityDestroyedPacketProcessor.cs | 23 +- .../EntityMetadataUpdateProcessor.cs | 30 +- .../Processors/EntityReparentedProcessor.cs | 10 +- .../EntitySpawnedByClientProcessor.cs | 66 ++- .../EntityTransformUpdatesProcessor.cs | 105 +++-- .../EscapePodChangedPacketProcessor.cs | 14 +- .../Packets/Processors/FMODAssetProcessor.cs | 10 +- .../Processors/FMODEventInstanceProcessor.cs | 25 +- .../Packets/Processors/FireDousedProcessor.cs | 21 +- .../Processors/FootstepPacketProcessor.cs | 24 +- .../Processors/GoalCompletedProcessor.cs | 8 +- .../Processors/KnownTechEntryAddProcessor.cs | 38 +- .../LargeWaterParkDeconstructedProcessor.cs | 14 +- .../Processors/LeakRepairedProcessor.cs | 18 +- .../ModifyConstructedAmountProcessor.cs | 12 +- ...ultiplayerSessionPolicyRequestProcessor.cs | 31 +- ...layerSessionReservationRequestProcessor.cs | 48 +-- .../PDAEncyclopediaEntryAddProcessor.cs | 28 +- .../Processors/PDALogEntryAddProcessor.cs | 33 +- .../PDAScanFinishedPacketProcessor.cs | 20 +- .../Processors/PickupItemPacketProcessor.cs | 32 +- .../Processors/PieceDeconstructedProcessor.cs | 12 +- .../Processors/PinnedRecipeMovedProcessor.cs | 11 +- .../Packets/Processors/PlaceBaseProcessor.cs | 14 +- .../Packets/Processors/PlaceGhostProcessor.cs | 12 +- .../Processors/PlaceModuleProcessor.cs | 14 +- .../Processors/PlayerDeathEventProcessor.cs | 46 +- .../PlayerHeldItemChangedProcessor.cs | 25 +- .../PlayerInCyclopsMovementProcessor.cs | 40 +- ...layerJoiningMultiplayerSessionProcessor.cs | 17 +- .../Processors/PlayerMovementProcessor.cs | 37 +- ...PlayerQuickSlotsBindingChangedProcessor.cs | 14 +- .../PlayerSeeOutOfCellEntityProcessor.cs | 16 +- .../Processors/PlayerStatsProcessor.cs | 26 +- .../Processors/PlayerSyncFinishedProcessor.cs | 37 +- .../PlayerUnseeOutOfCellEntityProcessor.cs | 35 +- .../Packets/Processors/PvPAttackProcessor.cs | 21 +- .../RadioPlayPendingMessageProcessor.cs | 36 +- .../RangedAttackLastTargetUpdateProcessor.cs | 7 +- .../Processors/RecipePinnedProcessor.cs | 13 +- .../RemoveCreatureCorpseProcessor.cs | 24 +- .../Packets/Processors/ScheduleProcessor.cs | 27 +- .../SeaDragonAttackTargetProcessor.cs | 3 +- .../SeaDragonGrabExosuitProcessor.cs | 3 +- .../SeaDragonSwatAttackProcessor.cs | 3 +- .../SeaTreaderSpawnedChunkProcessor.cs | 3 +- .../Processors/ServerCommandProcessor.cs | 41 +- .../SetIntroCinematicModeProcessor.cs | 30 +- .../SignalPingPreferenceChangedProcessor.cs | 9 +- .../SimulationOwnershipRequestProcessor.cs | 26 +- .../Processors/StoryGoalExecutedProcessor.cs | 30 +- .../SubRootChangedPacketProcessor.cs | 29 +- .../Processors/TextAutoCompleteProcessor.cs | 29 ++ .../Packets/Processors/UpdateBaseProcessor.cs | 14 +- .../UpdateDisplaySurfaceWaterProcessor.cs | 9 +- .../Processors/UpdateInPrecursorProcessor.cs | 9 +- .../Processors/VehicleDockingProcessor.cs | 15 +- .../VehicleMovementsPacketProcessor.cs | 36 +- .../VehicleOnPilotModeChangedProcessor.cs | 19 +- .../Processors/VehicleUndockingProcessor.cs | 21 +- .../WaterParkDeconstructedProcessor.cs | 14 +- .../Packets/Processors/WeldActionProcessor.cs | 34 +- Nitrox.Server.Subnautica/Models/Player.cs | 59 ++- .../Parsers/EntityDistributionsResource.cs | 11 +- .../PrefabPlaceholderGroupsResource.cs | 6 +- .../Resources/Parsers/RandomStartResource.cs | 13 +- .../Parsers/WorldEntitiesResource.cs | 14 +- .../Serialization/Json/PeerIdConverter.cs | 25 ++ .../Serialization/ServerJsonSerializer.cs | 3 +- .../Serialization/ServerProtoBufSerializer.cs | 2 +- .../Models/Serialization/World/WorldData.cs | 4 +- .../Serialization/World/WorldService.cs | 39 +- Nitrox.Server.Subnautica/Program.cs | 26 +- .../Services/AutoSaveService.cs | 2 +- .../Services/CommandService.cs | 302 +++++++++++-- .../Services/ConsoleInputService.cs | 10 +- .../Services/Core/QueingBackgroundService.cs | 23 + .../Services/FmodService.cs | 2 +- .../Services/HibernateService.cs | 6 +- .../Services/LanBroadcastService.cs | 2 +- .../Services/MemoryService.cs | 30 +- .../Services/PacketRegistryService.cs | 25 ++ .../Services/PacketSerializationService.cs | 86 ++++ .../PersistNitroxSerializableConfigService.cs | 25 ++ .../Services/SaveService.cs | 51 +-- .../Services/ServersManagementService.cs | 64 +-- .../Services/StatusService.cs | 49 ++- .../SubnauticaResourceLoaderService.cs | 2 +- .../DeferredPacketReceiverTest.cs | 2 +- .../Communication/TestNonActionPacket.cs | 19 +- .../Helper/Faker/NitroxAbstractFaker.cs | 9 +- .../Helper/Faker/NitroxCollectionFaker.cs | 160 +++---- Nitrox.Test/Helper/Faker/NitroxFaker.cs | 68 ++- .../Model/Packets/PacketsSerializableTest.cs | 2 +- .../Packets/Processors/PacketProcessorTest.cs | 101 ++--- .../{XORRandomTest.cs => XorRandomTest.cs} | 6 +- .../Server/Serialization/WorldServiceTest.cs | 1 - NitroxClient/ClientAutoFacRegistrar.cs | 10 +- NitroxClient/Communication/PacketReceiver.cs | 5 +- .../Abstract/ClientPacketProcessor.cs | 16 - .../Abstract/KeepInventoryChangedProcessor.cs | 20 - ...AggressiveWhenSeeTargetChangedProcessor.cs | 10 +- .../AnimationChangeEventProcessor.cs | 21 +- .../AttackCyclopsTargetChangedProcessor.cs | 10 +- .../AuroraAndTimeUpdateProcessor.cs | 17 +- .../Processors/BenchChangedProcessor.cs | 27 +- .../Packets/Processors/BuildProcessor.cs | 27 +- .../BuildingDesyncWarningProcessor.cs | 16 +- .../Processors/BuildingResyncProcessor.cs | 93 ++-- .../Processors/ChatMessageProcessor.cs | 105 +++-- .../Processors/CoffeeMachineUseProcessor.cs | 22 +- .../Processors/Core/ClientProcessorContext.cs | 17 + .../Processors/Core/IClientPacketProcessor.cs | 8 + .../Processors/CreatureActionProcessor.cs | 17 +- .../CreaturePoopPerformedProcessor.cs | 10 +- ...yclopsDamagePointHealthChangedProcessor.cs | 32 +- .../Processors/CyclopsDamageProcessor.cs | 280 ++++++------- .../Processors/CyclopsDecoyLaunchProcessor.cs | 28 +- .../Processors/CyclopsFireCreatedProcessor.cs | 28 +- .../CyclopsFireSuppressionProcessor.cs | 28 +- .../Processors/DebugStartMapProcessor.cs | 28 +- .../DeconstructionBeginProcessor.cs | 50 +-- .../Packets/Processors/DisconnectProcessor.cs | 45 +- .../Processors/DiscordRequestIPProcessor.cs | 10 +- .../DropSimulationOwnershipProcessor.cs | 17 +- .../Processors/EntityDestroyedProcessor.cs | 23 +- .../EntityMetadataUpdateProcessor.cs | 24 +- .../Processors/EntityReparentedProcessor.cs | 27 +- .../EntityTransformUpdatesProcessor.cs | 21 +- .../Processors/EscapePodChangedProcessor.cs | 40 +- .../Processors/ExosuitArmActionProcessor.cs | 11 +- .../Packets/Processors/FMODAssetProcessor.cs | 22 +- .../Processors/FMODCustomEmitterProcessor.cs | 12 +- .../FMODCustomLoopingEmitterProcessor.cs | 12 +- .../Processors/FMODEventInstanceProcessor.cs | 12 +- .../FMODStudioEventEmitterProcessor.cs | 12 +- .../Processors/FastCheatChangedProcessor.cs | 7 +- .../Packets/Processors/FireDousedProcessor.cs | 41 +- .../Processors/FootstepPacketProcessor.cs | 20 +- .../Processors/GameModeChangedProcessor.cs | 24 +- .../GrapplingHookMovementProcessor.cs | 9 +- .../Processors/InitialPlayerSyncProcessor.cs | 160 ++++--- .../Processors/ItemPositionProcessor.cs | 26 +- .../Processors/JoinQueueInfoProcessor.cs | 11 +- .../KeepInventoryChangedProcessor.cs | 16 + .../Processors/KnownTechEntryAddProcessor.cs | 18 +- .../Processors/LeakRepairedProcessor.cs | 11 +- .../MedicalCabinetClickedProcessor.cs | 10 +- .../MultiplayerSessionPolicyProcessor.cs | 30 +- .../MultiplayerSessionReservationProcessor.cs | 28 +- .../Packets/Processors/MutePlayerProcessor.cs | 26 +- .../OpenableStateChangedProcessor.cs | 32 +- .../PDAEncyclopediaEntryAddProcessor.cs | 17 +- .../Processors/PDALogEntryAddProcessor.cs | 28 +- .../Processors/PDAScanFinishedProcessor.cs | 15 +- .../Processors/PermsChangedProcessor.cs | 19 +- .../Processors/PlaySunbeamEventProcessor.cs | 9 +- .../PlayerCinematicControllerCallProcessor.cs | 25 +- .../Processors/PlayerDeathProcessor.cs | 21 +- .../PlayerHeldItemChangedProcessor.cs | 25 +- .../PlayerInCyclopsMovementProcessor.cs | 20 +- ...PlayerJoinedMultiplayerSessionProcessor.cs | 20 +- .../Processors/PlayerKickedProcessor.cs | 36 +- .../Processors/PlayerMovementProcessor.cs | 20 +- .../Processors/PlayerStatsProcessor.cs | 19 +- .../Processors/PlayerTeleportedProcessor.cs | 19 +- .../Packets/Processors/PvPAttackProcessor.cs | 8 +- .../RadioPlayPendingMessageProcessor.cs | 17 +- .../RangedAttackLastTargetUpdateProcessor.cs | 10 +- .../RemoveCreatureCorpseProcessor.cs | 70 ++-- .../Processors/RocketLaunchProcessor.cs | 18 +- .../Packets/Processors/ScheduleProcessor.cs | 8 +- .../SeaDragonAttackTargetProcessor.cs | 19 +- .../SeaDragonGrabExosuitProcessor.cs | 12 +- .../SeaDragonSwatAttackProcessor.cs | 12 +- .../SeaTreaderChunkPickedUpProcessor.cs | 12 +- .../SeaTreaderSpawnedChunkProcessor.cs | 11 +- .../SeamothModuleActionProcessor.cs | 15 +- .../Processors/ServerStoppedProcessor.cs | 19 +- .../SetIntroCinematicModeProcessor.cs | 33 +- .../SimulationOwnershipChangeProcessor.cs | 18 +- .../SimulationOwnershipResponseProcessor.cs | 69 ++- .../Processors/SleepCompleteProcessor.cs | 16 +- .../Processors/SleepStatusUpdateProcessor.cs | 16 +- .../Processors/SpawnEntitiesProcessor.cs | 33 +- .../Processors/StasisSphereHitProcessor.cs | 20 +- .../Processors/StasisSphereShotProcessor.cs | 20 +- .../StoryGoalExecutedClientProcessor.cs | 18 +- .../Processors/SubRootChangedProcessor.cs | 40 +- .../Processors/TextAutoCompleteProcessor.cs | 25 ++ .../Packets/Processors/TimeChangeProcessor.cs | 17 +- .../Processors/ToggleLightsProcessor.cs | 12 +- .../Packets/Processors/TorpedoHitProcessor.cs | 18 +- .../Processors/TorpedoShotProcessor.cs | 18 +- .../TorpedoTargetAcquiredProcessor.cs | 18 +- .../Processors/VehicleDockingProcessor.cs | 38 +- .../Processors/VehicleMovementsProcessor.cs | 12 +- .../VehicleOnPilotModeChangedProcessor.cs | 24 +- .../Processors/VehicleUndockingProcessor.cs | 40 +- .../Packets/Processors/WeldActionProcessor.cs | 48 +-- NitroxClient/GameLogic/AI.cs | 3 +- NitroxClient/GameLogic/Bases/BuildUtils.cs | 1 - .../GameLogic/Bases/BuildingHandler.cs | 1 - .../GameLogic/Bases/GhostMetadataApplier.cs | 1 - .../GameLogic/Bases/GhostMetadataRetriever.cs | 1 - NitroxClient/GameLogic/BulletManager.cs | 35 +- .../GameLogic/ChatUI/PlayerChatManager.cs | 15 +- NitroxClient/GameLogic/Cyclops.cs | 2 - NitroxClient/GameLogic/Entities.cs | 4 +- NitroxClient/GameLogic/FMOD/FMODSystem.cs | 1 - NitroxClient/GameLogic/Fires.cs | 1 - .../HUD/PdaTabs/uGUI_PlayerListTab.cs | 15 +- .../HUD/PdaTabs/uGUI_PlayerPingEntry.cs | 4 +- .../GameLogic/HUD/PlayerVitalsManager.cs | 17 +- .../Helper/TransientLocalObjectManager.cs | 77 ++-- .../Abstract/InitialSyncProcessor.cs | 1 - .../EquippedItemInitialSyncProcessor.cs | 1 - .../GlobalRootInitialSyncProcessor.cs | 6 +- .../InitialSync/PdaInitialSyncProcessor.cs | 2 - .../PlayerPositionInitialSyncProcessor.cs | 2 - .../PlayerPreferencesInitialSyncProcessor.cs | 1 - .../QuickSlotInitialSyncProcessor.cs | 1 - .../RemotePlayerInitialSyncProcessor.cs | 2 - ...SimulationOwnershipInitialSyncProcessor.cs | 1 - .../StoryGoalInitialSyncProcessor.cs | 1 - NitroxClient/GameLogic/Interior.cs | 1 - NitroxClient/GameLogic/Items.cs | 15 +- NitroxClient/GameLogic/LocalPlayer.cs | 44 +- NitroxClient/GameLogic/MedkitFabricator.cs | 1 - NitroxClient/GameLogic/MobileVehicleBay.cs | 1 - NitroxClient/GameLogic/NitroxConsole.cs | 1 - .../GameLogic/PlayerLogic/PlayerCinematics.cs | 18 +- .../PlayerModel/Abstract/INitroxPlayer.cs | 3 +- .../ColorSwap/DiveSuitColorSwapManager.cs | 1 - .../ColorSwap/FinColorSwapManager.cs | 1 - .../RadiationHelmetColorSwapManager.cs | 1 - .../RadiationSuitColorSwapManager.cs | 1 - .../RadiationSuitVestColorSwapManager.cs | 1 - .../RadiationTankColorSwapManager.cs | 1 - .../ColorSwap/RebreatherColorSwapManager.cs | 1 - .../ReinforcedSuitColorSwapManager.cs | 1 - .../ColorSwap/ScubaTankColorSwapManager.cs | 1 - .../ColorSwap/StillSuitColorSwapManager.cs | 1 - .../PlayerModel/PlayerModelManager.cs | 1 - .../PlayerPreferenceManager.cs | 1 - NitroxClient/GameLogic/PlayerManager.cs | 38 +- NitroxClient/GameLogic/RemotePlayer.cs | 12 +- NitroxClient/GameLogic/Rockets.cs | 1 - NitroxClient/GameLogic/SeamothModulesEvent.cs | 2 - NitroxClient/GameLogic/SimulationOwnership.cs | 5 +- .../Spawning/Bases/BaseLeakEntitySpawner.cs | 1 - .../Spawning/Bases/BuildEntitySpawner.cs | 1 - .../Bases/InteriorPieceEntitySpawner.cs | 1 - .../Spawning/EscapePodEntitySpawner.cs | 2 - .../Spawning/InstalledBatteryEntitySpawner.cs | 9 +- .../Spawning/InstalledModuleEntitySpawner.cs | 1 - .../Spawning/InventoryEntitySpawner.cs | 6 +- .../Spawning/InventoryItemEntitySpawner.cs | 9 +- .../Metadata/BeaconMetadataProcessor.cs | 1 - .../Extractor/SeaTreaderMetadataExtractor.cs | 1 - .../SubNameInputMetadataExtractor.cs | 1 - .../Processor/ConstructorMetadataProcessor.cs | 1 - .../Processor/CrafterMetadataProcessor.cs | 1 - .../CyclopsLightingMetadataProcessor.cs | 1 - .../Processor/CyclopsMetadataProcessor.cs | 1 - .../Processor/ExosuitMetadataProcessor.cs | 1 - .../Processor/FruitPlantMetadataProcessor.cs | 1 - .../Processor/PlayerMetadataProcessor.cs | 2 +- .../Processor/RadiationMetadataProcessor.cs | 1 - .../Processor/RocketMetadataProcessor.cs | 1 - .../Processor/SeaTreaderMetadataProcessor.cs | 1 - .../Processor/SeamothMetadataProcessor.cs | 1 - .../StayAtLeashPositionMetadataProcessor.cs | 1 - .../SubNameInputMetadataProcessor.cs | 2 - .../Spawning/PrefabChildEntitySpawner.cs | 6 +- .../WorldEntities/CrashEntitySpawner.cs | 1 - .../CreatureRespawnEntitySpawner.cs | 1 - .../DefaultWorldEntitySpawner.cs | 1 - .../WorldEntities/GeyserWorldEntitySpawner.cs | 1 - .../WorldEntities/GlobalRootEntitySpawner.cs | 2 - .../WorldEntities/PlacedWorldEntitySpawner.cs | 10 +- .../PlaceholderGroupWorldEntitySpawner.cs | 1 - .../PrefabPlaceholderEntitySpawner.cs | 1 - .../ReefbackChildEntitySpawner.cs | 1 - .../WorldEntities/ReefbackEntitySpawner.cs | 1 - .../SerializedWorldEntitySpawner.cs | 2 - .../WorldEntities/VehicleEntitySpawner.cs | 1 - .../WorldEntitySpawnerResolver.cs | 3 +- .../GameLogic/Spawning/WorldEntitySpawner.cs | 25 +- NitroxClient/GameLogic/Terrain.cs | 4 +- NitroxClient/GameLogic/TimeManager.cs | 1 - NitroxClient/GameLogic/Vehicles.cs | 13 +- NitroxClient/GlobalUsings.cs | 1 + .../MultiplayerCinematicController.cs | 15 +- .../MultiplayerCinematicReference.cs | 9 +- .../MonoBehaviours/Cyclops/NitroxCyclops.cs | 4 - .../MonoBehaviours/Gui/Chat/PlayerChat.cs | 79 +++- .../Gui/HUD/RemotePlayerVitals.cs | 3 +- .../ServerJoin/MainMenuJoinServerPanel.cs | 4 +- .../MonoBehaviours/IntroCinematicUpdater.cs | 2 +- NitroxClient/MonoBehaviours/Multiplayer.cs | 49 +-- NitroxClient/MonoBehaviours/NitroxEntity.cs | 6 +- .../PlayerMovementBroadcaster.cs | 2 +- ...SeeTarget_ScanForAggressionTarget_Patch.cs | 1 - .../AttackCyclops_UpdateAggression_Patch.cs | 1 - ...aseHullStrength_CrushDamageUpdate_Patch.cs | 1 - .../Patches/Dynamic/Bed_Update_Patch.cs | 1 - .../Dynamic/Bench_ExitSittingMode_Patch.cs | 1 - .../Dynamic/Bench_OnHandClick_Patch.cs | 1 - .../Dynamic/Bench_OnPlayerDeath_Patch.cs | 1 - ...CoffeeVendingMachine_OnMachineUse_Patch.cs | 1 - .../Dynamic/Constructable_Construct_Patch.cs | 1 - .../CrafterLogic_NotifyPickup_Patch.cs | 1 - .../Dynamic/CrashHome_OnDestroy_Patch.cs | 1 - .../Patches/Dynamic/CrashHome_Spawn_Patch.cs | 1 - .../Patches/Dynamic/CrashHome_Update_Patch.cs | 2 +- ...oder_OnConsoleCommand_explodeship_Patch.cs | 1 - .../CreatureDeath_OnKillAsync_Patch.cs | 2 - .../CreatureDeath_SpawnRespawner_Patch.cs | 1 - .../Dynamic/CreatureEgg_Hatch_Patch.cs | 1 - ...psDestructionEvent_SpawnLootAsync_Patch.cs | 2 - ...lopsSonarDisplay_NewEntityOnSonar_Patch.cs | 1 - ...ayNightCycle_OnConsoleCommand_day_Patch.cs | 1 - ...NightCycle_OnConsoleCommand_night_Patch.cs | 1 - .../Dynamic/DevConsole_Submit_Patch.cs | 1 - ...ckedVehicleHandTarget_OnHandClick_Patch.cs | 3 +- .../Dynamic/Eatable_IterateDespawn_Patch.cs | 1 - .../Dynamic/FMODUWE_PlayOneShotImpl_Patch.cs | 1 - .../FMOD_CustomEmitter_OnStop_Patch.cs | 1 - ...reExtinguisherHolder_TryStoreTank_Patch.cs | 1 - .../Patches/Dynamic/Flare_OnDestroy_Patch.cs | 1 - .../Dynamic/FootstepSounds_OnStep_Patch.cs | 8 +- .../Dynamic/FruitPlant_Update_Patch.cs | 1 - ...eConsoleCommands_OnConsoleCommand_Patch.cs | 3 - .../GhostCrafter_OnCraftingBegin_Patch.cs | 1 - .../GoalManager_OnCompletedGoal_Patch.cs | 1 - .../Dynamic/GrownPlant_OnKill_Patch.cs | 1 - ...Terminal_OnPlayerCinematicModeEnd_Patch.cs | 1 - .../Dynamic/IncubatorEgg_HatchNow_Patch.cs | 1 - .../Dynamic/Incubator_OnHatched_Patch.cs | 1 - .../ItemsContainer_DestroyItem_Patch.cs | 1 - .../Dynamic/KnownTech_NotifyAdd_Patch.cs | 2 - .../Dynamic/KnownTech_NotifyAnalyze_Patch.cs | 2 - .../Dynamic/LiveMixin_AddHealth_Patch.cs | 1 - .../Patches/Dynamic/LiveMixin_Kill_Patch.cs | 1 - .../Dynamic/LiveMixin_TakeDamage_Patch.cs | 3 +- ...Command_OnConsoleCommand_fastgrow_Patch.cs | 1 - ...ommand_OnConsoleCommand_fasthatch_Patch.cs | 1 - .../Dynamic/PDAEncyclopedia_Add_Patch.cs | 1 - .../Patches/Dynamic/PDALog_Add_Patch.cs | 1 - .../PickPrefab_AddToContainerAsync_Patch.cs | 1 - .../Dynamic/PickPrefab_SetPickedUp_Patch.cs | 1 - .../Dynamic/PinManager_NotifyAdd_Patch.cs | 1 - .../Dynamic/PinManager_NotifyRemove_Patch.cs | 1 - .../Dynamic/PingInstance_Set_Patches.cs | 1 - .../Patches/Dynamic/Poop_Perform_Patch.cs | 1 - .../PrecursorKeyTerminal_DestroyKey_Patch.cs | 1 - ...Terminal_OnPlayerCinematicModeEnd_Patch.cs | 1 - .../QuickSlots_DeselectInternal_Patch.cs | 1 - .../QuickSlots_SelectInternal_Patch.cs | 2 - .../Dynamic/Radio_PlayRadioMessage_Patch.cs | 1 - ...ngedAttackLastTarget_StartCasting_Patch.cs | 1 - ...gedAttackLastTarget_StartCharging_Patch.cs | 1 - ...SeaDragonMeleeAttack_OnTouchFront_Patch.cs | 1 - .../SeaDragonMeleeAttack_SwatAttack_Patch.cs | 1 - .../Dynamic/SeaDragon_GrabExosuit_Patch.cs | 1 - .../SeaTreaderSounds_SpawnChunks_Patch.cs | 2 - .../Dynamic/SeamothTorpedo_Explode_Patch.cs | 2 - ...SeamothTorpedo_RepeatingTargeting_Patch.cs | 2 - .../Dynamic/SpawnOnKill_OnKill_Patch.cs | 1 - .../Dynamic/StasisSphere_OnHit_Patch.cs | 9 +- .../Dynamic/StasisSphere_Shoot_Patch.cs | 9 +- ...stomEventHandler_OnConsoleCommand_Patch.cs | 1 - .../StoryGoalScheduler_Schedule_Patch.cs | 1 - .../Dynamic/StoryGoal_Execute_Patch.cs | 1 - .../Dynamic/SubNameInput_Awake_Patch.cs | 1 - .../Dynamic/SubNameInput_SetTarget_Patch.cs | 1 - .../Dynamic/SubRoot_OnTakeDamage_Patch.cs | 2 +- .../Patches/Dynamic/Survival_Eat_Patch.cs | 1 - .../Patches/Dynamic/Survival_Use_Patch.cs | 1 - .../Patches/Dynamic/TimeCapsule_Open_Patch.cs | 1 - .../Patches/Dynamic/Trashcan_Update_Patch.cs | 1 - .../Dynamic/Utils_PlayFMODAsset_Patch.cs | 1 - .../VehicleDockingBay_OnTriggerEnter.cs | 3 +- ...cleDockingBay_OnUndockingComplete_Patch.cs | 3 +- .../Dynamic/Vehicle_TorpedoShot_Patch.cs | 2 - .../WaterParkCreature_ManagedUpdate_Patch.cs | 1 - .../WaterParkItem_ManagedUpdate_Patch.cs | 1 - .../Dynamic/uGUI_ColorPicker_Awake_Patch.cs | 1 - .../uGUI_SceneIntro_IntroSequence_Patch.cs | 4 +- .../Persistent/uGUI_MainMenu_Start_Patch.cs | 2 +- 610 files changed, 7178 insertions(+), 6701 deletions(-) delete mode 100644 Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs create mode 100644 Nitrox.Model.Subnautica/Extensions/SunbeamEventExtensions.cs create mode 100644 Nitrox.Model/Core/PeerId.cs create mode 100644 Nitrox.Model/Core/SessionId.cs delete mode 100644 Nitrox.Model/Helper/AsyncBarrier.cs create mode 100644 Nitrox.Model/Packets/Core/IPacketProcessContext.cs create mode 100644 Nitrox.Model/Packets/Core/IPacketProcessor.cs create mode 100644 Nitrox.Model/Packets/Core/PacketProcessorsInvoker.cs delete mode 100644 Nitrox.Model/Packets/Processors/Abstract/IProcessorContext.cs delete mode 100644 Nitrox.Model/Packets/Processors/Abstract/PacketProcessor.cs create mode 100644 Nitrox.Model/Packets/TextAutoComplete.cs create mode 100644 Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs create mode 100644 Nitrox.Server.Subnautica/Models/Administration/IKickPlayer.cs create mode 100644 Nitrox.Server.Subnautica/Models/AppEvents/ISaveState.cs create mode 100644 Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs create mode 100644 Nitrox.Server.Subnautica/Models/AppEvents/Triggers/AsyncTrigger.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/CallArgs.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Command.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Parameter.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeBoolean.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeEnum.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeFloat.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeInt.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeNitroxId.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypePlayer.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeString.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/ConvertResult.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/IArgConverter.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/PlayerNameToPlayerArgConverter.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/SessionIdToPlayerArgConverter.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/WordToBoolArgConverter.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/AliasAttribute.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/CommandHandlerEntry.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/CommandOrigin.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/ICommandContext.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/ICommandHandler.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/ICommandSubmit.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/RequiresOrigin.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Core/RequiresPermissionAttribute.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/DebugStartMapCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/DebugStartMapCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/LoadBatchCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/PlayerCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/SeedNearPositionCommand.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/LoadBatchCommand.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/PlayerCommand.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Processor/TextCommandProcessor.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/SwapSerializerCommand.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Communication/LiteNetLibConnection.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Communication/NitroxConnection.cs create mode 100644 Nitrox.Server.Subnautica/Models/Communication/SessionManager.cs create mode 100644 Nitrox.Server.Subnautica/Models/Factories/RandomFactory.cs delete mode 100644 Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntitySpawner.cs create mode 100644 Nitrox.Server.Subnautica/Models/Helper/EasyPool.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/AnonProcessorContext.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/AuthProcessorContext.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/IAnonPacketProcessor.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/IAuthPacketProcessor.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/IPacketSender.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Core/NopPacketSender.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Packets/PacketHandler.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Packets/Processors/Core/AuthenticatedPacketProcessor.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Packets/Processors/Core/UnauthenticatedPacketProcessor.cs create mode 100644 Nitrox.Server.Subnautica/Models/Packets/Processors/TextAutoCompleteProcessor.cs create mode 100644 Nitrox.Server.Subnautica/Models/Serialization/Json/PeerIdConverter.cs create mode 100644 Nitrox.Server.Subnautica/Services/Core/QueingBackgroundService.cs create mode 100644 Nitrox.Server.Subnautica/Services/PacketRegistryService.cs create mode 100644 Nitrox.Server.Subnautica/Services/PacketSerializationService.cs create mode 100644 Nitrox.Server.Subnautica/Services/PersistNitroxSerializableConfigService.cs rename Nitrox.Test/Server/Helper/{XORRandomTest.cs => XorRandomTest.cs} (81%) delete mode 100644 NitroxClient/Communication/Packets/Processors/Abstract/ClientPacketProcessor.cs delete mode 100644 NitroxClient/Communication/Packets/Processors/Abstract/KeepInventoryChangedProcessor.cs create mode 100644 NitroxClient/Communication/Packets/Processors/Core/ClientProcessorContext.cs create mode 100644 NitroxClient/Communication/Packets/Processors/Core/IClientPacketProcessor.cs create mode 100644 NitroxClient/Communication/Packets/Processors/KeepInventoryChangedProcessor.cs create mode 100644 NitroxClient/Communication/Packets/Processors/TextAutoCompleteProcessor.cs diff --git a/Nitrox.Launcher/Models/Design/ServerEntry.cs b/Nitrox.Launcher/Models/Design/ServerEntry.cs index fd25ce95de..449e7fd739 100644 --- a/Nitrox.Launcher/Models/Design/ServerEntry.cs +++ b/Nitrox.Launcher/Models/Design/ServerEntry.cs @@ -411,7 +411,8 @@ private ServerProcess(string saveDir, CancellationTokenSource cts, bool isEmbedd ArgumentList = { "--save", - saveName + saveName, + $"--game-path \"{NitroxUser.GamePath}\"", }, WindowStyle = isEmbeddedMode ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal, CreateNoWindow = isEmbeddedMode diff --git a/Nitrox.Launcher/Models/Design/StaticCommands.cs b/Nitrox.Launcher/Models/Design/StaticCommands.cs index 2ea21de4ef..83cf6aeec3 100644 --- a/Nitrox.Launcher/Models/Design/StaticCommands.cs +++ b/Nitrox.Launcher/Models/Design/StaticCommands.cs @@ -23,7 +23,11 @@ private static async Task CopyToClipboard(object? controlOrText) { if (GetWindowOfObject(controlOrText) is not { } window) { - return; + window = App.Instance.AppWindow; + } + if (window == null) + { + throw new InvalidOperationException("A window instance must be provided"); } string text = controlOrText switch { @@ -57,7 +61,7 @@ await Dispatcher.UIThread.InvokeAsync(async () => } catch (Exception e) { - Log.Error(e, "Error trying to set clipboard"); + Log.Error(e, "Error trying to copy to clipboard"); } static Window? GetWindowOfObject(object? obj) => diff --git a/Nitrox.Launcher/Models/HttpDelegatingHandlers/FileCacheDelegatingHandler.cs b/Nitrox.Launcher/Models/HttpDelegatingHandlers/FileCacheDelegatingHandler.cs index 62129f5800..cb30f916e1 100644 --- a/Nitrox.Launcher/Models/HttpDelegatingHandlers/FileCacheDelegatingHandler.cs +++ b/Nitrox.Launcher/Models/HttpDelegatingHandlers/FileCacheDelegatingHandler.cs @@ -61,7 +61,7 @@ protected override async Task SendAsync(HttpRequestMessage try { Directory.CreateDirectory(NitroxUser.CachePath); - return Path.Combine(NitroxUser.CachePath, $"nitrox_{string.Join('_', $"{uri.Host}{uri.LocalPath}".ReplaceInvalidFileNameCharacters('_').Split('_').Select(s => s[0]))}_{Convert.ToHexStringLower(uri.ToString().AsMd5Hash())}.cache"); + return Path.Combine(NitroxUser.CachePath, $"nitrox_{string.Join('_', $"{uri.Host}{uri.LocalPath}".ReplaceInvalidFileNameCharacters('_').Split('_').Select(s => s[0]))}_{Convert.ToHexStringLower(uri.ToString().ToMd5Hash())}.cache"); } catch (Exception ex) { diff --git a/Nitrox.Launcher/Models/Services/StorageService.cs b/Nitrox.Launcher/Models/Services/StorageService.cs index fb5ef7ab49..60c8b014bd 100644 --- a/Nitrox.Launcher/Models/Services/StorageService.cs +++ b/Nitrox.Launcher/Models/Services/StorageService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Avalonia; @@ -10,10 +11,9 @@ namespace Nitrox.Launcher.Models.Services; internal sealed class StorageService { - private IStorageProvider? storageProvider; - // TODO: Remove this hack when Avalonia allows IStorageProvider to be accessed without demanding a Window instance. - private IStorageProvider StorageProvider => storageProvider ??= ((IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime)?.Windows.FirstOrDefault()?.StorageProvider ?? throw new Exception($"{nameof(IStorageProvider)} not available!"); + [field: MaybeNull, AllowNull] + private IStorageProvider StorageProvider => field ??= ((IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime)?.Windows.FirstOrDefault()?.StorageProvider ?? throw new Exception($"{nameof(IStorageProvider)} not available!"); public async Task OpenFolderPickerAsync(string title, string? startingFolder = null) { diff --git a/Nitrox.Launcher/Models/Styles/Theme/RadioButtonGroupStyle.axaml b/Nitrox.Launcher/Models/Styles/Theme/RadioButtonGroupStyle.axaml index 3cc64a75e1..5981e17c48 100644 --- a/Nitrox.Launcher/Models/Styles/Theme/RadioButtonGroupStyle.axaml +++ b/Nitrox.Launcher/Models/Styles/Theme/RadioButtonGroupStyle.axaml @@ -3,20 +3,20 @@ xmlns:controls="clr-namespace:Nitrox.Launcher.Models.Controls" xmlns:converters="clr-namespace:Nitrox.Launcher.Models.Converters" xmlns:design="clr-namespace:Nitrox.Launcher.Models.Design" - xmlns:server="clr-namespace:Nitrox.Model.Server;assembly=Nitrox.Model" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:system="clr-namespace:System;assembly=System.Runtime"> + xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:gameLogic="clr-namespace:Nitrox.Model.DataStructures.GameLogic;assembly=Nitrox.Model"> - + - + diff --git a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs index c3a90929a7..0c96c66679 100644 --- a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs @@ -474,7 +474,7 @@ partial void OnServerEmbeddedChanged(bool value) private bool CanDeleteServer() => !ServerIsOnline; - private Config LoadConfig() => NitroxConfig.Load(Path.Combine(SaveFolderDirectory, typeof(Config).GetCustomAttribute()?.FileName ?? throw new InvalidOperationException())); + private Config LoadConfig() => NitroxConfig.Load(SaveFolderDirectory); private void StoreConfig(Config config) { diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/EscapePodWorldEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/EscapePodWorldEntity.cs index 5917fb5fe3..ae8071f9af 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/EscapePodWorldEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/EscapePodWorldEntity.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using BinaryPack.Attributes; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; @@ -13,7 +14,7 @@ namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; public class EscapePodEntity : GlobalRootEntity { [DataMember(Order = 1)] - public List Players { get; set; } = []; + public List Players { get; set; } = []; [IgnoreConstructor] protected EscapePodEntity() @@ -34,7 +35,7 @@ public EscapePodEntity(NitroxVector3 position, NitroxId id, EntityMetadata metad } /// Used for deserialization - public EscapePodEntity(List players, NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) : + public EscapePodEntity(List players, NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) : base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) { Players = players; diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/GlobalRootEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/GlobalRootEntity.cs index 6e21c85d9c..4a17a1d2b5 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/GlobalRootEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/GlobalRootEntity.cs @@ -32,7 +32,7 @@ protected GlobalRootEntity() } /// Used for deserialization - public GlobalRootEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) : + public GlobalRootEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata? metadata, NitroxId? parentId, List childEntities) : base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) { } } diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs index 376f11da97..078f96baf4 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUwePrefabFactory.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using System.Threading.Tasks; namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; public interface IUwePrefabFactory { - public bool TryGetPossiblePrefabs(string biomeType, out List prefabs); + Task> TryGetPossiblePrefabsAsync(string? biome); } diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs deleted file mode 100644 index f142ae84ff..0000000000 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/IUweWorldEntityFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; - -public interface IUweWorldEntityFactory -{ - public bool TryFind(string classId, out UweWorldEntity uweWorldEntity); -} diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/InventoryItemEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/InventoryItemEntity.cs index edcf53d63b..98c2db2468 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/InventoryItemEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/InventoryItemEntity.cs @@ -21,7 +21,7 @@ protected InventoryItemEntity() } /// Used for deserialization - public InventoryItemEntity(NitroxId id, string classId, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) + public InventoryItemEntity(NitroxId id, string classId, NitroxTechType techType, EntityMetadata? metadata, NitroxId parentId, List childEntities) { ClassId = classId; Id = id; diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/OxygenPipeEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/OxygenPipeEntity.cs index 644f5f13a0..e863edf167 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/OxygenPipeEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/OxygenPipeEntity.cs @@ -27,7 +27,7 @@ protected OxygenPipeEntity() } /// Used for deserialization - public OxygenPipeEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities, NitroxId parentPipeId, NitroxId rootPipeId, NitroxVector3 parentPosition) : + public OxygenPipeEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata? metadata, NitroxId? parentId, List childEntities, NitroxId parentPipeId, NitroxId rootPipeId, NitroxVector3 parentPosition) : base(transform, level, classId, spawnedByServer, id, techType, metadata, parentId, childEntities) { ParentPipeId = parentPipeId; diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/PrefabChildEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/PrefabChildEntity.cs index 5af1c0572f..54199e6304 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/PrefabChildEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/PrefabChildEntity.cs @@ -8,7 +8,7 @@ namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities { /* - * A PrefabChildEntity is a gameobject that resides inside of a spawned prefab. Although the server knows about these, + * A PrefabChildEntity is a gameobject that resides inside a spawned prefab. Although the server knows about these, * it is too cost prohibitive for it to send spawn data for all of these. Instead, we let the game spawn them and tag * the entity after the fact. An example of this is a keypad in the aurora; there is an overarching Door prefab with * the keypad baked in - we simply update the id of the keypad on spawn. Each PrefabChildEntity will always bubble up diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs index 71d78660d8..d2ee39868c 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/SerializedWorldEntity.cs @@ -46,7 +46,7 @@ protected SerializedWorldEntity() // Constructor for serialization. Has to be "protected" for json serialization. } - public SerializedWorldEntity(List components, int layer, NitroxTransform transform, NitroxId id, NitroxId parentId, AbsoluteEntityCell cell) + public SerializedWorldEntity(List components, int layer, NitroxTransform transform, NitroxId id, NitroxId? parentId, AbsoluteEntityCell cell) { Components = components; Layer = layer; diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/WorldEntity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/WorldEntity.cs index 7079aaeb64..6ed8bae3aa 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/WorldEntity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/WorldEntity.cs @@ -57,7 +57,7 @@ protected WorldEntity() // Constructor for serialization. Has to be "protected" for json serialization. } - public WorldEntity(NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 scale, NitroxTechType techType, int level, string classId, bool spawnedByServer, NitroxId id, Entity parentEntity) + public WorldEntity(NitroxVector3 localPosition, NitroxQuaternion localRotation, NitroxVector3 scale, NitroxTechType techType, int level, string classId, bool spawnedByServer, NitroxId id, Entity? parentEntity) { Transform = new NitroxTransform(localPosition, localRotation, scale); TechType = techType; @@ -78,7 +78,7 @@ public WorldEntity(NitroxVector3 localPosition, NitroxQuaternion localRotation, } /// Used for deserialization - public WorldEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata metadata, NitroxId parentId, List childEntities) + public WorldEntity(NitroxTransform transform, int level, string classId, bool spawnedByServer, NitroxId id, NitroxTechType techType, EntityMetadata? metadata, NitroxId? parentId, List childEntities) { Id = id; TechType = techType; diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entity.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entity.cs index f41c14d5b1..af291185ec 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entity.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entity.cs @@ -28,7 +28,7 @@ public abstract class Entity public NitroxTechType TechType { get; set; } [DataMember(Order = 3)] - public EntityMetadata Metadata { get; set; } + public EntityMetadata? Metadata { get; set; } [DataMember(Order = 4)] public NitroxId? ParentId { get; set; } diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/RandomStartGenerator.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/RandomStartGenerator.cs index e0636176b5..64ec6db2ba 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/RandomStartGenerator.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/RandomStartGenerator.cs @@ -4,52 +4,27 @@ namespace Nitrox.Model.Subnautica.DataStructures.GameLogic; -public class RandomStartGenerator +public sealed class RandomStartGenerator(RandomStartGenerator.IPixelProvider pixelProvider) { - private readonly IPixelProvider pixelProvider; + private readonly IPixelProvider pixelProvider = pixelProvider; - public RandomStartGenerator(IPixelProvider pixelProvider) - { - this.pixelProvider = pixelProvider; - } - - public NitroxVector3 GenerateRandomStartPosition(Random rnd) - { - for (int i = 0; i < 1000; i++) - { - float normalizedX = (float)rnd.NextDouble(); - float normalizedZ = (float)rnd.NextDouble(); - - if (IsStartPointValid(normalizedX, normalizedZ)) - { - float x = 4096f * normalizedX - 2048f; // normalizedX = (x + 2048) / 4096 - float z = 4096f * normalizedZ - 2048f; - return new NitroxVector3(x, 0, z); - } - } - - return NitroxVector3.Zero; - } - - public List GenerateRandomStartPositions(string seed) + /// + /// Generates all starts positions available for a given randomization. Only take as many positions as needed to avoid unnecessary compute. + /// + public IEnumerable GenerateAllStartPositions(Random random) { - Random rnd = new(seed.GetHashCode()); - List list = new(); - - for (int i = 0; i < 1000; i++) + for (int i = 0; i < int.MaxValue; i++) { - float normalizedX = (float)rnd.NextDouble(); - float normalizedZ = (float)rnd.NextDouble(); + float normalizedX = (float)random.NextDouble(); + float normalizedZ = (float)random.NextDouble(); if (IsStartPointValid(normalizedX, normalizedZ)) { float x = 4096f * normalizedX - 2048f; // normalizedX = (x + 2048) / 4096 float z = 4096f * normalizedZ - 2048f; - list.Add(new NitroxVector3(x, 0, z)); + yield return new NitroxVector3(x, 0, z); } } - - return list; } private bool IsStartPointValid(float normalizedX, float normalizedZ) diff --git a/Nitrox.Model.Subnautica/Extensions/SunbeamEventExtensions.cs b/Nitrox.Model.Subnautica/Extensions/SunbeamEventExtensions.cs new file mode 100644 index 0000000000..615a95a622 --- /dev/null +++ b/Nitrox.Model.Subnautica/Extensions/SunbeamEventExtensions.cs @@ -0,0 +1,17 @@ +using System; +using static Nitrox.Model.Subnautica.Packets.PlaySunbeamEvent; +using static Nitrox.Model.Subnautica.Packets.PlaySunbeamEvent.SunbeamEvent; + +namespace Nitrox.Model.Subnautica.Extensions; + +public static class SunbeamEventExtensions +{ + public static string ToStoryKey(this SunbeamEvent storyEvent) => + storyEvent switch + { + STORYSTART => "RadioSunbeamStart", + GUNAIM => "PrecursorGunAimCheck", + COUNTDOWN => "OnPlayRadioSunbeam4", + _ => throw new ArgumentOutOfRangeException(nameof(storyEvent), $"Unknown {nameof(SunbeamEvent)} with number {(int)storyEvent}.") + }; +} diff --git a/Nitrox.Model.Subnautica/MultiplayerSession/PlayerContext.cs b/Nitrox.Model.Subnautica/MultiplayerSession/PlayerContext.cs index 5e892ac66d..040f892a8d 100644 --- a/Nitrox.Model.Subnautica/MultiplayerSession/PlayerContext.cs +++ b/Nitrox.Model.Subnautica/MultiplayerSession/PlayerContext.cs @@ -1,8 +1,7 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Server; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; namespace Nitrox.Model.Subnautica.MultiplayerSession; @@ -11,7 +10,7 @@ namespace Nitrox.Model.Subnautica.MultiplayerSession; public class PlayerContext { public string PlayerName { get; } - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxId PlayerNitroxId { get; } public bool WasBrandNewPlayer { get; } public PlayerSettings PlayerSettings { get; } @@ -20,15 +19,15 @@ public class PlayerContext /// /// Not null if the player is currently driving a vehicle. /// - public NitroxId DrivingVehicle { get; set; } + public NitroxId? DrivingVehicle { get; set; } public IntroCinematicMode IntroCinematicMode { get; set; } public PlayerAnimation Animation { get; set; } - public PlayerContext(string playerName, ushort playerId, NitroxId playerNitroxId, bool wasBrandNewPlayer, PlayerSettings playerSettings, bool isMuted, - SubnauticaGameMode gameMode, NitroxId drivingVehicle, IntroCinematicMode introCinematicMode, PlayerAnimation animation) + public PlayerContext(string playerName, SessionId sessionId, NitroxId playerNitroxId, bool wasBrandNewPlayer, PlayerSettings playerSettings, bool isMuted, + SubnauticaGameMode gameMode, NitroxId? drivingVehicle, IntroCinematicMode introCinematicMode, PlayerAnimation animation) { PlayerName = playerName; - PlayerId = playerId; + SessionId = sessionId; PlayerNitroxId = playerNitroxId; WasBrandNewPlayer = wasBrandNewPlayer; PlayerSettings = playerSettings; @@ -41,6 +40,6 @@ public PlayerContext(string playerName, ushort playerId, NitroxId playerNitroxId public override string ToString() { - return $"[{nameof(PlayerContext)} PlayerName: {PlayerName}, PlayerId: {PlayerId}, PlayerNitroxId: {PlayerNitroxId}, WasBrandNewPlayer: {WasBrandNewPlayer}, PlayerSettings: {PlayerSettings}, GameMode: {GameMode}, DrivingVehicle: {DrivingVehicle}, IntroCinematicMode: {IntroCinematicMode}, Animation: {Animation}]"; + return $"[{nameof(PlayerContext)} PlayerName: {PlayerName}, {nameof(SessionId)}: {SessionId}, PlayerNitroxId: {PlayerNitroxId}, WasBrandNewPlayer: {WasBrandNewPlayer}, PlayerSettings: {PlayerSettings}, GameMode: {GameMode}, DrivingVehicle: {DrivingVehicle}, IntroCinematicMode: {IntroCinematicMode}, Animation: {Animation}]"; } } diff --git a/Nitrox.Model.Subnautica/Packets/AnimationChangeEvent.cs b/Nitrox.Model.Subnautica/Packets/AnimationChangeEvent.cs index 0e18951f07..eaac32f28f 100644 --- a/Nitrox.Model.Subnautica/Packets/AnimationChangeEvent.cs +++ b/Nitrox.Model.Subnautica/Packets/AnimationChangeEvent.cs @@ -1,18 +1,13 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; namespace Nitrox.Model.Subnautica.Packets; [Serializable] -public class AnimationChangeEvent : Packet +public class AnimationChangeEvent(SessionId sessionId, PlayerAnimation animation) : Packet { - public ushort PlayerId { get; } - public PlayerAnimation Animation { get; } - - public AnimationChangeEvent(ushort playerId, PlayerAnimation animation) - { - PlayerId = playerId; - Animation = animation; - } + public SessionId SessionId { get; } = sessionId; + public PlayerAnimation Animation { get; } = animation; } diff --git a/Nitrox.Model.Subnautica/Packets/BenchChanged.cs b/Nitrox.Model.Subnautica/Packets/BenchChanged.cs index 9414305665..be6dfb9303 100644 --- a/Nitrox.Model.Subnautica/Packets/BenchChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/BenchChanged.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -7,13 +8,13 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class BenchChanged : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxId BenchId { get; } public BenchChangeState ChangeState { get; } - public BenchChanged(ushort playerId, NitroxId benchId, BenchChangeState changeState) + public BenchChanged(SessionId sessionId, NitroxId benchId, BenchChangeState changeState) { - PlayerId = playerId; + SessionId = sessionId; BenchId = benchId; ChangeState = changeState; } diff --git a/Nitrox.Model.Subnautica/Packets/BuildingResyncRequest.cs b/Nitrox.Model.Subnautica/Packets/BuildingResyncRequest.cs index cb57bae02d..92d236d427 100644 --- a/Nitrox.Model.Subnautica/Packets/BuildingResyncRequest.cs +++ b/Nitrox.Model.Subnautica/Packets/BuildingResyncRequest.cs @@ -5,14 +5,8 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] -public sealed class BuildingResyncRequest : Packet +public sealed class BuildingResyncRequest(NitroxId? entityId = null, bool resyncEverything = true) : Packet { - public NitroxId EntityId { get; } - public bool ResyncEverything { get; } - - public BuildingResyncRequest(NitroxId entityId = null, bool resyncEverything = true) - { - EntityId = entityId; - ResyncEverything = resyncEverything; - } + public NitroxId? EntityId { get; } = entityId; + public bool ResyncEverything { get; } = resyncEverything; } diff --git a/Nitrox.Model.Subnautica/Packets/CellVisibilityChanged.cs b/Nitrox.Model.Subnautica/Packets/CellVisibilityChanged.cs index 6f4d61f4b8..7d9dbc841d 100644 --- a/Nitrox.Model.Subnautica/Packets/CellVisibilityChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/CellVisibilityChanged.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Nitrox.Model.Core; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -8,13 +9,13 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class CellVisibilityChanged : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public List Added { get; } public List Removed { get; } - public CellVisibilityChanged(ushort playerId, List added, List removed) + public CellVisibilityChanged(SessionId sessionId, List added, List removed) { - PlayerId = playerId; + SessionId = sessionId; Added = added; Removed = removed; } diff --git a/Nitrox.Model.Subnautica/Packets/ChatMessage.cs b/Nitrox.Model.Subnautica/Packets/ChatMessage.cs index 25f6beb4de..f6a80ec9b7 100644 --- a/Nitrox.Model.Subnautica/Packets/ChatMessage.cs +++ b/Nitrox.Model.Subnautica/Packets/ChatMessage.cs @@ -1,19 +1,18 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class ChatMessage : Packet { - [Serializable] - public class ChatMessage : Packet - { - public ushort PlayerId { get; } - public string Text { get; } - public const ushort SERVER_ID = ushort.MaxValue; + public SessionId SessionId { get; } + public string Text { get; } - public ChatMessage(ushort playerId, string text) - { - PlayerId = playerId; - Text = text; - } + public ChatMessage(SessionId sessionId, string text) + { + SessionId = sessionId; + Text = text; } } diff --git a/Nitrox.Model.Subnautica/Packets/DebugStartMapPacket.cs b/Nitrox.Model.Subnautica/Packets/DebugStartMapPacket.cs index ac100b2a64..3c68eb97e7 100644 --- a/Nitrox.Model.Subnautica/Packets/DebugStartMapPacket.cs +++ b/Nitrox.Model.Subnautica/Packets/DebugStartMapPacket.cs @@ -3,16 +3,10 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets -{ - [Serializable] - public class DebugStartMapPacket : Packet - { - public List StartPositions { get; } +namespace Nitrox.Model.Subnautica.Packets; - public DebugStartMapPacket(List startPositions) - { - StartPositions = startPositions; - } - } +[Serializable] +public class DebugStartMapPacket(IList startPositions) : Packet +{ + public IList StartPositions { get; } = startPositions; } diff --git a/Nitrox.Model.Subnautica/Packets/Disconnect.cs b/Nitrox.Model.Subnautica/Packets/Disconnect.cs index c6a34ea3e2..a959dc07cf 100644 --- a/Nitrox.Model.Subnautica/Packets/Disconnect.cs +++ b/Nitrox.Model.Subnautica/Packets/Disconnect.cs @@ -1,16 +1,11 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets -{ - [Serializable] - public class Disconnect : Packet - { - public ushort PlayerId { get; } +namespace Nitrox.Model.Subnautica.Packets; - public Disconnect(ushort playerId) - { - PlayerId = playerId; - } - } +[Serializable] +public class Disconnect(SessionId sessionId) : Packet +{ + public SessionId SessionId { get; } = sessionId; } diff --git a/Nitrox.Model.Subnautica/Packets/EscapePodChanged.cs b/Nitrox.Model.Subnautica/Packets/EscapePodChanged.cs index e385304f0b..69a4ae1e5d 100644 --- a/Nitrox.Model.Subnautica/Packets/EscapePodChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/EscapePodChanged.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -7,12 +8,12 @@ namespace Nitrox.Model.Subnautica.Packets [Serializable] public class EscapePodChanged : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public Optional EscapePodId { get; } - public EscapePodChanged(ushort playerId, Optional escapePodId) + public EscapePodChanged(SessionId sessionId, Optional escapePodId) { - PlayerId = playerId; + SessionId = sessionId; EscapePodId = escapePodId; } } diff --git a/Nitrox.Model.Subnautica/Packets/FootstepPacket.cs b/Nitrox.Model.Subnautica/Packets/FootstepPacket.cs index ee85216e9d..0e3358f87e 100644 --- a/Nitrox.Model.Subnautica/Packets/FootstepPacket.cs +++ b/Nitrox.Model.Subnautica/Packets/FootstepPacket.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; namespace Nitrox.Model.Subnautica.Packets; @@ -6,12 +7,12 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class FootstepPacket : Packet { - public ushort PlayerID { get; } + public SessionId SessionId { get; } public StepSounds AssetIndex { get; } - public FootstepPacket(ushort playerID, StepSounds assetIndex) + public FootstepPacket(SessionId sessionId, StepSounds assetIndex) { - PlayerID = playerID; + SessionId = sessionId; AssetIndex = assetIndex; DeliveryMethod = Networking.NitroxDeliveryMethod.DeliveryMethod.UNRELIABLE_SEQUENCED; diff --git a/Nitrox.Model.Subnautica/Packets/GameModeChanged.cs b/Nitrox.Model.Subnautica/Packets/GameModeChanged.cs index ec005b4516..926db1a8d9 100644 --- a/Nitrox.Model.Subnautica/Packets/GameModeChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/GameModeChanged.cs @@ -1,28 +1,27 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.Packets; -using Nitrox.Model.Server; -using Nitrox.Model.Subnautica.DataStructures; namespace Nitrox.Model.Subnautica.Packets; [Serializable] -public class GameModeChanged : Packet +public sealed class GameModeChanged : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public bool AllPlayers { get; } public SubnauticaGameMode GameMode { get; } - public GameModeChanged(ushort playerId, bool allPlayers, SubnauticaGameMode gameMode) + public GameModeChanged(SessionId sessionId, bool allPlayers, SubnauticaGameMode gameMode) { - PlayerId = playerId; + SessionId = sessionId; AllPlayers = allPlayers; GameMode = gameMode; } - public static GameModeChanged ForPlayer(ushort playerId, SubnauticaGameMode gameMode) + public static GameModeChanged ForPlayer(SessionId sessionId, SubnauticaGameMode gameMode) { - return new(playerId, false, gameMode); + return new(sessionId, false, gameMode); } public static GameModeChanged ForAllPlayers(SubnauticaGameMode gameMode) diff --git a/Nitrox.Model.Subnautica/Packets/KnownTechEntryAdd.cs b/Nitrox.Model.Subnautica/Packets/KnownTechEntryAdd.cs index 0b03ef5ae2..98c5d889df 100644 --- a/Nitrox.Model.Subnautica/Packets/KnownTechEntryAdd.cs +++ b/Nitrox.Model.Subnautica/Packets/KnownTechEntryAdd.cs @@ -28,12 +28,12 @@ public enum EntryCategory public EntryCategory Category { get; } public List PartialTechTypesToRemove { get; } - public KnownTechEntryAdd(EntryCategory category, NitroxTechType techType, bool verbose, List partialTechTypesToRemove = null) + public KnownTechEntryAdd(EntryCategory category, NitroxTechType techType, bool verbose, List? partialTechTypesToRemove = null) { Category = category; TechType = techType; Verbose = verbose; - PartialTechTypesToRemove = partialTechTypesToRemove; + PartialTechTypesToRemove = partialTechTypesToRemove ?? []; } } } diff --git a/Nitrox.Model.Subnautica/Packets/Movement.cs b/Nitrox.Model.Subnautica/Packets/Movement.cs index c3389fbe36..c8a9fc27f3 100644 --- a/Nitrox.Model.Subnautica/Packets/Movement.cs +++ b/Nitrox.Model.Subnautica/Packets/Movement.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Packets; @@ -7,7 +8,7 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public abstract class Movement : Packet { - public abstract ushort PlayerId { get; } + public abstract SessionId SessionId { get; } public abstract NitroxVector3 Position { get; } public abstract NitroxVector3 Velocity { get; } public abstract NitroxQuaternion BodyRotation { get; } diff --git a/Nitrox.Model.Subnautica/Packets/MultiplayerSessionReservation.cs b/Nitrox.Model.Subnautica/Packets/MultiplayerSessionReservation.cs index 906e9b49a1..c82e98c061 100644 --- a/Nitrox.Model.Subnautica/Packets/MultiplayerSessionReservation.cs +++ b/Nitrox.Model.Subnautica/Packets/MultiplayerSessionReservation.cs @@ -1,26 +1,30 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.MultiplayerSession; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class MultiplayerSessionReservation : CorrelatedPacket { - [Serializable] - public class MultiplayerSessionReservation : CorrelatedPacket + public MultiplayerSessionReservation(string correlationId, MultiplayerSessionReservationState reservationState) : base(correlationId) { - public ushort PlayerId { get; } - public string ReservationKey { get; } - public MultiplayerSessionReservationState ReservationState { get; } + ReservationState = reservationState; + } - public MultiplayerSessionReservation(string correlationId, MultiplayerSessionReservationState reservationState) : base(correlationId) - { - ReservationState = reservationState; - } - - public MultiplayerSessionReservation(string correlationId, ushort playerId, string reservationKey, - MultiplayerSessionReservationState reservationState = MultiplayerSessionReservationState.RESERVED) : this(correlationId, reservationState) - { - PlayerId = playerId; - ReservationKey = reservationKey; - } + public MultiplayerSessionReservation(string correlationId, SessionId sessionId, string reservationKey, + MultiplayerSessionReservationState reservationState = MultiplayerSessionReservationState.RESERVED) : this(correlationId, reservationState) + { + SessionId = sessionId; + ReservationKey = reservationKey; } + + /// + /// Gets the session id of the player. + /// + public SessionId SessionId { get; } + + public string ReservationKey { get; } + public MultiplayerSessionReservationState ReservationState { get; } } diff --git a/Nitrox.Model.Subnautica/Packets/MutePlayer.cs b/Nitrox.Model.Subnautica/Packets/MutePlayer.cs index 05ffefe403..7051842979 100644 --- a/Nitrox.Model.Subnautica/Packets/MutePlayer.cs +++ b/Nitrox.Model.Subnautica/Packets/MutePlayer.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; namespace Nitrox.Model.Subnautica.Packets; @@ -6,12 +7,12 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class MutePlayer : Packet { - public ushort PlayerId; + public SessionId SessionId; public bool Muted; - public MutePlayer(ushort playerId, bool muted) + public MutePlayer(SessionId sessionId, bool muted) { - PlayerId = playerId; + SessionId = sessionId; Muted = muted; } } diff --git a/Nitrox.Model.Subnautica/Packets/PDAScanFinished.cs b/Nitrox.Model.Subnautica/Packets/PDAScanFinished.cs index 1f663e1f02..7afeca35a1 100644 --- a/Nitrox.Model.Subnautica/Packets/PDAScanFinished.cs +++ b/Nitrox.Model.Subnautica/Packets/PDAScanFinished.cs @@ -8,14 +8,14 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class PDAScanFinished : Packet { - public NitroxId Id { get; } + public NitroxId? Id { get; } public NitroxTechType TechType { get; } public int UnlockedAmount { get; } public bool FullyResearched { get; } public bool Destroy { get; } public bool WasAlreadyResearched { get; } - public PDAScanFinished(NitroxId id, NitroxTechType techType, int unlockedAmount, bool fullyResearched, bool destroy, bool wasAlreadyResearched = false) + public PDAScanFinished(NitroxId? id, NitroxTechType techType, int unlockedAmount, bool fullyResearched, bool destroy, bool wasAlreadyResearched = false) { Id = id; TechType = techType; diff --git a/Nitrox.Model.Subnautica/Packets/PieceDeconstructed.cs b/Nitrox.Model.Subnautica/Packets/PieceDeconstructed.cs index d195b1eeb3..39076f50ff 100644 --- a/Nitrox.Model.Subnautica/Packets/PieceDeconstructed.cs +++ b/Nitrox.Model.Subnautica/Packets/PieceDeconstructed.cs @@ -12,7 +12,7 @@ public class PieceDeconstructed : OrderedBuildPacket public NitroxId PieceId { get; } public BuildPieceIdentifier BuildPieceIdentifier { get; } public GhostEntity ReplacerGhost { get; } - public BaseData BaseData { get; set; } + public BaseData? BaseData { get; set; } public PieceDeconstructed(NitroxId baseId, NitroxId pieceId, BuildPieceIdentifier buildPieceIdentifier, GhostEntity replacerGhost, BaseData baseData, int operationId) : base(operationId) { diff --git a/Nitrox.Model.Subnautica/Packets/PlaySunbeamEvent.cs b/Nitrox.Model.Subnautica/Packets/PlaySunbeamEvent.cs index 2c46b17021..db77e8113b 100644 --- a/Nitrox.Model.Subnautica/Packets/PlaySunbeamEvent.cs +++ b/Nitrox.Model.Subnautica/Packets/PlaySunbeamEvent.cs @@ -2,33 +2,44 @@ using Nitrox.Model.Packets; namespace Nitrox.Model.Subnautica.Packets; - [Serializable] -public class PlaySunbeamEvent : Packet +public class PlaySunbeamEvent(string eventKey) : Packet { - public string EventKey { get; } - - public PlaySunbeamEvent(string eventKey) - { - EventKey = eventKey; - } - - /// - /// Associates an understandable event name and the associated goal from . - /// - public static class SunbeamEvent - { - public const string STORYSTART = "RadioSunbeamStart"; - public const string COUNTDOWN = "OnPlayRadioSunbeam4"; - public const string GUNAIM = "PrecursorGunAimCheck"; - } + public string EventKey { get; } = eventKey; /// - /// An ordered list of the goals forming part of the whole Sunbeam story. + /// An ordered list of the goals forming part of the whole Sunbeam story. /// /// - /// If you modify this list, make sure to accordingly modify . + /// If you modify this list, make sure to accordingly modify . /// [NonSerialized] - public static readonly string[] SunbeamGoals = new string[] { SunbeamEvent.STORYSTART, "OnPlayRadioSunbeamStart", "RadioSunbeam1", "OnPlayRadioSunbeam1", "RadioSunbeam2", "OnPlayRadioSunbeam2", "RadioSunbeam3", "OnPlayRadioSunbeam3", "RadioSunbeam4", SunbeamEvent.COUNTDOWN, SunbeamEvent.GUNAIM, "PrecursorGunAim", "SunbeamCheckPlayerRange", "PDASunbeamDestroyEventOutOfRange", "PDASunbeamDestroyEventInRange" }; + public static readonly string[] SunbeamGoals = + [ + SunbeamEvent.STORYSTART.ToStoryKey(), + "OnPlayRadioSunbeamStart", + "RadioSunbeam1", + "OnPlayRadioSunbeam1", + "RadioSunbeam2", + "OnPlayRadioSunbeam2", + "RadioSunbeam3", + "OnPlayRadioSunbeam3", + "RadioSunbeam4", + SunbeamEvent.COUNTDOWN.ToStoryKey(), + SunbeamEvent.GUNAIM.ToStoryKey(), + "PrecursorGunAim", + "SunbeamCheckPlayerRange", + "PDASunbeamDestroyEventOutOfRange", + "PDASunbeamDestroyEventInRange" + ]; + + /// + /// Associates an understandable event name and the associated goal from . + /// + public enum SunbeamEvent + { + STORYSTART, + COUNTDOWN, + GUNAIM + } } diff --git a/Nitrox.Model.Subnautica/Packets/PlayerCinematicControllerCall.cs b/Nitrox.Model.Subnautica/Packets/PlayerCinematicControllerCall.cs index 47ca582b40..24313c2fde 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerCinematicControllerCall.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerCinematicControllerCall.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -7,15 +8,15 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class PlayerCinematicControllerCall : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxId ControllerID { get; } public int ControllerNameHash { get; } public string Key { get; } public bool StartPlaying { get; } - public PlayerCinematicControllerCall(ushort playerId, NitroxId controllerID, int controllerNameHash, string key, bool startPlaying) + public PlayerCinematicControllerCall(SessionId sessionId, NitroxId controllerID, int controllerNameHash, string key, bool startPlaying) { - PlayerId = playerId; + SessionId = sessionId; ControllerID = controllerID; ControllerNameHash = controllerNameHash; Key = key; diff --git a/Nitrox.Model.Subnautica/Packets/PlayerDeathEvent.cs b/Nitrox.Model.Subnautica/Packets/PlayerDeathEvent.cs index f1f63a4343..4d9c7f84d6 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerDeathEvent.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerDeathEvent.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Packets; @@ -7,12 +8,12 @@ namespace Nitrox.Model.Subnautica.Packets [Serializable] public class PlayerDeathEvent : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxVector3 DeathPosition { get; } - public PlayerDeathEvent(ushort playerId, NitroxVector3 deathPosition) + public PlayerDeathEvent(SessionId sessionId, NitroxVector3 deathPosition) { - PlayerId = playerId; + SessionId = sessionId; DeathPosition = deathPosition; } } diff --git a/Nitrox.Model.Subnautica/Packets/PlayerHeldItemChanged.cs b/Nitrox.Model.Subnautica/Packets/PlayerHeldItemChanged.cs index 20a839d7b8..69c1ada055 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerHeldItemChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerHeldItemChanged.cs @@ -1,32 +1,32 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class PlayerHeldItemChanged : Packet { - [Serializable] - public class PlayerHeldItemChanged : Packet - { - public ushort PlayerId { get; } - public NitroxId ItemId { get; } - public ChangeType Type { get; } - public NitroxTechType IsFirstTime { get; } // If it's the first time the player used that item type it send the techType, if not null. + public SessionId SessionId { get; } + public NitroxId ItemId { get; } + public ChangeType Type { get; } + public NitroxTechType? IsFirstTime { get; } // If it's the first time the player used that item type it send the techType, if not null. - public PlayerHeldItemChanged(ushort playerId, NitroxId itemId, ChangeType type, NitroxTechType isFirstTime) - { - PlayerId = playerId; - ItemId = itemId; - Type = type; - IsFirstTime = isFirstTime; - } + public PlayerHeldItemChanged(SessionId sessionId, NitroxId itemId, ChangeType type, NitroxTechType? isFirstTime) + { + SessionId = sessionId; + ItemId = itemId; + Type = type; + IsFirstTime = isFirstTime; + } - public enum ChangeType - { - DRAW_AS_TOOL, - DRAW_AS_ITEM, - HOLSTER_AS_TOOL, - HOLSTER_AS_ITEM - } + public enum ChangeType + { + DRAW_AS_TOOL, + DRAW_AS_ITEM, + HOLSTER_AS_TOOL, + HOLSTER_AS_ITEM } } diff --git a/Nitrox.Model.Subnautica/Packets/PlayerInCyclopsMovement.cs b/Nitrox.Model.Subnautica/Packets/PlayerInCyclopsMovement.cs index 2bab1f4c93..3df4fd543e 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerInCyclopsMovement.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerInCyclopsMovement.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Networking; using Nitrox.Model.Packets; @@ -8,13 +9,13 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class PlayerInCyclopsMovement : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxVector3 LocalPosition { get; } public NitroxQuaternion LocalRotation { get; } - public PlayerInCyclopsMovement(ushort playerId, NitroxVector3 localPosition, NitroxQuaternion localRotation) + public PlayerInCyclopsMovement(SessionId sessionId, NitroxVector3 localPosition, NitroxQuaternion localRotation) { - PlayerId = playerId; + SessionId = sessionId; LocalPosition = localPosition; LocalRotation = localRotation; DeliveryMethod = NitroxDeliveryMethod.DeliveryMethod.UNRELIABLE_SEQUENCED; diff --git a/Nitrox.Model.Subnautica/Packets/PlayerMovement.cs b/Nitrox.Model.Subnautica/Packets/PlayerMovement.cs index a5e90f78f0..de1ee24921 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerMovement.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerMovement.cs @@ -1,27 +1,27 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Networking; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class PlayerMovement : Movement { - [Serializable] - public class PlayerMovement : Movement + public PlayerMovement(SessionId sessionId, NitroxVector3 position, NitroxVector3 velocity, NitroxQuaternion bodyRotation, NitroxQuaternion aimingRotation) { - public override ushort PlayerId { get; } - public override NitroxVector3 Position { get; } - public override NitroxVector3 Velocity { get; } - public override NitroxQuaternion BodyRotation { get; } - public override NitroxQuaternion AimingRotation { get; } - - public PlayerMovement(ushort playerId, NitroxVector3 position, NitroxVector3 velocity, NitroxQuaternion bodyRotation, NitroxQuaternion aimingRotation) - { - PlayerId = playerId; - Position = position; - Velocity = velocity; - BodyRotation = bodyRotation; - AimingRotation = aimingRotation; - DeliveryMethod = NitroxDeliveryMethod.DeliveryMethod.UNRELIABLE_SEQUENCED; - UdpChannel = UdpChannelId.MOVEMENTS; - } + SessionId = sessionId; + Position = position; + Velocity = velocity; + BodyRotation = bodyRotation; + AimingRotation = aimingRotation; + DeliveryMethod = NitroxDeliveryMethod.DeliveryMethod.UNRELIABLE_SEQUENCED; + UdpChannel = UdpChannelId.MOVEMENTS; } + + public override SessionId SessionId { get; } + public override NitroxVector3 Position { get; } + public override NitroxVector3 Velocity { get; } + public override NitroxQuaternion BodyRotation { get; } + public override NitroxQuaternion AimingRotation { get; } } diff --git a/Nitrox.Model.Subnautica/Packets/PlayerStats.cs b/Nitrox.Model.Subnautica/Packets/PlayerStats.cs index f7deea3bd5..90ca64e990 100644 --- a/Nitrox.Model.Subnautica/Packets/PlayerStats.cs +++ b/Nitrox.Model.Subnautica/Packets/PlayerStats.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Networking; using Nitrox.Model.Packets; @@ -7,7 +8,7 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class PlayerStats : Packet { - public ushort PlayerId { get; set; } + public SessionId SessionId { get; set; } public float Oxygen { get; } public float MaxOxygen { get; } public float Health { get; } @@ -15,9 +16,9 @@ public class PlayerStats : Packet public float Water { get; } public float InfectionAmount { get; } - public PlayerStats(ushort playerId, float oxygen, float maxOxygen, float health, float food, float water, float infectionAmount) + public PlayerStats(SessionId sessionId, float oxygen, float maxOxygen, float health, float food, float water, float infectionAmount) { - PlayerId = playerId; + SessionId = sessionId; Oxygen = oxygen; MaxOxygen = maxOxygen; Health = health; diff --git a/Nitrox.Model.Subnautica/Packets/PvPAttack.cs b/Nitrox.Model.Subnautica/Packets/PvPAttack.cs index de1b677e28..a398d37837 100644 --- a/Nitrox.Model.Subnautica/Packets/PvPAttack.cs +++ b/Nitrox.Model.Subnautica/Packets/PvPAttack.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; namespace Nitrox.Model.Subnautica.Packets; @@ -6,13 +7,13 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class PvPAttack : Packet { - public ushort TargetPlayerId { get; } + public SessionId TargetSessionId { get; } public float Damage { get; set; } public AttackType Type { get; } - public PvPAttack(ushort targetPlayerId, float damage, AttackType type) + public PvPAttack(SessionId targetSessionId, float damage, AttackType type) { - TargetPlayerId = targetPlayerId; + TargetSessionId = targetSessionId; Damage = damage; Type = type; } diff --git a/Nitrox.Model.Subnautica/Packets/SetIntroCinematicMode.cs b/Nitrox.Model.Subnautica/Packets/SetIntroCinematicMode.cs index fa13aebf82..2ca9819a8f 100644 --- a/Nitrox.Model.Subnautica/Packets/SetIntroCinematicMode.cs +++ b/Nitrox.Model.Subnautica/Packets/SetIntroCinematicMode.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -7,20 +8,20 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class SetIntroCinematicMode : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public IntroCinematicMode Mode { get; } - public ushort? PartnerId { get; set; } + public SessionId? PartnerId { get; set; } - public SetIntroCinematicMode(ushort playerId, IntroCinematicMode mode) + public SetIntroCinematicMode(SessionId sessionId, IntroCinematicMode mode) { - PlayerId = playerId; + SessionId = sessionId; Mode = mode; PartnerId = null; } - public SetIntroCinematicMode(ushort playerId, IntroCinematicMode mode, ushort? partnerId) + public SetIntroCinematicMode(SessionId sessionId, IntroCinematicMode mode, SessionId? partnerId) { - PlayerId = playerId; + SessionId = sessionId; Mode = mode; PartnerId = partnerId; } diff --git a/Nitrox.Model.Subnautica/Packets/SimulationOwnershipChange.cs b/Nitrox.Model.Subnautica/Packets/SimulationOwnershipChange.cs index f136094f92..23a25752ce 100644 --- a/Nitrox.Model.Subnautica/Packets/SimulationOwnershipChange.cs +++ b/Nitrox.Model.Subnautica/Packets/SimulationOwnershipChange.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -10,11 +11,11 @@ public class SimulationOwnershipChange : Packet { public List Entities { get; } - public SimulationOwnershipChange(NitroxId id, ushort owningPlayerId, SimulationLockType lockType, bool changesPosition = false) + public SimulationOwnershipChange(NitroxId id, SessionId owningSessionId, SimulationLockType lockType, bool changesPosition = false) { Entities = new List { - new(id, owningPlayerId, changesPosition, lockType) + new(id, owningSessionId, changesPosition, lockType) }; } diff --git a/Nitrox.Model.Subnautica/Packets/SimulationOwnershipRequest.cs b/Nitrox.Model.Subnautica/Packets/SimulationOwnershipRequest.cs index bffb19fe92..884df30939 100644 --- a/Nitrox.Model.Subnautica/Packets/SimulationOwnershipRequest.cs +++ b/Nitrox.Model.Subnautica/Packets/SimulationOwnershipRequest.cs @@ -1,21 +1,21 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class SimulationOwnershipRequest : Packet { - [Serializable] - public class SimulationOwnershipRequest : Packet - { - public ushort PlayerId { get; } - public NitroxId Id { get; } - public SimulationLockType LockType { get; } + public SessionId SessionId { get; } + public NitroxId Id { get; } + public SimulationLockType LockType { get; } - public SimulationOwnershipRequest(ushort playerId, NitroxId id, SimulationLockType lockType) - { - PlayerId = playerId; - Id = id; - LockType = lockType; - } + public SimulationOwnershipRequest(SessionId sessionId, NitroxId id, SimulationLockType lockType) + { + SessionId = sessionId; + Id = id; + LockType = lockType; } } diff --git a/Nitrox.Model.Subnautica/Packets/SpawnEntities.cs b/Nitrox.Model.Subnautica/Packets/SpawnEntities.cs index 1bded78973..437d3626eb 100644 --- a/Nitrox.Model.Subnautica/Packets/SpawnEntities.cs +++ b/Nitrox.Model.Subnautica/Packets/SpawnEntities.cs @@ -24,7 +24,7 @@ public SpawnEntities(List entities, List spawnedCell ForceRespawn = forceRespawn; } - public SpawnEntities(Entity entity, SimulatedEntity simulatedEntity = null, bool forceRespawn = false) + public SpawnEntities(Entity entity, SimulatedEntity? simulatedEntity = null, bool forceRespawn = false) { Entities = [entity]; Simulations = []; diff --git a/Nitrox.Model.Subnautica/Packets/StasisSphereHit.cs b/Nitrox.Model.Subnautica/Packets/StasisSphereHit.cs index 45a505745b..256ded8881 100644 --- a/Nitrox.Model.Subnautica/Packets/StasisSphereHit.cs +++ b/Nitrox.Model.Subnautica/Packets/StasisSphereHit.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Packets; @@ -7,15 +8,15 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class StasisSphereHit : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxVector3 Position { get; } public NitroxQuaternion Rotation { get; } public float ChargeNormalized { get; } public float Consumption { get; } - public StasisSphereHit(ushort playerId, NitroxVector3 position, NitroxQuaternion rotation, float chargeNormalized, float consumption) + public StasisSphereHit(SessionId sessionId, NitroxVector3 position, NitroxQuaternion rotation, float chargeNormalized, float consumption) { - PlayerId = playerId; + SessionId = sessionId; Position = position; Rotation = rotation; ChargeNormalized = chargeNormalized; diff --git a/Nitrox.Model.Subnautica/Packets/StasisSphereShot.cs b/Nitrox.Model.Subnautica/Packets/StasisSphereShot.cs index 61dba4a9d6..ca2bfa8d60 100644 --- a/Nitrox.Model.Subnautica/Packets/StasisSphereShot.cs +++ b/Nitrox.Model.Subnautica/Packets/StasisSphereShot.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Packets; @@ -7,16 +8,16 @@ namespace Nitrox.Model.Subnautica.Packets; [Serializable] public class StasisSphereShot : Packet { - public ushort PlayerId { get; } + public SessionId SessionId { get; } public NitroxVector3 Position { get; } public NitroxQuaternion Rotation { get; } public float Speed { get; } public float LifeTime { get; } public float ChargeNormalized { get; } - public StasisSphereShot(ushort playerId, NitroxVector3 position, NitroxQuaternion rotation, float speed, float lifeTime, float chargeNormalized) + public StasisSphereShot(SessionId sessionId, NitroxVector3 position, NitroxQuaternion rotation, float speed, float lifeTime, float chargeNormalized) { - PlayerId = playerId; + SessionId = sessionId; Position = position; Rotation = rotation; Speed = speed; diff --git a/Nitrox.Model.Subnautica/Packets/SubRootChanged.cs b/Nitrox.Model.Subnautica/Packets/SubRootChanged.cs index f864889e0a..61a8e89b28 100644 --- a/Nitrox.Model.Subnautica/Packets/SubRootChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/SubRootChanged.cs @@ -1,19 +1,19 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; -namespace Nitrox.Model.Subnautica.Packets +namespace Nitrox.Model.Subnautica.Packets; + +[Serializable] +public class SubRootChanged : Packet { - [Serializable] - public class SubRootChanged : Packet - { - public ushort PlayerId { get; } - public Optional SubRootId { get; } + public SessionId SessionId { get; } + public Optional SubRootId { get; } - public SubRootChanged(ushort playerId, Optional subRootId) - { - PlayerId = playerId; - SubRootId = subRootId; - } + public SubRootChanged(SessionId sessionId, Optional subRootId) + { + SessionId = sessionId; + SubRootId = subRootId; } } diff --git a/Nitrox.Model.Subnautica/Packets/VehicleDocking.cs b/Nitrox.Model.Subnautica/Packets/VehicleDocking.cs index 5ea4155cb6..16f2596318 100644 --- a/Nitrox.Model.Subnautica/Packets/VehicleDocking.cs +++ b/Nitrox.Model.Subnautica/Packets/VehicleDocking.cs @@ -1,20 +1,14 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; namespace Nitrox.Model.Subnautica.Packets; [Serializable] -public class VehicleDocking : Packet +public sealed class VehicleDocking(NitroxId vehicleId, NitroxId dockId, SessionId sessionId) : Packet { - public NitroxId VehicleId { get; } - public NitroxId DockId { get; } - public ushort PlayerId { get; } - - public VehicleDocking(NitroxId vehicleId, NitroxId dockId, ushort playerId) - { - VehicleId = vehicleId; - DockId = dockId; - PlayerId = playerId; - } + public NitroxId VehicleId { get; } = vehicleId; + public NitroxId DockId { get; } = dockId; + public SessionId SessionId { get; } = sessionId; } diff --git a/Nitrox.Model.Subnautica/Packets/VehicleOnPilotModeChanged.cs b/Nitrox.Model.Subnautica/Packets/VehicleOnPilotModeChanged.cs index 426bd4b20a..0cf39ad68e 100644 --- a/Nitrox.Model.Subnautica/Packets/VehicleOnPilotModeChanged.cs +++ b/Nitrox.Model.Subnautica/Packets/VehicleOnPilotModeChanged.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -8,13 +9,13 @@ namespace Nitrox.Model.Subnautica.Packets; public class VehicleOnPilotModeChanged : Packet { public NitroxId VehicleId { get; } - public ushort PlayerId { get; } + public SessionId SessionId { get; } public bool IsPiloting { get; } - public VehicleOnPilotModeChanged(NitroxId vehicleId, ushort playerId, bool isPiloting) + public VehicleOnPilotModeChanged(NitroxId vehicleId, SessionId sessionId, bool isPiloting) { VehicleId = vehicleId; - PlayerId = playerId; + SessionId = sessionId; IsPiloting = isPiloting; } } diff --git a/Nitrox.Model.Subnautica/Packets/VehicleUndocking.cs b/Nitrox.Model.Subnautica/Packets/VehicleUndocking.cs index c6823836f0..68a1735108 100644 --- a/Nitrox.Model.Subnautica/Packets/VehicleUndocking.cs +++ b/Nitrox.Model.Subnautica/Packets/VehicleUndocking.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; @@ -9,14 +10,14 @@ public class VehicleUndocking : Packet { public NitroxId VehicleId { get; } public NitroxId DockId { get; } - public ushort PlayerId { get; } + public SessionId SessionId { get; } public bool UndockingStart { get; } - public VehicleUndocking(NitroxId vehicleId, NitroxId dockId, ushort playerId, bool undockingStart) + public VehicleUndocking(NitroxId vehicleId, NitroxId dockId, SessionId sessionId, bool undockingStart) { VehicleId = vehicleId; DockId = dockId; - PlayerId = playerId; + SessionId = sessionId; UndockingStart = undockingStart; } } diff --git a/Nitrox.Model/Configuration/ServerStartOptions.cs b/Nitrox.Model/Configuration/ServerStartOptions.cs index 613b7b1677..c4d1c27bba 100644 --- a/Nitrox.Model/Configuration/ServerStartOptions.cs +++ b/Nitrox.Model/Configuration/ServerStartOptions.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; diff --git a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs index 39ade10f8d..5cb1c90eb7 100644 --- a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs +++ b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs @@ -24,6 +24,7 @@ public sealed partial class SubnauticaServerOptions [Range(1, ushort.MaxValue)] public ushort ServerPort { get; set; } = SubnauticaServerConstants.DEFAULT_PORT; + [PropertyDescription("If not empty, users will be asked to enter a password to join the server")] [RegularExpression(@"\w+")] public string ServerPassword { get; set; } = ""; diff --git a/Nitrox.Model/Constants/NitroxConstants.cs b/Nitrox.Model/Constants/NitroxConstants.cs index 6965b6e0b7..d1adc40cf6 100644 --- a/Nitrox.Model/Constants/NitroxConstants.cs +++ b/Nitrox.Model/Constants/NitroxConstants.cs @@ -3,4 +3,5 @@ namespace Nitrox.Model.Constants; public static class NitroxConstants { public const string LAUNCHER_APP_NAME = "Nitrox.Launcher"; + public const string PLAYER_NAME_VALID_REGEX = @"^[ a-zA-Z0-9._-]{3,25}$"; } diff --git a/Nitrox.Model/Constants/SubnauticaServerConstants.cs b/Nitrox.Model/Constants/SubnauticaServerConstants.cs index 710ef4c283..b109341b0c 100644 --- a/Nitrox.Model/Constants/SubnauticaServerConstants.cs +++ b/Nitrox.Model/Constants/SubnauticaServerConstants.cs @@ -1,11 +1,14 @@ namespace Nitrox.Model.Constants; -public class SubnauticaServerConstants +public static class SubnauticaServerConstants { public const int DEFAULT_PORT = 11000; /// - /// Default seed in development so life pod spawn stays the same. + /// Default seed in development so starting spawn (escape pod) stays the same. /// - public const string DEFAULT_DEVELOPMENT_SEED = "TCCBIBZXAB"; + /// + /// This seed causes first escape pod spawn to be reasonably close to [-112, 0, -320] which we determined is optimal start position. + /// + public const string DEFAULT_DEVELOPMENT_SEED = "95311395"; } diff --git a/Nitrox.Model/Core/PeerId.cs b/Nitrox.Model/Core/PeerId.cs new file mode 100644 index 0000000000..4f845301cb --- /dev/null +++ b/Nitrox.Model/Core/PeerId.cs @@ -0,0 +1,27 @@ +using System; +using System.Diagnostics; + +namespace Nitrox.Model.Core; + +/// +/// Globally unique ID of the networked entity. Is 0 for server. Starts from 1 if player. +/// +[DebuggerDisplay($"{{{nameof(id)}}}")] +public readonly record struct PeerId : IComparable +{ + public const uint SERVER_ID = 0; + + private readonly uint id; + + public bool IsServer => id == SERVER_ID; + + private PeerId(uint id) + { + this.id = id; + } + + public static implicit operator uint(PeerId id) => id.id; + + public static implicit operator PeerId(uint id) => new(id); + public int CompareTo(PeerId other) => id.CompareTo(other.id); +} diff --git a/Nitrox.Model/Core/SessionId.cs b/Nitrox.Model/Core/SessionId.cs new file mode 100644 index 0000000000..a9632cb30a --- /dev/null +++ b/Nitrox.Model/Core/SessionId.cs @@ -0,0 +1,40 @@ +using System; + +namespace Nitrox.Model.Core; + +/// +/// The session id (index) of a connection. The server uses 0, players will start from 1. +/// +/// +/// It's important that, once a session id is assigned by the server, no other connection can impersonate by using the +/// same id. +/// Force a 10 minute "hands-off" time before which this session id can be reused. +/// +public readonly record struct SessionId : IComparable +{ + public const int DELAY_REUSE_MINUTES = 10; + public const ushort SERVER_ID = (ushort)PeerId.SERVER_ID; + + private readonly ushort id; + + public bool IsPlayer => id != SERVER_ID; + + private SessionId(ushort id) + { + this.id = id; + } + + public static implicit operator ushort(SessionId id) + { + return id.id; + } + + public static implicit operator SessionId(ushort id) + { + return new SessionId(id); + } + + public int CompareTo(SessionId other) => id.CompareTo(other.id); + + public override string ToString() => id.ToString(); +} diff --git a/Nitrox.Model/DataStructures/GameLogic/Perms.cs b/Nitrox.Model/DataStructures/GameLogic/Perms.cs index 89de6e5205..5b47ae2ee5 100644 --- a/Nitrox.Model/DataStructures/GameLogic/Perms.cs +++ b/Nitrox.Model/DataStructures/GameLogic/Perms.cs @@ -1,46 +1,39 @@ -using System; +namespace Nitrox.Model.DataStructures.GameLogic; -namespace Nitrox.Model.DataStructures.GameLogic +/// +/// Should be sorted from least to most authoritative. +/// +public enum Perms : byte { - /// - /// Should be sorted from least to most authoritative. - /// - public enum Perms : byte - { - /// - /// No permissions - /// - NONE, - - /// - /// Default player permission, cannot use cheat and have access to basic server commands (e.g: help, list, whisper, - /// whois, ...) - /// - PLAYER, + /// + /// No permissions + /// + NONE, - /// - /// Player that can manage other players in game. Can use vanilla cheat commands and some advanced server commands - /// (e.g: mute, kick, broadcast, ...) - /// - MODERATOR, + /// + /// Default player permission, cannot use cheats and only have access to basic server commands (e.g: help, list, whisper, + /// whois, ...) + /// + PLAYER, - /// - /// Server administrator, can manage server settings and players. Can use vanilla cheat commands and all server - /// commands (e.g: op, promote, server settings, ...) - /// - ADMIN, + /// + /// Player that can manage other players in game. Can use vanilla cheat commands and some advanced server commands + /// (e.g: mute, kick, broadcast, ...) + /// + MODERATOR, - /// - /// All permissions - /// - HOST, - DEFAULT = PLAYER - } + /// + /// Server administrator, can manage server settings and players. Can use vanilla cheat commands and almost all server + /// commands (e.g: op, promote, server settings, ...) + /// + ADMIN, - [Flags] - public enum PermsFlag : byte - { - NONE = 0x0, - NO_CONSOLE = 0x1 - } + /// + /// All permissions, the owner of the server. + /// + /// + /// This is the permission used when using the server console. + /// + HOST, + DEFAULT = PLAYER } diff --git a/Nitrox.Model/DataStructures/NitroxId.cs b/Nitrox.Model/DataStructures/NitroxId.cs index 1ffeeb0841..ea0a5cb2f3 100644 --- a/Nitrox.Model/DataStructures/NitroxId.cs +++ b/Nitrox.Model/DataStructures/NitroxId.cs @@ -52,7 +52,7 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) info.AddValue("id", guid.ToByteArray()); } - public static bool operator ==(NitroxId id1, NitroxId id2) + public static bool operator ==(NitroxId? id1, NitroxId? id2) { if (id1 is null) { @@ -65,7 +65,7 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) return id1.Equals(id2); } - public static bool operator !=(NitroxId id1, NitroxId id2) + public static bool operator !=(NitroxId? id1, NitroxId? id2) { return !(id1 == id2); } @@ -86,7 +86,7 @@ public override bool Equals(object obj) } - public bool Equals(NitroxId other) + public bool Equals(NitroxId? other) { if (ReferenceEquals(null, other)) { diff --git a/Nitrox.Model/DataStructures/Optional.cs b/Nitrox.Model/DataStructures/Optional.cs index 8a8c6d9c9e..27456cf5f6 100644 --- a/Nitrox.Model/DataStructures/Optional.cs +++ b/Nitrox.Model/DataStructures/Optional.cs @@ -123,9 +123,9 @@ internal static Optional Of(T value) return new Optional(value); } - internal static Optional OfNullable(T value) + internal static Optional OfNullable(T? value) { - return !valueChecksForT(value) ? Optional.Empty : new Optional(value); + return !valueChecksForT(value) ? Optional.Empty : new Optional(value!); } public override string ToString() @@ -206,7 +206,7 @@ public static class Optional #pragma warning restore CS0618 public static Optional Of(T value) where T : class => Optional.Of(value); - public static Optional OfNullable(T value) where T : class => Optional.OfNullable(value); + public static Optional OfNullable(T? value) where T : class => Optional.OfNullable(value); /// /// Adds a condition to the optional of the given type that is checked whenever is diff --git a/Nitrox.Model/DataStructures/SimulatedEntity.cs b/Nitrox.Model/DataStructures/SimulatedEntity.cs index f62bd9a253..900ec28b6a 100644 --- a/Nitrox.Model/DataStructures/SimulatedEntity.cs +++ b/Nitrox.Model/DataStructures/SimulatedEntity.cs @@ -1,33 +1,33 @@ using System; +using Nitrox.Model.Core; -namespace Nitrox.Model.DataStructures +namespace Nitrox.Model.DataStructures; + +/// +/// A simulated entity that is tracked by the Nitrox server so that it knows which connected game client owns (and simulates) the entity. +/// See for more information. +/// +[Serializable] +public class SimulatedEntity { /// - /// A simulated entity that is tracked by the Nitrox server so that it knows which connected game client owns (and simulates) the entity. - /// See for more information. + /// True if entity isn't static (e.g. welded to world). /// - [Serializable] - public class SimulatedEntity - { - /// - /// True if entity isn't static (e.g. welded to world). - /// - public bool ChangesPosition { get; } - public NitroxId Id { get; } - public ushort PlayerId { get; } - public SimulationLockType LockType { get; } + public bool ChangesPosition { get; } + public NitroxId Id { get; } + public SessionId SessionId { get; } + public SimulationLockType LockType { get; } - public SimulatedEntity(NitroxId id, ushort playerId, bool changesPosition, SimulationLockType lockType) - { - Id = id; - PlayerId = playerId; - ChangesPosition = changesPosition; - LockType = lockType; - } + public SimulatedEntity(NitroxId id, SessionId sessionId, bool changesPosition, SimulationLockType lockType) + { + Id = id; + SessionId = sessionId; + ChangesPosition = changesPosition; + LockType = lockType; + } - public override string ToString() - { - return $"[SimulatedEntity Id: {Id}, PlayerId: {PlayerId}, ChangesPosition: {ChangesPosition}, LockType: {LockType}]"; - } + public override string ToString() + { + return $"[SimulatedEntity Id: {Id}, {nameof(SessionId)}: {SessionId}, ChangesPosition: {ChangesPosition}, LockType: {LockType}]"; } } diff --git a/Nitrox.Model/DataStructures/Unity/NitroxTransform.cs b/Nitrox.Model/DataStructures/Unity/NitroxTransform.cs index a3b4ddf8d8..9ea2a02bbe 100644 --- a/Nitrox.Model/DataStructures/Unity/NitroxTransform.cs +++ b/Nitrox.Model/DataStructures/Unity/NitroxTransform.cs @@ -28,7 +28,7 @@ public Matrix4x4 LocalToWorldMatrix } } - public NitroxTransform Parent; + public NitroxTransform? Parent; [IgnoredMember] public NitroxVector3 Position @@ -64,7 +64,7 @@ public NitroxQuaternion Rotation } } - public void SetParent(NitroxTransform parent, bool worldPositionStays = true) + public void SetParent(NitroxTransform? parent, bool worldPositionStays = true) { if (!worldPositionStays) { diff --git a/Nitrox.Model/Extensions/StringExtensions.cs b/Nitrox.Model/Extensions/StringExtensions.cs index 62c47696c4..5497ad7e00 100644 --- a/Nitrox.Model/Extensions/StringExtensions.cs +++ b/Nitrox.Model/Extensions/StringExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; @@ -6,13 +7,6 @@ namespace Nitrox.Model.Extensions; public static class StringExtensions { - public static byte[] AsMd5Hash(this string input) - { - using MD5 md5 = MD5.Create(); - byte[] inputBytes = Encoding.ASCII.GetBytes(input); - return md5.ComputeHash(inputBytes); - } - /// /// Gets the arguments passed to a command, given its name. /// @@ -44,4 +38,16 @@ public static IEnumerable GetCommandArgs(this string[] args, string name } } } + + extension(string self) + { + public byte[] ToMd5Hash() + { + using MD5 md5 = MD5.Create(); + byte[] inputBytes = Encoding.ASCII.GetBytes(self); + return md5.ComputeHash(inputBytes); + } + + public int ToMd5HashedInt32() => BitConverter.ToInt32(self.ToMd5Hash(), 0); + } } diff --git a/Nitrox.Model/GlobalUsings.cs b/Nitrox.Model/GlobalUsings.cs index d7587de670..88c6919fbb 100644 --- a/Nitrox.Model/GlobalUsings.cs +++ b/Nitrox.Model/GlobalUsings.cs @@ -3,4 +3,5 @@ #else global using LockObject = object; #endif +global using System.Runtime.CompilerServices; global using Nitrox.Model.Extensions; diff --git a/Nitrox.Model/Helper/AsyncBarrier.cs b/Nitrox.Model/Helper/AsyncBarrier.cs deleted file mode 100644 index 38993e1d68..0000000000 --- a/Nitrox.Model/Helper/AsyncBarrier.cs +++ /dev/null @@ -1,52 +0,0 @@ -#if NET -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Nitrox.Model.Helper; - -/// -/// Provides an automatically resetting asynchronous way to wait on a signal. -/// -public sealed class AsyncBarrier -{ - private readonly LockObject locker = new(); - private TaskCompletionSource signal = new(); - - public void Signal() - { - lock (locker) - { - signal.TrySetResult(); - } - } - - public async Task WaitForSignalAsync(CancellationToken cancellationToken) => await AtomicRefreshTcs(ref signal).WaitAsync(cancellationToken); - - public TaskAwaiter GetAwaiter() - { - lock (locker) - { - return signal.Task.GetAwaiter(); - } - } - - private Task AtomicRefreshTcs(ref TaskCompletionSource tcs) - { - Task tcsTask; - lock (locker) - { - tcsTask = tcs.Task; - } - if (tcsTask.IsCompletedSuccessfully) - { - lock (locker) - { - tcs = new TaskCompletionSource(); - return tcs.Task; - } - } - return tcsTask; - } -} -#endif diff --git a/Nitrox.Model/Helper/Validate.cs b/Nitrox.Model/Helper/Validate.cs index aa52d4f67a..2949e0328f 100644 --- a/Nitrox.Model/Helper/Validate.cs +++ b/Nitrox.Model/Helper/Validate.cs @@ -1,6 +1,5 @@ extern alias JB; using System; -using System.Runtime.CompilerServices; using JB::JetBrains.Annotations; using Nitrox.Model.DataStructures; @@ -11,17 +10,17 @@ public static class Validate // "where T : class" prevents non-nullable valuetypes from getting boxed to objects. // In other words: Error when trying to assert non-null on something that can't be null in the first place. [ContractAnnotation("o:null => halt")] - public static void NotNull(T? o, [CallerArgumentExpression("o")] string argumentExpression = null) where T : class + public static void NotNull(T? o, [CallerArgumentExpression("o")] string? argumentExpression = null) where T : class { if (o != null) { return; } - throw new ArgumentNullException(argumentExpression); + throw new ArgumentNullException(nameof(o), argumentExpression); } - public static void IsTrue(bool b, [CallerArgumentExpression("b")] string argumentExpression = null) + public static void IsTrue(bool b, [CallerArgumentExpression("b")] string? argumentExpression = null) { if (!b) { @@ -29,7 +28,7 @@ public static void IsTrue(bool b, [CallerArgumentExpression("b")] string argumen } } - public static void IsFalse(bool b, [CallerArgumentExpression("b")] string argumentExpression = null) + public static void IsFalse(bool b, [CallerArgumentExpression("b")] string? argumentExpression = null) { if (b) { diff --git a/Nitrox.Model/Packets/Core/IPacketProcessContext.cs b/Nitrox.Model/Packets/Core/IPacketProcessContext.cs new file mode 100644 index 0000000000..6d31d3ee49 --- /dev/null +++ b/Nitrox.Model/Packets/Core/IPacketProcessContext.cs @@ -0,0 +1,11 @@ +namespace Nitrox.Model.Packets.Core; + +public interface IPacketProcessContext; + +public interface IPacketProcessContext : IPacketProcessContext +{ + /// + /// The sender of the packet. + /// + public TSenderRef Sender { get; set; } +} diff --git a/Nitrox.Model/Packets/Core/IPacketProcessor.cs b/Nitrox.Model/Packets/Core/IPacketProcessor.cs new file mode 100644 index 0000000000..3b0183aea5 --- /dev/null +++ b/Nitrox.Model/Packets/Core/IPacketProcessor.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Nitrox.Model.Packets.Core; + +public interface IPacketProcessor; + +/// +/// A packet processor. The method is called when a connection sends data. +/// +public interface IPacketProcessor : IPacketProcessor + where TContext : IPacketProcessContext + where TPacket : Packet +{ + /// + /// Processes an incoming packet of type . + /// + /// The context provided to this processor containing data about its sender. + /// The incoming packet data that should be processed + Task Process(TContext context, TPacket packet); +} diff --git a/Nitrox.Model/Packets/Core/PacketProcessorsInvoker.cs b/Nitrox.Model/Packets/Core/PacketProcessorsInvoker.cs new file mode 100644 index 0000000000..b1d16b1191 --- /dev/null +++ b/Nitrox.Model/Packets/Core/PacketProcessorsInvoker.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +#if NET +using System.Collections.Frozen; +#endif + +namespace Nitrox.Model.Packets.Core; + +public sealed class PacketProcessorsInvoker +{ +#if NET + private readonly FrozenDictionary packetTypeToProcessorEntry; +#else + private readonly Dictionary packetTypeToProcessorEntry; +#endif + + public PacketProcessorsInvoker(IEnumerable packetProcessors) + { + if (packetProcessors is null) + { + throw new ArgumentOutOfRangeException(nameof(packetProcessors)); + } + + var entries = packetProcessors.SelectMany(pInstance => pInstance.GetType() + .GetInterfaces() + .Where(i => typeof(IPacketProcessor).IsAssignableFrom(i)) + .Select(i => new + { + InterfaceType = i, + PacketType = i.GetGenericArguments().FirstOrDefault(t => typeof(Packet).IsAssignableFrom(t)), + Processor = pInstance + }) + .Where(p => p.PacketType != null)); + Dictionary lookup = []; + foreach (var entry in entries) + { + if (lookup.TryGetValue(entry.PacketType, out Entry value)) + { + if (value.Processor != entry.Processor) + { + throw new Exception($"Packet type {value.PacketType} has multiple handlers (A: {value.Processor.GetType()}, B: {entry.Processor.GetType()}), which is not allowed."); + } + if (entry.InterfaceType.IsAssignableFrom(value.InterfaceType)) + { + continue; + } + } + + lookup[entry.PacketType] = new Entry(entry.Processor, entry.InterfaceType, entry.PacketType); + } +#if NET + packetTypeToProcessorEntry = lookup.ToFrozenDictionary(); +#else + packetTypeToProcessorEntry = lookup; +#endif + } + + public Entry? GetProcessor(Type packetType) + { + Type current = packetType; + Type? prior = null; + while (current != prior) + { + if (packetTypeToProcessorEntry.TryGetValue(packetType, out Entry processor)) + { + return processor; + } + prior = current; + current = packetType.BaseType; + } + + return null; + } + + [DebuggerDisplay($"{{{nameof(InterfaceType)}}}")] + public sealed class Entry + { + private static readonly Type[] expectedProcessorParameterTypes = [typeof(IPacketProcessContext), typeof(Packet)]; + private readonly Func invoker; + + public Type PacketType { get; } + public Type InterfaceType { get; } + + public object Processor => invoker.Target; + + internal Entry(IPacketProcessor processor, Type processorInterfaceType, Type packetType) + { + PacketType = packetType; + InterfaceType = processorInterfaceType; + + MethodInfo method = processor.GetType().GetMethods().FirstOrDefault(m => + { + if (!typeof(Task).IsAssignableFrom(m.ReturnType)) + { + return false; + } + ParameterInfo[] parameterInfos = m.GetParameters(); + if (parameterInfos.Length != expectedProcessorParameterTypes.Length) + { + return false; + } + for (int i = 0; i < parameterInfos.Length; i++) + { + Type expectedParamType = expectedProcessorParameterTypes[i]; + // For packet parameter, we want the most specific method that can handle it. + if (expectedParamType == typeof(Packet)) + { + expectedParamType = packetType; + } + + if (!expectedParamType.IsAssignableFrom(parameterInfos[i].ParameterType)) + { + return false; + } + } + + return true; + }); + if (method == null) + { + throw new ArgumentOutOfRangeException( + nameof(processor), $"Processor {processor.GetType()} implementing {processorInterfaceType} does not have a method that looks like 'Task M({string.Join(", ", expectedProcessorParameterTypes.Select(t => t.Name))})'"); + } + ParameterInfo[] parameters = method.GetParameters(); + Type funcType = typeof(Func<,,>).MakeGenericType(parameters[0].ParameterType, parameters[1].ParameterType, method.ReturnType); + Delegate processorDelegate = method.CreateDelegate(funcType, processor); + RuntimeHelpers.PrepareDelegate(processorDelegate); + invoker = Unsafe.As>(processorDelegate); + } + + public Task Execute(IPacketProcessContext context, Packet packet) => invoker(context, packet); + public override string ToString() => $"Processor: {invoker.Target.GetType().Name}, {nameof(InterfaceType)}: {InterfaceType.Name}"; + } +} diff --git a/Nitrox.Model/Packets/CorrelatedPacket.cs b/Nitrox.Model/Packets/CorrelatedPacket.cs index 66dd6bebc6..a7c1d85b16 100644 --- a/Nitrox.Model/Packets/CorrelatedPacket.cs +++ b/Nitrox.Model/Packets/CorrelatedPacket.cs @@ -1,15 +1,10 @@ using System; -namespace Nitrox.Model.Packets -{ - [Serializable] - public abstract class CorrelatedPacket : Packet - { - public string CorrelationId { get; protected set; } +namespace Nitrox.Model.Packets; - protected CorrelatedPacket(string correlationId) - { - CorrelationId = correlationId; - } - } +// TODO: Refactor this away. Use SessionId instead of correlationId. +[Serializable] +public abstract class CorrelatedPacket(string correlationId) : Packet +{ + public string CorrelationId { get; protected set; } = correlationId; } diff --git a/Nitrox.Model/Packets/Packet.cs b/Nitrox.Model/Packets/Packet.cs index 51a3ffb50b..1618291832 100644 --- a/Nitrox.Model/Packets/Packet.cs +++ b/Nitrox.Model/Packets/Packet.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text; @@ -14,12 +15,12 @@ namespace Nitrox.Model.Packets public abstract class Packet { private static readonly Dictionary cachedPropertiesByType = new(); - private static readonly object cachedPropertiesByTypeLocker = new(); + private static readonly LockObject cachedPropertiesByTypeLocker = new(); [ThreadStatic] private static StringBuilder? toStringBuilder; - private static readonly object lockObject = new(); + private static readonly LockObject lockObject = new(); [IgnoredMember] public NitroxDeliveryMethod.DeliveryMethod DeliveryMethod { get; protected set; } = NitroxDeliveryMethod.DeliveryMethod.RELIABLE_ORDERED; @@ -83,11 +84,13 @@ public byte[] Serialize() return BinaryConverter.Serialize(new Wrapper(this)); } - public static Packet Deserialize(byte[] data) + public void SerializeInto(Stream stream) { - return BinaryConverter.Deserialize(data).Packet; + BinaryConverter.Serialize(new Wrapper(this), stream); } + public static Packet? Deserialize(byte[] data) => BinaryConverter.Deserialize(data).Packet; + public override string ToString() { Type packetType = GetType(); @@ -134,14 +137,9 @@ public override string ToString() ///

/// This type solves both problems and only adds a single byte to the data. ///
- public readonly struct Wrapper + public readonly struct Wrapper(Packet packet) { - public Packet Packet { get; init; } = null; - - public Wrapper(Packet packet) - { - Packet = packet; - } + public Packet? Packet { get; init; } = packet; } public enum UdpChannelId : byte diff --git a/Nitrox.Model/Packets/Processors/Abstract/IProcessorContext.cs b/Nitrox.Model/Packets/Processors/Abstract/IProcessorContext.cs deleted file mode 100644 index 7f2365b373..0000000000 --- a/Nitrox.Model/Packets/Processors/Abstract/IProcessorContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Nitrox.Model.Packets.Processors.Abstract -{ - public interface IProcessorContext - { - } -} diff --git a/Nitrox.Model/Packets/Processors/Abstract/PacketProcessor.cs b/Nitrox.Model/Packets/Processors/Abstract/PacketProcessor.cs deleted file mode 100644 index 28a8bf81e1..0000000000 --- a/Nitrox.Model/Packets/Processors/Abstract/PacketProcessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Nitrox.Model.Packets.Processors.Abstract -{ - public abstract class PacketProcessor - { - public abstract void ProcessPacket(Packet packet, IProcessorContext context); - - public static Dictionary GetProcessors(Dictionary processorArguments, Func additionalConstraints) - { - return Assembly.GetCallingAssembly() - .GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) - .Where(additionalConstraints) - .ToDictionary(proc => proc.BaseType.GetGenericArguments()[0], proc => - { - ConstructorInfo[] ctors = proc.GetConstructors(); - if (ctors.Length > 1) - { - throw new NotSupportedException($"{proc.Name} has more than one constructor!"); - } - - ConstructorInfo ctor = ctors.First(); - - // Prepare arguments for constructor (if applicable): - object[] args = ctor.GetParameters().Select(pi => - { - if (processorArguments.TryGetValue(pi.ParameterType, out object v)) - { - return v; - } - - throw new ArgumentException($"Argument value not defined for type {pi.ParameterType}! Used in {proc}"); - }).ToArray(); - - return (PacketProcessor)ctor.Invoke(args); - }); - } - } -} diff --git a/Nitrox.Model/Packets/TextAutoComplete.cs b/Nitrox.Model/Packets/TextAutoComplete.cs new file mode 100644 index 0000000000..2cf1ddad92 --- /dev/null +++ b/Nitrox.Model/Packets/TextAutoComplete.cs @@ -0,0 +1,20 @@ +using System; + +namespace Nitrox.Model.Packets; + +[Serializable] +public sealed class TextAutoComplete(string? text, TextAutoComplete.AutoCompleteContext context) : Packet +{ + /// + /// Text to send over as either a suggestion for auto complete (from client) or a reply to the suggestion (from + /// server). + /// + public string? Text { get; init; } = text; + + public AutoCompleteContext Context { get; init; } = context; + + public enum AutoCompleteContext + { + COMMAND_NAME + } +} diff --git a/Nitrox.Server.Subnautica/Extensions/LoggerExtensions.cs b/Nitrox.Server.Subnautica/Extensions/LoggerExtensions.cs index a9df50ae5d..cdca9b3f07 100644 --- a/Nitrox.Server.Subnautica/Extensions/LoggerExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/LoggerExtensions.cs @@ -1,5 +1,7 @@ using System.Net; using System.Runtime.CompilerServices; +using Nitrox.Model.Core; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.Logging; using Nitrox.Server.Subnautica.Models.Logging.Scopes; @@ -7,22 +9,6 @@ namespace Nitrox.Server.Subnautica.Extensions; internal static partial class LoggerExtensions { - /// - /// Sets the logger into "plain" mode. Text will be logged without the time, category or log level info. - /// - public static IDisposable? BeginPlainScope(this ILogger logger) => logger.BeginScope(new PlainScope()); - - public static IDisposable? BeginPrefixScope(this ILogger logger, string prefix) => logger.BeginScope(new PrefixScope(prefix)); - - /// - public static CaptureScope BeginCaptureScope(this ILogger logger) - { - CaptureScope scope = new(); - IDisposable disposable = logger.BeginScope(scope); - scope.InnerDisposable = disposable; - return scope; - } - public static void ZLogWarningOnce(this ILogger logger, [InterpolatedStringHandlerArgument("logger")] ref DeduplicateWarningInterpolatedStringHandler message, @@ -96,6 +82,37 @@ public static void ZLogErrorOnce(this ILogger logger, [ZLoggerMessage(Level = LogLevel.Error, Message = "Unable to open directory {Path} because it does not exist")] public static partial void LogOpenDirectoryNotExists(this ILogger logger, string path); - [ZLoggerMessage(Level = LogLevel.Information, Message = "Server password changed to '{Password}' by player '{PlayerName}'")] - public static partial void LogServerPasswordChanged(this ILogger logger, string password, string playerName); + [ZLoggerMessage(Level = LogLevel.Information, Message = "Server password changed to '{Password}' by '{PlayerName}' on session #{SessionId}")] + public static partial void LogServerPasswordChanged(this ILogger logger, string password, string playerName, SessionId sessionId); + + [ZLoggerMessage(Level = LogLevel.Trace, Message = "Adding {Handler}")] + public static partial void LogCommandHandlerAdded(this ILogger logger, CommandHandlerEntry handler); + + /// + /// Logs a save request as being issued by the issuer. + /// + /// The logger instance to use. + /// Name of the issuer. + /// Session ID of the issuer. + [ZLoggerMessage(Level = LogLevel.Information, Message = "Save requested by '{Name}' #{SessionId}")] + public static partial void LogSaveRequest(this ILogger logger, string name, SessionId sessionId); + + extension(ILogger logger) + { + /// + /// Sets the logger into "plain" mode. Text will be logged without the time, category or log level info. + /// + public IDisposable? BeginPlainScope() => logger.BeginScope(new PlainScope()); + + public IDisposable? BeginPrefixScope(string prefix) => logger.BeginScope(new PrefixScope(prefix)); + + /// + public CaptureScope BeginCaptureScope() + { + CaptureScope scope = new(); + IDisposable disposable = logger.BeginScope(scope); + scope.InnerDisposable = disposable; + return scope; + } + } } diff --git a/Nitrox.Server.Subnautica/Extensions/LoggingBuilderExtensions.cs b/Nitrox.Server.Subnautica/Extensions/LoggingBuilderExtensions.cs index 4151ed1bf8..2e3dea1e08 100644 --- a/Nitrox.Server.Subnautica/Extensions/LoggingBuilderExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/LoggingBuilderExtensions.cs @@ -1,19 +1,83 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Console; +using Nitrox.Server.Subnautica.Models.Logging.Redaction.Core; +using Nitrox.Server.Subnautica.Models.Logging.Scopes; using Nitrox.Server.Subnautica.Models.Logging.ZLogger; +using Nitrox.Server.Subnautica.Services; +using ZLogger.Providers; namespace Nitrox.Server.Subnautica.Extensions; internal static class LoggingBuilderExtensions { - public static ILoggingBuilder AddNitroxZLoggerPlain(this ILoggingBuilder builder, Action configure) + extension(ILoggingBuilder builder) { - builder.Services.AddSingleton(_ => + private ILoggingBuilder AddNitroxZLoggerPlain(Action configure) { - PlainLogProcessor processor = new() { Options = new() }; - configure(processor.Options); - processor.Formatter = processor.Options.CreateFormatter(); - return new ZLoggerPlainLoggerProvider(processor, processor.Options); - }); - return builder; + builder.Services.AddSingleton(_ => + { + PlainLogProcessor processor = new() { Options = new() }; + configure(processor.Options); + processor.Formatter = processor.Options.CreateFormatter(); + return new ZLoggerPlainLoggerProvider(processor, processor.Options); + }); + return builder; + } + + public ILoggingBuilder AddNitroxLogging() + { + builder.Services.AddRedactors(); + return builder + .AddZLoggerConsole(static (options, provider) => + { + options.IncludeScopes = true; + options.UseNitroxFormatter(formatterOptions => + { + formatterOptions.OmitWhenCaptured = true; + bool isEmbedded = provider.GetRequiredService>().Value.IsEmbedded; + formatterOptions.ColorBehavior = isEmbedded ? LoggerColorBehavior.Disabled : LoggerColorBehavior.Enabled; + }); + }) + .AddNitroxZLoggerPlain(options => + { + options.IncludeScopes = true; + options.UseNitroxFormatter(o => + { + o.OmitWhenCaptured = true; + o.IsPlain = true; + }).OutputFunc = async (entry, formatter, generator, writer) => await ServersManagementService.LogQueue.Writer.WriteAsync(new ServersManagementService.LogEntry(entry, formatter, generator, writer)); + }) + .AddNitroxZLoggerPlain(options => + { + options.IncludeScopes = true; + options.UseNitroxFormatter().OutputFunc = (entry, formatter, generator, writer) => + { + if (entry.TryGetProperty(out CaptureScope scope)) + { + scope.Capture(generator(entry, formatter, writer)); + } + return Task.CompletedTask; + }; + }) + .AddZLoggerRollingFile(static (options, provider) => + { + ServerStartOptions serverStartOptions = provider.GetRequiredService>().Value; + options.FilePathSelector = (timestamp, sequence) => + { + string filename = $"{timestamp.ToLocalTime():yyyy-MM-dd}_server_{serverStartOptions.SaveName}_{sequence:000}.log"; + return Path.Combine(serverStartOptions.GetServerLogsPath(), filename); + }; + options.RollingInterval = RollingInterval.Day; + options.IncludeScopes = true; + options.UseNitroxFormatter(formatterOptions => + { + formatterOptions.OmitWhenCaptured = true; + formatterOptions.Redactors = provider.GetRequiredService>()?.ToArray() ?? []; + }); + }); + } } } diff --git a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs index 09abfe20f9..d5cf231979 100644 --- a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs @@ -1,34 +1,30 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using AssetsTools.NET.Extra; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging.Console; using Nitrox.Model.Constants; +using Nitrox.Model.Packets.Core; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Administration.Core; using Nitrox.Server.Subnautica.Models.AppEvents.Core; -using Nitrox.Server.Subnautica.Models.AppEvents.Triggers; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Processor; +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.Communication; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; using Nitrox.Server.Subnautica.Models.Logging.Redaction.Core; -using Nitrox.Server.Subnautica.Models.Logging.Scopes; -using Nitrox.Server.Subnautica.Models.Packets; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Models.Packets.Processors; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.Resources.Core; using Nitrox.Server.Subnautica.Models.Serialization; using Nitrox.Server.Subnautica.Models.Serialization.SaveDataUpgrades; using Nitrox.Server.Subnautica.Models.Serialization.World; using Nitrox.Server.Subnautica.Services; using ServiceScan.SourceGenerator; -using ZLogger.Providers; namespace Nitrox.Server.Subnautica.Extensions; @@ -36,191 +32,11 @@ internal static partial class ServiceCollectionExtensions { private static readonly Lazy newWorldSeed = new(() => StringHelper.GenerateRandomString(10)); - /// - /// Adds the fallback implementation for the interface if no other implementation is set. - /// - public static IServiceCollection AddFallback(this IServiceCollection services) where TInterface : class where TFallback : class, TInterface - { - services.TryAddSingleton(); - return services; - } - - public static IServiceCollection AddHostedSingletonService(this IServiceCollection services) where T : class, IHostedService => services.AddSingleton().AddHostedService(provider => provider.GetRequiredService()); - - public static IServiceCollection TryAddSingletonLazyArrayProvider(this IServiceCollection services) - { - services.TryAddSingleton>(provider => () => provider.GetRequiredService>().ToArray()); - return services; - } - - public static IServiceCollection AddNitroxOptions(this IServiceCollection services) - { - services.AddOptionsWithValidateOnStart() - .BindConfiguration("") - .Configure(options => - { - if (string.IsNullOrWhiteSpace(options.GamePath)) - { - options.GamePath = NitroxUser.GamePath; - } - if (string.IsNullOrWhiteSpace(options.NitroxAssetsPath)) - { - options.NitroxAssetsPath = NitroxUser.AssetsPath; - } - if (string.IsNullOrWhiteSpace(options.NitroxAppDataPath)) - { - options.NitroxAppDataPath = NitroxUser.AppDataPath; - } - }); - services.AddOptionsWithValidateOnStart() - .BindConfiguration(SubnauticaServerOptions.CONFIG_SECTION_PATH) - .Configure((SubnauticaServerOptions options, IHostEnvironment environment) => - { - options.Seed = options.Seed switch - { - null or "" when environment.IsDevelopment() => SubnauticaServerConstants.DEFAULT_DEVELOPMENT_SEED, - null or "" => newWorldSeed.Value, - _ => options.Seed - }; - }); - return services; - } - - public static ILoggingBuilder AddNitroxLogging(this ILoggingBuilder builder) - { - builder.Services.AddRedactors(); - return builder - .AddZLoggerConsole(static (options, provider) => - { - options.IncludeScopes = true; - options.UseNitroxFormatter(formatterOptions => - { - formatterOptions.OmitWhenCaptured = true; - bool isEmbedded = provider.GetRequiredService>().Value.IsEmbedded; - formatterOptions.ColorBehavior = isEmbedded ? LoggerColorBehavior.Disabled : LoggerColorBehavior.Enabled; - }); - }) - .AddNitroxZLoggerPlain(options => - { - options.IncludeScopes = true; - options.UseNitroxFormatter(o => - { - o.OmitWhenCaptured = true; - o.IsPlain = true; - }).OutputFunc = async (entry, formatter, generator, writer) => await ServersManagementService.LogQueue.Writer.WriteAsync(new ServersManagementService.LogEntry(entry, formatter, generator, writer)); - }) - .AddNitroxZLoggerPlain(options => - { - options.IncludeScopes = true; - options.UseNitroxFormatter().OutputFunc = (entry, formatter, generator, writer) => - { - if (entry.TryGetProperty(out CaptureScope scope)) - { - scope.Capture(generator(entry, formatter, writer)); - } - return Task.CompletedTask; - }; - }) - .AddZLoggerRollingFile(static (options, provider) => - { - ServerStartOptions serverStartOptions = provider.GetRequiredService>().Value; - options.FilePathSelector = (timestamp, sequence) => - { - string filename = $"{timestamp.ToLocalTime():yyyy-MM-dd}_server_{serverStartOptions.SaveName}_{sequence:000}.log"; - return Path.Combine(serverStartOptions.GetServerLogsPath(), filename); - }; - options.RollingInterval = RollingInterval.Day; - options.IncludeScopes = true; - options.UseNitroxFormatter(formatterOptions => - { - formatterOptions.OmitWhenCaptured = true; - formatterOptions.Redactors = provider.GetRequiredService>()?.ToArray() ?? []; - }); - }); - } - - /// - /// Provides a console reader, command registration and command handling to facilitate server administration. - /// - public static IServiceCollection AddCommands(this IServiceCollection services) - { - services.AddHostedSingletonService() - .AddHostedSingletonService() - .AddSingleton() - .AddSingleton() - .AddCommandHandlers(); - return services; - } - - public static IServiceCollection AddWorld(this IServiceCollection services) - { - // Hack: Save service strongly depends on WorldService so it's a Func to prevent StackOverflow. TODO: Remove need for WorldService; each service should save / load its own data through a common interface. - services.AddHostedSingletonService() - .AddHostedSingletonService() - .AddSingleton>(provider => provider.GetRequiredService) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - return services; - } - - /// - /// Provides packet type registration, processing and a listener for these packets on a configured UDP port. - /// - public static IServiceCollection AddPackets(this IServiceCollection services) - { - services.AddPacketProcessors() - .AddSingleton() - .AddSingleton() - .AddHostedSingletonService(); - return services; - } + [GenerateServiceRegistrations(AssignableTo = typeof(IRedactor), Lifetime = ServiceLifetime.Singleton)] + internal static partial IServiceCollection AddRedactors(this IServiceCollection services); - /// - /// Provides an API for local processes on the current machine to communicate and manage this server. - /// - public static IServiceCollection AddLocalServerManagement(this IServiceCollection services) => - services - .AddHostedSingletonService(); - - public static IServiceCollection AddSubnauticaResources(this IServiceCollection services) => - services - .AddHostedSingletonService() - .AddGameResources() - .AddSingleton() - .AddTransient() - .AddSingleton() - .AddTransient(); - - public static IServiceCollection AddSaving(this IServiceCollection services) => - services - .AddSaveUpgraders() - .AddHostedSingletonService() - .AddHostedSingletonService(); - - public static IServiceCollection AddAppEvents(this IServiceCollection services) => - services - .AddEvents() - .AddEventTriggers(); - - private static IServiceCollection AddEvent(this IServiceCollection services) => - services - .TryAddSingletonLazyArrayProvider>() - .AddSingleton(provider => (IEvent)provider.GetRequiredService()); + [GenerateServiceRegistrations(AssignableTo = typeof(IAdminFeature<>), CustomHandler = nameof(AddImplementedAdminFeatures))] + internal static partial IServiceCollection AddAdminFeatures(this IServiceCollection services); [GenerateServiceRegistrations(AssignableTo = typeof(IGameResource), Lifetime = ServiceLifetime.Singleton, AsSelf = true, AsImplementedInterfaces = true)] private static partial IServiceCollection AddGameResources(this IServiceCollection services); @@ -228,19 +44,197 @@ private static IServiceCollection AddEvent(this ISe [GenerateServiceRegistrations(AssignableTo = typeof(SaveDataUpgrade), Lifetime = ServiceLifetime.Scoped)] private static partial IServiceCollection AddSaveUpgraders(this IServiceCollection services); - [GenerateServiceRegistrations(AssignableTo = typeof(AuthenticatedPacketProcessor<>), ExcludeAssignableTo = typeof(DefaultServerPacketProcessor), Lifetime = ServiceLifetime.Scoped)] - [GenerateServiceRegistrations(AssignableTo = typeof(UnauthenticatedPacketProcessor<>), Lifetime = ServiceLifetime.Scoped)] + [GenerateServiceRegistrations(AssignableTo = typeof(IPacketProcessor), Lifetime = ServiceLifetime.Scoped)] private static partial IServiceCollection AddPacketProcessors(this IServiceCollection services); - [GenerateServiceRegistrations(AssignableTo = typeof(IRedactor), Lifetime = ServiceLifetime.Singleton)] - private static partial IServiceCollection AddRedactors(this IServiceCollection services); - - [GenerateServiceRegistrations(AssignableTo = typeof(Command), Lifetime = ServiceLifetime.Scoped)] - private static partial IServiceCollection AddCommandHandlers(this IServiceCollection services); - [GenerateServiceRegistrations(AssignableTo = typeof(EventTrigger<>), AsSelf = true, AsImplementedInterfaces = false, Lifetime = ServiceLifetime.Singleton)] private static partial IServiceCollection AddEventTriggers(this IServiceCollection services); [GenerateServiceRegistrations(AssignableTo = typeof(IEvent<>), Lifetime = ServiceLifetime.Singleton, CustomHandler = nameof(AddEvent))] private static partial IServiceCollection AddEvents(this IServiceCollection services); + + [GenerateServiceRegistrations(AssignableTo = typeof(ICommandHandlerBase), CustomHandler = nameof(AddCommandHandler))] + private static partial IServiceCollection AddCommandHandlers(this IServiceCollection services); + + [GenerateServiceRegistrations(AssignableTo = typeof(IArgConverter), Lifetime = ServiceLifetime.Singleton, AsSelf = true, AsImplementedInterfaces = true)] + private static partial IServiceCollection AddCommandArgConverters(this IServiceCollection services); + + /// + /// Registers a single command and all of its handlers as can be known by the implemented interfaces. + /// + private static void AddCommandHandler(this IServiceCollection services) where T : class, ICommandHandlerBase + { + Type[] handlerTypes = typeof(T).GetInterfaces().Where(t => t != typeof(ICommandHandlerBase) && typeof(ICommandHandlerBase).IsAssignableFrom(t)).ToArray(); + if (handlerTypes.Length < 1) + { + return; + } + services.AddSingleton(); + + foreach (Type handlerType in handlerTypes) + { + services.AddSingleton(provider => + { + T owner = provider.GetRequiredService(); + return new CommandHandlerEntry(owner, handlerType); + }); + } + } + + private static void AddImplementedAdminFeatures(this IServiceCollection services) where TImplementation : class, IAdminFeature + { + foreach (Type featureInterfaceType in typeof(TImplementation).GetInterfaces() + .Where(i => typeof(IAdminFeature).IsAssignableFrom(i)) + .Select(i => i.GetGenericArguments()) + .Where(types => types.Length == 1) + .Select(types => types[0])) + { + services.AddSingleton(featureInterfaceType, provider => provider.GetRequiredService()); + } + } + + extension(IServiceCollection services) + { + /// + /// Adds the fallback implementation for the interface if no other implementation is set. + /// + public IServiceCollection AddFallback() where TInterface : class where TFallback : class, TInterface + { + services.TryAddSingleton(); + return services; + } + + public IServiceCollection AddHostedSingletonService() where T : class, IHostedService => services.AddSingleton().AddHostedService(provider => provider.GetRequiredService()); + + public IServiceCollection AddNitroxOptions() + { + services.AddOptionsWithValidateOnStart() + .BindConfiguration("") + .Configure(options => + { + if (string.IsNullOrWhiteSpace(options.GamePath)) + { + options.GamePath = NitroxUser.GamePath; + } + if (string.IsNullOrWhiteSpace(options.NitroxAssetsPath)) + { + options.NitroxAssetsPath = NitroxUser.AssetsPath; + } + if (string.IsNullOrWhiteSpace(options.NitroxAppDataPath)) + { + options.NitroxAppDataPath = NitroxUser.AppDataPath; + } + }); + services.AddOptionsWithValidateOnStart() + .BindConfiguration(SubnauticaServerOptions.CONFIG_SECTION_PATH) + .Configure((SubnauticaServerOptions options, IHostEnvironment environment) => + { + options.Seed = options.Seed switch + { + null or "" when environment.IsDevelopment() => SubnauticaServerConstants.DEFAULT_DEVELOPMENT_SEED, + null or "" => newWorldSeed.Value, + _ => options.Seed + }; + }); + services.AddHostedSingletonService(); + return services; + } + + /// + /// Provides a console reader, command registration and command handling to facilitate server administration. + /// + public IServiceCollection AddCommands() => + services.AddHostedSingletonService() + .AddHostedSingletonService() + .AddSingleton() + .AddSingleton>(provider => provider.GetRequiredService) + .AddCommandHandlers() + .AddCommandArgConverters() + .AddSingleton(); + + /// + /// Adds all the services and managers necessary to simulate a Subnautica world. + /// + public IServiceCollection AddWorld() + { + // Hack: Save service strongly depends on WorldService so it's a Func to prevent StackOverflow. TODO: Remove need for WorldService; each service should save / load its own data through a common interface. + services.AddHostedSingletonService() + .AddHostedSingletonService() + .AddHostedSingletonService() + .AddSingleton>(provider => provider.GetRequiredService) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + return services; + } + + /// + /// Provides packet type registration, processing and a listener for these packets on a configured UDP port. + /// + public IServiceCollection AddPackets() + { + services.AddHostedSingletonService() + .AddHostedSingletonService() + .AddHostedSingletonService() + .AddPacketProcessors() + .TryAddSingletonLazyArrayProvider() + .AddSingleton() + .AddSingleton(provider => provider.GetRequiredService()); + return services; + } + + /// + /// Provides an API for local processes on the current machine to communicate and manage this server. + /// + public IServiceCollection AddLocalServerManagement() => + services + .AddHostedSingletonService(); + + public IServiceCollection AddSubnauticaResources() => + services + .AddHostedSingletonService() + .AddGameResources() + .AddSingleton() + .AddTransient() + .AddSingleton() + .AddTransient(); + + public IServiceCollection AddSaving() => + services + .AddSaveUpgraders() + .AddHostedSingletonService() + .AddHostedSingletonService(); + + public IServiceCollection AddAppEvents() => + services + .AddEvents() + .AddEventTriggers(); + + private IServiceCollection TryAddSingletonLazyArrayProvider() + { + services.TryAddSingleton>(provider => () => provider.GetRequiredService>().ToArray()); + return services; + } + + private IServiceCollection AddEvent() => + services + .TryAddSingletonLazyArrayProvider>() + .AddSingleton(provider => (IEvent)provider.GetRequiredService()); + } } diff --git a/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs b/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs new file mode 100644 index 0000000000..122518366c --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs @@ -0,0 +1,5 @@ +namespace Nitrox.Server.Subnautica.Models.Administration.Core; + +internal interface IAdminFeature; + +internal interface IAdminFeature : IAdminFeature where T : IAdminFeature; diff --git a/Nitrox.Server.Subnautica/Models/Administration/IKickPlayer.cs b/Nitrox.Server.Subnautica/Models/Administration/IKickPlayer.cs new file mode 100644 index 0000000000..631348cf31 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Administration/IKickPlayer.cs @@ -0,0 +1,9 @@ +using Nitrox.Model.Core; +using Nitrox.Server.Subnautica.Models.Administration.Core; + +namespace Nitrox.Server.Subnautica.Models.Administration; + +internal interface IKickPlayer : IAdminFeature +{ + Task KickPlayer(SessionId sessionId, string reason = ""); +} diff --git a/Nitrox.Server.Subnautica/Models/AppEvents/ISaveState.cs b/Nitrox.Server.Subnautica/Models/AppEvents/ISaveState.cs new file mode 100644 index 0000000000..251d738feb --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/AppEvents/ISaveState.cs @@ -0,0 +1,15 @@ +using Nitrox.Server.Subnautica.Models.AppEvents.Core; +using Nitrox.Server.Subnautica.Models.AppEvents.Triggers; + +namespace Nitrox.Server.Subnautica.Models.AppEvents; + +/// +/// Event to let other services save their state to the same directory as the server save. +/// +internal interface ISaveState : IEvent +{ + /// Path to the save directory of the current game server instance. + public record Args(string SavePath); + + public class Trigger(Func[]> lazyHandlersProvider) : AsyncTrigger(lazyHandlersProvider); +} diff --git a/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs b/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs new file mode 100644 index 0000000000..84a035417e --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs @@ -0,0 +1,12 @@ +using Nitrox.Server.Subnautica.Models.AppEvents.Core; +using Nitrox.Server.Subnautica.Models.AppEvents.Triggers; +using Nitrox.Server.Subnautica.Models.Communication; + +namespace Nitrox.Server.Subnautica.Models.AppEvents; + +internal interface ISessionCleaner : IEvent +{ + public record Args(SessionManager.Session Session, int NewPlayerTotal); + + public class Trigger(Func[]> lazyHandlersProvider) : AsyncTrigger(lazyHandlersProvider); +} diff --git a/Nitrox.Server.Subnautica/Models/AppEvents/Triggers/AsyncTrigger.cs b/Nitrox.Server.Subnautica/Models/AppEvents/Triggers/AsyncTrigger.cs new file mode 100644 index 0000000000..45741017ef --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/AppEvents/Triggers/AsyncTrigger.cs @@ -0,0 +1,27 @@ +using System.Buffers; +using Nitrox.Server.Subnautica.Models.AppEvents.Core; + +namespace Nitrox.Server.Subnautica.Models.AppEvents.Triggers; + +internal abstract class AsyncTrigger(Func[]> handlers) : EventTrigger(handlers) +{ + private static readonly ArrayPool pool = ArrayPool.Create(); + + public async Task InvokeAsync(TEventArgs args) + { + IEvent[] handlers = Handlers.Value; + Task[] tasks = pool.Rent(handlers.Length); + try + { + for (int i = 0; i < handlers.Length; i++) + { + tasks[i] = handlers[i].OnEventAsync(args); + } + await Task.WhenAll(tasks.AsSpan(0, handlers.Length)); + } + finally + { + pool.Return(tasks); + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/CallArgs.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/CallArgs.cs deleted file mode 100644 index 3a43eed7fd..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/CallArgs.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Nitrox.Model.DataStructures; - -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract -{ - public abstract partial class Command - { - public ref struct CallArgs - { - public Command Command { get; } - public Optional Sender { get; } - public Span Args { get; } - - public bool IsConsole => !Sender.HasValue; - public string SenderName => Sender.HasValue ? Sender.Value.Name : "SERVER"; - - public CallArgs(Command command, Optional sender, Span args) - { - Command = command; - Sender = sender; - Args = args; - } - - public bool IsValid(int index) - { - return index < Args.Length && index >= 0 && Args.Length != 0; - } - - public string GetTillEnd(int startIndex = 0) - { - // TODO: Proper argument capture/parse instead of this argument join hack - if (Args.Length > 0) - { - return string.Join(" ", Args.Slice(startIndex).ToArray()); - } - - return string.Empty; - } - - public string Get(int index) - { - return Get(index); - } - - public T Get(int index) - { - IParameter param = Command.Parameters[index]; - string arg = IsValid(index) ? Args[index] : null; - - if (arg == null) - { - return default(T); - } - - if (typeof(T) == typeof(string)) - { - return (T)(object)arg; - } - - return (T)param.Read(arg); - } - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Command.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Command.cs deleted file mode 100644 index a91d2c5cc3..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Command.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Nitrox.Model.Core; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic; - -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract -{ - // TODO: Refactor to a new command system that doesn't use base class (only interfaces and attributes). - public abstract partial class Command - { - private static ILogger? logger; - private int optional, required; - - public virtual IEnumerable Aliases { get; } - - public string Name { get; } - public string Description { get; } - public Perms RequiredPermLevel { get; } - public PermsFlag Flags { get; } - public bool AllowedArgOverflow { get; set; } - public List> Parameters { get; } - - private static ILogger Logger => logger ??= NitroxServiceLocator.LocateService().CreateLogger(nameof(Command)); - - protected Command(string name, Perms perms, PermsFlag flag, string description) : this(name, perms, description) - { - Flags = flag; - } - - protected Command(string name, Perms perms, string description) - { - Validate.NotNull(name); - - Name = name; - Flags = PermsFlag.NONE; - RequiredPermLevel = perms; - AllowedArgOverflow = false; - Aliases = []; - Parameters = new List>(); - Description = string.IsNullOrEmpty(description) ? "No description provided" : description; - } - - protected abstract void Execute(CallArgs args); - - /// - /// Send a message to an existing player - /// - public static void SendMessageToPlayer(Optional player, string message) - { - if (player.HasValue) - { - player.Value.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, message)); - } - } - - /// - /// Send a message to an existing player and logs it in the console - /// - public static void SendMessage(Optional player, string message) - { - SendMessageToPlayer(player, message); - if (!player.HasValue) - { - Logger.ZLogInformation($"{message}"); - } - } - - /// - /// Send a message to all connected players - /// - public static void SendMessageToAllPlayers(string message) - { - PlayerManager playerManager = NitroxServiceLocator.LocateService(); - playerManager.SendPacketToAllPlayers(new ChatMessage(ChatMessage.SERVER_ID, message)); - Logger.ZLogInformation($"[BROADCAST] {message}"); - } - - public void TryExecute(Optional sender, Span args) - { - if (args.Length < required) - { - SendMessage(sender, $"Error: Invalid Parameters\nUsage: {ToHelpText(false, true)}"); - return; - } - - if (!AllowedArgOverflow && args.Length > optional + required) - { - SendMessage(sender, $"Error: Too many Parameters\nUsage: {ToHelpText(false, true)}"); - return; - } - - Execute(new CallArgs(this, sender, args)); - } - - public bool CanExecute(Perms treshold) - { - return RequiredPermLevel <= treshold; - } - - public string ToHelpText(bool singleCommand, bool cropText = false) - { - StringBuilder cmd = new(Name); - - if (Aliases.Any()) - { - cmd.AppendFormat("/{0}", string.Join("/", Aliases)); - } - - cmd.AppendFormat(" {0}", string.Join(" ", Parameters)); - - if (singleCommand) - { - string parameterPreText = Parameters.Count == 0 ? "" : Environment.NewLine; - string parameterText = $"{parameterPreText}{string.Join("\n", Parameters.Select(p => $"{p,-47} - {p.GetDescription()}"))}"; - - return cropText ? $"{cmd}" : $"{cmd,-32} - {Description} {parameterText}"; - } - return cropText ? $"{cmd}" : $"{cmd,-32} - {Description}"; - } - - protected void AddParameter(T param) where T : IParameter - { - Validate.NotNull(param as object); - Parameters.Add(param); - - if (param.IsRequired) - { - required++; - } - else - { - optional++; - } - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Parameter.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Parameter.cs deleted file mode 100644 index eb2e6b6dbf..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Parameter.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract -{ - public abstract class Parameter : IParameter - { - public bool IsRequired { get; } - public string Name { get; } - private string Description { get; } - - protected Parameter(string name, bool isRequired, string description) - { - Validate.IsFalse(string.IsNullOrEmpty(name)); - - Name = name; - IsRequired = isRequired; - Description = description; - } - - public abstract bool IsValid(string arg); - public abstract T Read(string arg); - - public virtual string GetDescription() - { - return Description; - } - - public override string ToString() - { - return $"{(IsRequired ? '{' : '[')}{Name}{(IsRequired ? '}' : ']')}"; - } - } - - public interface IParameter - { - bool IsRequired { get; } - string Name { get; } - - bool IsValid(string arg); - T Read(string arg); - string GetDescription(); - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeBoolean.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeBoolean.cs deleted file mode 100644 index 597f0d46df..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeBoolean.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; - -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypeBoolean : Parameter, IParameter - { - private static readonly string[] noValues = new string[] - { - bool.FalseString, - "no", - "off" - }; - - private static readonly string[] yesValues = new string[] - { - bool.TrueString, - "yes", - "on" - }; - - public TypeBoolean(string name, bool isRequired, string description) : base(name, isRequired, description) { } - - public override bool IsValid(string arg) - { - return yesValues.Contains(arg, StringComparer.OrdinalIgnoreCase) || noValues.Contains(arg, StringComparer.OrdinalIgnoreCase); - } - - public override bool Read(string arg) - { - Validate.IsTrue(IsValid(arg), "Invalid boolean value received"); - return yesValues.Contains(arg, StringComparer.OrdinalIgnoreCase); - } - - object IParameter.Read(string arg) - { - return Read(arg); - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeEnum.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeEnum.cs deleted file mode 100644 index 7be55312b9..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeEnum.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypeEnum : Parameter where T : struct, Enum - { - public TypeEnum(string name, bool required, string description) : base(name, required, description) - { - Validate.IsTrue(typeof(T).IsEnum, $"Type {typeof(T).FullName} isn't an enum"); - } - - public override bool IsValid(string arg) - { - return Enum.TryParse(arg, true, out _); - } - - public override object Read(string arg) - { - Validate.IsTrue(Enum.TryParse(arg, true, out T value), $"Unknown value received (pick from: {string.Join(", ", Enum.GetNames(typeof(T)))})"); - return value; - } - - public override string GetDescription() - { - return $"{base.GetDescription()} (values: {string.Join(", ", Enum.GetNames(typeof(T)))})"; - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeFloat.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeFloat.cs deleted file mode 100644 index 7d0195efb7..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeFloat.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypeFloat : Parameter, IParameter - { - public TypeFloat(string name, bool isRequired, string description) : base(name, isRequired, description) { } - - public override bool IsValid(string arg) - { - return float.TryParse(arg, out _); - } - - public override float Read(string arg) - { - Validate.IsTrue(float.TryParse(arg, out float value), "Invalid decimal number received"); - return value; - } - - object IParameter.Read(string arg) - { - return Read(arg); - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeInt.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeInt.cs deleted file mode 100644 index 56255ddcdd..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeInt.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypeInt : Parameter, IParameter - { - public TypeInt(string name, bool isRequired, string description) : base(name, isRequired, description) { } - - public override bool IsValid(string arg) - { - return int.TryParse(arg, out _); - } - - public override int Read(string arg) - { - Validate.IsTrue(int.TryParse(arg, out int value), "Invalid integer received"); - return value; - } - - object IParameter.Read(string arg) - { - return Read(arg); - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeNitroxId.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeNitroxId.cs deleted file mode 100644 index 6263dafe84..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeNitroxId.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Nitrox.Model.DataStructures; - -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; - -public class TypeNitroxId(string name, bool isRequired, string description) : Parameter(name, isRequired, description) -{ - public override bool IsValid(string arg) - { - return IsValid(arg, out _); - } - - private static bool IsValid(string arg, out Guid result) - { - return Guid.TryParse(arg, out result); - } - - public override NitroxId Read(string arg) - { - Validate.IsTrue(IsValid(arg, out Guid result), "Received an invalid NitroxId"); - return new NitroxId(result); - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypePlayer.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypePlayer.cs deleted file mode 100644 index 1e4354b896..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypePlayer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Nitrox.Model.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; - -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypePlayer : Parameter - { - private static readonly PlayerManager playerManager = NitroxServiceLocator.LocateService(); - - public TypePlayer(string name, bool required, string description) : base(name, required, description) - { - Validate.NotNull(playerManager, "PlayerManager can't be null to resolve the command"); - } - - public override bool IsValid(string arg) - { - return playerManager.TryGetPlayerByName(arg, out _); - } - - public override Player Read(string arg) - { - Validate.IsTrue(playerManager.TryGetPlayerByName(arg, out Player player), "Player not found"); - return player; - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeString.cs b/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeString.cs deleted file mode 100644 index bd7f1d19e3..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Abstract/Type/TypeString.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Nitrox.Server.Subnautica.Models.Commands.Abstract.Type -{ - public class TypeString : Parameter - { - public TypeString(string name, bool isRequired, string description) : base(name, isRequired, description) { } - - public override bool IsValid(string arg) - { - return !string.IsNullOrEmpty(arg); - } - - public override string Read(string arg) - { - Validate.IsTrue(IsValid(arg), "Received null/empty instead of a valid string"); - return arg; - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/ConvertResult.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/ConvertResult.cs new file mode 100644 index 0000000000..192e735174 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/ConvertResult.cs @@ -0,0 +1,17 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; + +internal readonly struct ConvertResult +{ + public bool Success { get; init; } + public object Value { get; init; } + + public static ConvertResult Ok(T value) => + new() + { + Success = true, + Value = value + }; + + public static ConvertResult Fail(string message = null) => + new() { Success = false, Value = message }; +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/IArgConverter.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/IArgConverter.cs new file mode 100644 index 0000000000..a94ceb4f7e --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/Core/IArgConverter.cs @@ -0,0 +1,16 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; + +internal interface IArgConverter +{ + Task ConvertAsync(object from); +} + +/// +/// Converts an object of type to . +/// +internal interface IArgConverter : IArgConverter +{ + Task ConvertAsync(TFrom from); + + Task IArgConverter.ConvertAsync(object from) => ConvertAsync(from is TFrom tFrom ? tFrom : default); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/PlayerNameToPlayerArgConverter.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/PlayerNameToPlayerArgConverter.cs new file mode 100644 index 0000000000..ad8be337e1 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/PlayerNameToPlayerArgConverter.cs @@ -0,0 +1,21 @@ +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; + +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters; + +/// +/// Converts a player name to a player object, if known. +/// +internal sealed class PlayerNameToPlayerArgConverter(PlayerManager playerManager) : IArgConverter +{ + private readonly PlayerManager playerManager = playerManager; + + public Task ConvertAsync(string playerName) + { + if (!playerManager.TryGetPlayerByName(playerName, out Player? player)) + { + return Task.FromResult(ConvertResult.Fail($"No player found by name '{playerName}'")); + } + return Task.FromResult(ConvertResult.Ok(player)); + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/SessionIdToPlayerArgConverter.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/SessionIdToPlayerArgConverter.cs new file mode 100644 index 0000000000..b4585e8b13 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/SessionIdToPlayerArgConverter.cs @@ -0,0 +1,26 @@ +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; + +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters; + +/// +/// Converts a player ID to a player object, if known. +/// +internal sealed class SessionIdToPlayerArgConverter(PlayerManager playerManager) : IArgConverter +{ + private readonly PlayerManager playerManager = playerManager; + + public Task ConvertAsync(ushort sessionId) + { + if (sessionId < 1) + { + return Task.FromResult(ConvertResult.Fail("Session id must start with 1")); + } + if (!playerManager.TryGetPlayerBySessionId(sessionId, out Player player)) + { + return Task.FromResult(ConvertResult.Fail($"No player found by session #{sessionId}")); + } + + return Task.FromResult(ConvertResult.Ok(player)); + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/WordToBoolArgConverter.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/WordToBoolArgConverter.cs new file mode 100644 index 0000000000..46d90b45ba --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/WordToBoolArgConverter.cs @@ -0,0 +1,17 @@ +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; + +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters; + +/// +/// Converts a word to a bool value. +/// +internal sealed class WordToBoolArgConverter : IArgConverter +{ + public Task ConvertAsync(string value) => + Task.FromResult(value switch + { + "on" or "enable" or "1" => ConvertResult.Ok(true), + "off" or "disable" or "0" => ConvertResult.Ok(false), + _ => ConvertResult.Fail() + }); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/AuroraCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/AuroraCommand.cs index cba3e9d725..097f50be88 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/AuroraCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/AuroraCommand.cs @@ -1,42 +1,41 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; namespace Nitrox.Server.Subnautica.Models.Commands; -// TODO: When we make the new command system, move this stuff to it -internal sealed class AuroraCommand : Command +// We shouldn't let the server-side use this command because it needs some stuff to happen client-side (e.g. story goals) +[RequiresPermission(Perms.ADMIN)] +[RequiresOrigin(CommandOrigin.PLAYER)] +internal sealed class AuroraCommand(StoryManager storyManager) : ICommandHandler { - private readonly StoryManager storyManager; + private readonly StoryManager storyManager = storyManager; - // We shouldn't let the server use this command because it needs some stuff to happen client-side like goals - public AuroraCommand(StoryManager storyManager) : base("aurora", Perms.ADMIN, PermsFlag.NO_CONSOLE, "Manage Aurora's state") + [Description("Which action to apply to Aurora")] + public Task Execute(ICommandContext context, AuroraAction action) { - AddParameter(new TypeString("countdown/restore/explode", true, "Which action to apply to Aurora")); - - this.storyManager = storyManager; - } - - protected override void Execute(CallArgs args) - { - string action = args.Get(0); - - switch (action.ToLower()) + switch (action) { - case "countdown": - storyManager.BroadcastExplodeAurora(true); + case AuroraAction.COUNTDOWN: + storyManager.BroadcastExplodeAurora(false); break; - case "restore": + case AuroraAction.RESTORE: storyManager.BroadcastRestoreAurora(); break; - case "explode": - storyManager.BroadcastExplodeAurora(false); + case AuroraAction.EXPLODE: + storyManager.BroadcastExplodeAurora(true); break; default: - // Same message as in the abstract class, in method TryExecute - SendMessage(args.Sender, $"Error: Invalid Parameters\nUsage: {ToHelpText(false, true)}"); - break; + throw new ArgumentOutOfRangeException(nameof(action), action, null); } + return Task.CompletedTask; + } + + public enum AuroraAction + { + COUNTDOWN, + RESTORE, + EXPLODE } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/AutosaveCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/AutosaveCommand.cs index e438c097dc..2dc3e61649 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/AutosaveCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/AutosaveCommand.cs @@ -1,23 +1,32 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class AutoSaveCommand(IOptions serverOptionsProvider) : ICommandHandler { - internal class AutoSaveCommand : Command - { - private readonly IOptions options; + private readonly IOptions serverOptionsProvider = serverOptionsProvider; - public AutoSaveCommand(IOptions options) : base("autosave", Perms.ADMIN, "Toggles the map autosave") + [Description("Whether autosave should be on or off")] + public async Task Execute(ICommandContext context, bool toggle) + { + SubnauticaServerOptions options = serverOptionsProvider.Value; + if (toggle) { - AddParameter(new TypeBoolean("on/off", true, "Whether autosave should be on or off")); - - this.options = options; + // Ensure save interval is a sensible value before turning on auto saving. + if (options.SaveInterval <= 1000) + { + options.SaveInterval = new SubnauticaServerOptions().SaveInterval; + } + options.AutoSave = true; + await context.ReplyAsync("Enabled periodical saving"); } - - protected override void Execute(CallArgs args) + else { - options.Value.AutoSave = args.Get(0); + options.AutoSave = false; + await context.ReplyAsync("Disabled periodical saving"); } } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/BackCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/BackCommand.cs index d8f0b23dea..bb3b1a434b 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/BackCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/BackCommand.cs @@ -1,26 +1,36 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.MODERATOR)] +[RequiresOrigin(CommandOrigin.PLAYER)] +internal sealed class BackCommand(IPacketSender packetSender, PlayerManager playerManager, ILogger logger) : ICommandHandler { - internal class BackCommand : Command + private readonly ILogger logger = logger; + private readonly PlayerManager playerManager = playerManager; + private readonly IPacketSender packetSender = packetSender; + + [Description("Teleports you back on your last location")] + public Task Execute(ICommandContext context) { - public BackCommand() : base("back", Perms.MODERATOR, PermsFlag.NO_CONSOLE, "Teleports you back on your last location") + if (!playerManager.TryGetPlayerBySessionId(context.OriginId, out Player player)) { + logger.ZLogError($"Failed to get player instance from session #{context.OriginId}"); + return Task.CompletedTask; } - protected override void Execute(CallArgs args) + if (player.LastStoredPosition == null) { - Player player = args.Sender.Value; - - if (player.LastStoredPosition == null) - { - SendMessage(args.Sender, "No previous location..."); - return; - } - - player.Teleport(player.LastStoredPosition.Value, player.LastStoredSubRootID); - SendMessage(args.Sender, $"Teleported back to {player.LastStoredPosition.Value}"); + context.ReplyAsync("No previous location..."); + return Task.CompletedTask; } + + player.Teleport(player.LastStoredPosition.Value, player.LastStoredSubRootID, packetSender); + context.ReplyAsync($"Teleported back to {player.LastStoredPosition.Value}"); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/BroadcastCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/BroadcastCommand.cs index 362415a71c..63c25962c3 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/BroadcastCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/BroadcastCommand.cs @@ -1,24 +1,19 @@ -using System.Collections.Generic; +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class BroadcastCommand : Command - { - public override IEnumerable Aliases { get; } = new[] { "say" }; - - public BroadcastCommand() : base("broadcast", Perms.MODERATOR, "Broadcasts a message on the server") - { - AddParameter(new TypeString("message", true, "The message to be broadcast")); +namespace Nitrox.Server.Subnautica.Models.Commands; - AllowedArgOverflow = true; - } +[Alias("say")] +[RequiresPermission(Perms.MODERATOR)] +internal sealed class BroadcastCommand(ILogger logger) : ICommandHandler +{ + private readonly ILogger logger = logger; - protected override void Execute(CallArgs args) - { - SendMessageToAllPlayers(args.GetTillEnd()); - } + [Description("Broadcasts a message on the server")] + public async Task Execute(ICommandContext context, string messageToBroadcast) + { + await context.SendToAllAsync(messageToBroadcast); + logger.ZLogInformation($"{context.OriginName} #{context.OriginId:@SessionId} sent a message to everyone: '{messageToBroadcast:@ChatMessage}'"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ChangeAdminPasswordCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/ChangeAdminPasswordCommand.cs index b2c0a4ac46..ebe9aab385 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/ChangeAdminPasswordCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/ChangeAdminPasswordCommand.cs @@ -1,29 +1,20 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class ChangeAdminPasswordCommand : Command - { - private readonly IOptions options; - private readonly ILogger logger; - - public ChangeAdminPasswordCommand(IOptions options, ILogger logger) : base("changeadminpassword", Perms.ADMIN, "Changes admin password") - { - AddParameter(new TypeString("password", true, "The new admin password")); +namespace Nitrox.Server.Subnautica.Models.Commands; - this.options = options; - this.logger = logger; - } - - protected override void Execute(CallArgs args) - { - string newPassword = args.Get(0); - options.Value.AdminPassword = newPassword; - logger.ZLogInformation($"Admin password changed to {newPassword:@password} by {args.SenderName:@playername}"); +[RequiresPermission(Perms.HOST)] +internal sealed class ChangeAdminPasswordCommand(IOptions options, ILogger logger) : ICommandHandler +{ + private readonly IOptions options = options; + private readonly ILogger logger = logger; - SendMessageToPlayer(args.Sender, "Admin password has been updated"); - } + [Description("Changes admin password")] + public Task Execute(ICommandContext context, string newPassword) + { + options.Value.AdminPassword = newPassword; + logger.ZLogInformation($"Admin password changed to '{newPassword:@Password}'"); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ChangeServerGamemodeCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/ChangeServerGamemodeCommand.cs index 92c1370a74..13569ee090 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/ChangeServerGamemodeCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/ChangeServerGamemodeCommand.cs @@ -1,41 +1,32 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal class ChangeServerGamemodeCommand : Command +[RequiresPermission(Perms.ADMIN)] +internal sealed class ChangeServerGamemodeCommand(PlayerManager playerManager, IOptions serverConfig) : ICommandHandler { - private readonly PlayerManager playerManager; - private readonly IOptions serverConfig; + private readonly IOptions serverConfig = serverConfig; + private readonly PlayerManager playerManager = playerManager; - public ChangeServerGamemodeCommand(PlayerManager playerManager, IOptions serverConfig) : base("changeservergamemode", Perms.ADMIN, "Changes server gamemode") + [Description("Changes server gamemode")] + public async Task Execute(ICommandContext context, [Description("Gamemode to change to")] SubnauticaGameMode newGameMode) { - AddParameter(new TypeEnum("gamemode", true, "Gamemode to change to")); - - this.playerManager = playerManager; - this.serverConfig = serverConfig; - } - - protected override void Execute(CallArgs args) - { - SubnauticaGameMode sgm = args.Get(0); - - if (serverConfig.Value.GameMode != sgm) + if (serverConfig.Value.GameMode == newGameMode) { - serverConfig.Value.GameMode = sgm; - - foreach (Player player in playerManager.GetAllPlayers()) - { - player.GameMode = sgm; - } - playerManager.SendPacketToAllPlayers(GameModeChanged.ForAllPlayers(sgm)); - SendMessageToAllPlayers($"Server gamemode changed to \"{sgm}\" by {args.SenderName}"); + await context.ReplyAsync("Server is already using this gamemode"); + return; } - else + + serverConfig.Value.GameMode = newGameMode; + foreach (Player player in playerManager.GetAllPlayers()) { - SendMessage(args.Sender, "Server is already using this gamemode"); + player.GameMode = newGameMode; } + await context.SendToAllAsync(GameModeChanged.ForAllPlayers(newGameMode)); + await context.SendToAllAsync($"Server gamemode changed to \"{newGameMode}\" by {context.OriginName}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ChangeServerPasswordCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/ChangeServerPasswordCommand.cs index e6cfd6bf77..cb5ee957b6 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/ChangeServerPasswordCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/ChangeServerPasswordCommand.cs @@ -1,30 +1,25 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class ChangeServerPasswordCommand : Command - { - private readonly IOptions serverConfig; - private readonly ILogger logger; - - public ChangeServerPasswordCommand(IOptions serverConfig, ILogger logger) : base("changeserverpassword", Perms.ADMIN, "Changes server password. Clear it without argument") - { - AddParameter(new TypeString("password", false, "The new server password")); +namespace Nitrox.Server.Subnautica.Models.Commands; - this.serverConfig = serverConfig; - this.logger = logger; - } +[RequiresPermission(Perms.ADMIN)] +internal sealed class ChangeServerPasswordCommand(ILogger logger, IOptions serverConfig) : ICommandHandler, ICommandHandler +{ + private readonly IOptions serverConfig = serverConfig; + private readonly ILogger logger = logger; - protected override void Execute(CallArgs args) - { - string password = args.Get(0) ?? string.Empty; + [Description("Changes server password. Clear it without argument")] + public async Task Execute(ICommandContext context, [Description("The new server password")] string newPassword) => await SetPasswordAsync(context, newPassword); - serverConfig.Value.ServerPassword = password; + [Description("Clears server password")] + public async Task Execute(ICommandContext context) => await SetPasswordAsync(context, ""); - logger.LogServerPasswordChanged(password, args.SenderName); - SendMessageToPlayer(args.Sender, "Server password has been updated"); - } + private async Task SetPasswordAsync(ICommandContext context, string password) + { + serverConfig.Value.ServerPassword = password; + logger.LogServerPasswordChanged(password, context.OriginName, context.OriginId); + await context.ReplyAsync("Server password has been updated"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ConfigCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/ConfigCommand.cs index 287372661b..4fd9b2928b 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/ConfigCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/ConfigCommand.cs @@ -1,68 +1,25 @@ -using System.Diagnostics; -using Nitrox.Model.DataStructures.GameLogic; +using System.ComponentModel; +using System.IO; using Nitrox.Model.Platforms.OS.Shared; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class ConfigCommand : Command - { - private readonly SemaphoreSlim configOpenLock = new(1); - private readonly IOptions options; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Commands; - public ConfigCommand(IOptions options, ILogger logger) : base("config", Perms.HOST, "Opens the server configuration file") - { - this.options = options; - this.logger = logger; - } +[RequiresOrigin(CommandOrigin.SERVER)] +internal sealed class ConfigCommand(IOptions optionsProvider) : ICommandHandler +{ + private readonly IOptions optionsProvider = optionsProvider; - protected override void Execute(CallArgs args) + [Description("Opens the server configuration file")] + public async Task Execute(ICommandContext context) + { + string filePath = optionsProvider.Value.GetServerConfigFilePath(); + if (!File.Exists(filePath)) { - if (!configOpenLock.Wait(0)) - { - logger.ZLogWarning($"Waiting on previous config command to close the configuration file."); - return; - } - - Task.Run(async () => - { - try - { - await StartWithDefaultProgramAsync(options.Value.GetServerConfigFilePath()); - } - finally - { - configOpenLock.Release(); - } - logger.ZLogInformation($"If you made changes, restart the server for them to take effect."); - }) - .ContinueWith(t => - { -#if DEBUG - if (t.Exception != null) - { - throw t.Exception; - } -#endif - }); + // TODO: Handle this case to generate config? + await context.ReplyAsync("No configuration file exists"); } - private async Task StartWithDefaultProgramAsync(string fileToOpen) - { - using Process process = FileSystem.Instance.OpenOrExecuteFile(fileToOpen); - await process.WaitForExitAsync(); - try - { - while (!process.HasExited) - { - await Task.Delay(100); - } - } - catch (Exception) - { - // ignored - } - } + FileSystem.Instance.OpenOrExecuteFile(filePath); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/AliasAttribute.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/AliasAttribute.cs new file mode 100644 index 0000000000..280d0bb1c9 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/AliasAttribute.cs @@ -0,0 +1,7 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +[AttributeUsage(AttributeTargets.Class)] +internal sealed class AliasAttribute(params string[] aliases) : Attribute +{ + public string[] Aliases { get; } = aliases; +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/CommandHandlerEntry.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandHandlerEntry.cs new file mode 100644 index 0000000000..dfac4225d9 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandHandlerEntry.cs @@ -0,0 +1,166 @@ +using System.Collections; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Nitrox.Model.DataStructures.GameLogic; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +internal sealed record CommandHandlerEntry +{ + private readonly object[] defaultValues = []; + private readonly MethodInvoker execute; + public object Owner { get; } + + [field: AllowNull] + [field: MaybeNull] + private Type OwnerType => field ??= Owner.GetType(); + + public string Name { get; } = ""; + private MethodInfo ExecuteMethod { get; } + public ParameterInfo[] Parameters { get; } = []; + public Type[] ParameterTypes { get; } = []; + public string[] Aliases { get; init; } = []; + public string Description { get; init; } = ""; + + /// + public CommandOrigin AcceptedOrigin { get; init; } + + /// + public Perms MinimumPermissions { get; init; } + + public CommandHandlerEntry(ICommandHandlerBase owner, Type? handlerType) + { + Owner = owner; + + MethodInfo[] methods = handlerType == null ? OwnerType.GetMethods() : OwnerType.GetInterfaceMap(handlerType).TargetMethods; + MethodInfo executeMethod = methods.First(m => m.Name == nameof(ICommandHandler.Execute)); + + Name = GetName(owner); + ExecuteMethod = executeMethod; + Description = GetCommandAttribute()?.Description ?? ""; + Parameters = executeMethod.GetParameters().Skip(1).ToArray(); + ParameterTypes = Parameters.Select(p => p.ParameterType).ToArray(); + Type unsupportedType = ParameterTypes.FirstOrDefault(t => t.IsArray || typeof(IList).IsAssignableFrom(t)); + if (unsupportedType != null) + { + throw new NotSupportedException($"Arrays like {unsupportedType} are unsupported"); + } + Aliases = OwnerType.GetCustomAttribute()?.Aliases ?? []; + AcceptedOrigin = GetCommandAttribute()?.AcceptedOrigin ?? CommandOrigin.DEFAULT; + MinimumPermissions = GetCommandAttribute()?.MinimumPermission ?? Perms.DEFAULT; + + execute = MethodInvoker.Create(executeMethod); + + // This asks the JIT to compile this method which reduces initial command execution time. + RuntimeHelpers.PrepareMethod(executeMethod.MethodHandle); + } + + public CommandHandlerEntry(CommandHandlerEntry derivedHandler, ParameterInfo[] parameters, ReadOnlySpan defaultValues) + { + Owner = derivedHandler.Owner; + Name = derivedHandler.Name; + Description = derivedHandler.Description; + ExecuteMethod = derivedHandler.ExecuteMethod; + Parameters = parameters; + ParameterTypes = Parameters.Select(p => p.ParameterType).ToArray(); + Aliases = derivedHandler.Aliases; + AcceptedOrigin = derivedHandler.AcceptedOrigin; + MinimumPermissions = derivedHandler.MinimumPermissions; + this.defaultValues = [..defaultValues]; + + execute = derivedHandler.execute; + } + + public Task InvokeAsync(params Span args) + { + if (defaultValues.Length > 0) + { + return (Task)execute.Invoke(Owner, new Span([..args, ..defaultValues]))!; + } + return (Task)execute.Invoke(Owner, args)!; + } + + public override string ToString() => ToDisplayString(true); + + public string ToDisplayString(bool includeNames) + { + string nameText = ""; + if (includeNames) + { + nameText = string.Join('|', Aliases.OrderBy(n => n.Length).ThenBy(n => n)); + if (nameText != "") + { + nameText = $"|{nameText}"; + } + nameText = $"{Name}{nameText} "; + } + + return $"{nameText}{GetParametersInfo(Parameters)}{(Description != "" ? $"- {Description}" : "")}"; + + static string GetParametersInfo(params ParameterInfo[] parms) + { + if (parms.Length < 1) + { + return ""; + } + + return $"{string.Join(" ", parms.Select(GetParameterInfo))} "; + + static string GetParameterInfo(ParameterInfo parameter) + { + string surrounding = parameter.IsOptional ? "[]" : "<>"; + string description = parameter.GetCustomAttribute()?.Description ?? parameter.Name; + return $"{surrounding[0]}{GetNiceTypeName(parameter.ParameterType)}:{description}{surrounding[1]}"; + } + + static string GetNiceTypeName(Type type) + { + if (type.IsEnum) + { + return $"'{string.Join("'|'", Enum.GetNames(type))}'"; + } + if (type == typeof(int)) + { + return "int"; + } + if (type == typeof(string)) + { + return "string"; + } + if (type == typeof(bool)) + { + return "bool"; + } + if (type == typeof(float)) + { + return "float"; + } + if (type == typeof(object)) + { + return "object"; + } + if (type == typeof(Player)) + { + return "sessionIdOrName"; + } + return type.Name; + } + } + } + + private static string GetName(object owner) + { + string name = owner.GetType().Name; + int lastIndexOf = name.LastIndexOf("Command", StringComparison.Ordinal); + if (lastIndexOf == -1) + { + throw new ArgumentOutOfRangeException(nameof(owner), @"Expected command type name to end with ""Command"""); + } + return name[.. lastIndexOf].ToLowerInvariant(); + } + + private TAttr? GetCommandAttribute() where TAttr : Attribute => ExecuteMethod.GetCustomAttribute() ?? OwnerType.GetCustomAttribute(); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/CommandOrigin.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandOrigin.cs new file mode 100644 index 0000000000..d298f89c64 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandOrigin.cs @@ -0,0 +1,10 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +[Flags] +internal enum CommandOrigin +{ + SERVER = 1 << 0, + PLAYER = 1 << 1, + ANY = SERVER | PLAYER, + DEFAULT = ANY +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs new file mode 100644 index 0000000000..da23fca2a5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs @@ -0,0 +1,261 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +/// +/// Aggregates known commands into a read-optimized and sorted lookup. +/// +internal sealed class CommandRegistry +{ + private static readonly Type[] specificToGeneralizingTypeOrder = + [typeof(bool), typeof(char), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(object), typeof(string)]; + + private readonly ILogger logger; + + /// + /// Lookup for command name -> list of known handlers. Each with different arg types or count. + /// + private Dictionary> HandlerLookup { get; } = new(StringComparer.OrdinalIgnoreCase); + + private Dictionary>.AlternateLookup> SpanHandlerLookup { get; } + + /// + /// Lookup of converters that can convert to a type. + /// + private Dictionary> ArgConverterLookup { get; } = []; + + public IEnumerable Handlers => HandlerLookup + .DistinctBy(p => p.Value) + .SelectMany(p => p.Value); + + public CommandRegistry(IEnumerable handlers, IEnumerable argConverters, ILogger logger) + { + this.logger = logger; + CommandHandlerEntry[] commandHandlers = [.. handlers]; + foreach (CommandHandlerEntry handler in commandHandlers + .SelectMany(h => + { + // Split handlers with optional parameters into separate handlers. + List result = [h]; + List defaultValues = []; + for (int i = h.Parameters.Length - 1; i >= 0; i--) + { + ParameterInfo current = h.Parameters[i]; + if (current.IsOptional) + { + defaultValues.Insert(0, current.DefaultValue); + result.Add(new CommandHandlerEntry(h, h.Parameters.Take(i).ToArray(), [..defaultValues])); + } + } + return result; + }) + .OrderByDescending(h => h.Parameters.Length) + .ThenBy(h => h.ParameterTypes.Length == 0 + ? 0 + : h.ParameterTypes.Max(t => + { + // More specific handler parameters should be prioritized over generalizing handlers (i.e. try handlers in this order: bool -> int -> float -> Player -> object -> string). + int index = specificToGeneralizingTypeOrder.GetIndex(t); + // String is a catch-all type and comes last. This is because command input is string-based. Anything assignable to object but not string comes before string. + return index == -1 ? specificToGeneralizingTypeOrder.GetIndex(typeof(object)) : index; + })) + .ThenBy(h => h.ParameterTypes.Any(o => o == typeof(object)) ? 1 : 0)) + { + RegisterHandler(handler); + } + SpanHandlerLookup = HandlerLookup.GetAlternateLookup>(); + logger.ZLogDebug($"{commandHandlers.Length:@CommandCount} commands found and registered"); + + IArgConverter[] converters = [.. argConverters]; + foreach (IArgConverter converter in converters) + { + Type[] converterInterfaces = converter.GetType().GetInterfaces(); + foreach (Type converterInterface in converterInterfaces) + { + if (!converterInterface.IsAssignableTo(typeof(IArgConverter))) + { + continue; + } + Type[] genericArgsOnConverter = converterInterface.GetGenericArguments(); + if (genericArgsOnConverter is not { Length: 2 }) + { + continue; + } + Type toType = genericArgsOnConverter[1]; + if (!ArgConverterLookup.TryGetValue(toType, out List registeredConverters)) + { + registeredConverters = []; + } + registeredConverters.Add(new ArgConverterInfo(converter, genericArgsOnConverter[0], toType)); + ArgConverterLookup[toType] = registeredConverters; + } + } + } + + /// + /// Gets the command handlers if command exists. Returns false if context origin (or permissions) are not + /// compatible/allowed to execute any of the handlers. + /// + public bool TryGetHandlersByCommandName(ICommandContext context, ReadOnlySpan commandNameOrAlias, out List validHandlers) + { + validHandlers = []; + if (!SpanHandlerLookup.TryGetValue(commandNameOrAlias, out List knownHandlers)) + { + return false; + } + foreach (CommandHandlerEntry handler in knownHandlers) + { + if (!IsValidHandlerForContext(handler, context)) + { + continue; + } + validHandlers.Add(handler); + } + return validHandlers.Count > 0; + } + + public bool IsValidHandlerForContext(CommandHandlerEntry handler, ICommandContext context) => handler.AcceptedOrigin.HasFlag(context.Origin) && handler.MinimumPermissions <= context.Permissions; + + public async ValueTask> TryConvertToType(string value, Type targetType) + { + List results = []; + if (ArgConverterLookup.TryGetValue(targetType, out List list)) + { + foreach (ArgConverterInfo converterInfo in list) + { + if (TryParseToType(value, converterInfo.From) is not { } obj) + { + continue; + } + ConvertResult result = await converterInfo.Converter.ConvertAsync(obj); + if (result.Success) + { + return [result]; + } + results.Add(result); + } + } + return results; + } + + public object? TryParseToType(ReadOnlySpan value, Type type) + { + try + { + switch (type) + { + case null: + throw new ArgumentNullException(nameof(type)); + case not null when type == typeof(string): + return value.ToString(); + case not null when type == typeof(bool): + return bool.Parse(value); + case not null when type == typeof(char): + return char.Parse(value.ToString()); + case not null when type == typeof(sbyte): + return sbyte.Parse(value); + case not null when type == typeof(byte): + return byte.Parse(value); + case not null when type == typeof(short): + return short.Parse(value); + case not null when type == typeof(ushort): + return ushort.Parse(value); + case not null when type == typeof(int): + return int.Parse(value); + case not null when type == typeof(uint): + return uint.Parse(value); + case not null when type == typeof(long): + return long.Parse(value); + case not null when type == typeof(ulong): + return ulong.Parse(value); + case not null when type == typeof(float): + return float.Parse(value); + case not null when type == typeof(double): + return double.Parse(value); + case { IsEnum: true }: + return Enum.Parse(type, value, true); + default: + return null; + } + } + catch + { + return null; + } + } + + public string? FindCommandName(Regex regex, Perms viewerPerms, bool ignoreAliases = false) + { + foreach ((string commandName, List? handlers) in HandlerLookup) + { + if (handlers == null) + { + continue; + } + bool canViewCommand = false; + foreach (CommandHandlerEntry h in handlers) + { + if (h.MinimumPermissions <= viewerPerms) + { + canViewCommand = true; + break; + } + } + if (!canViewCommand) + { + continue; + } + if (ignoreAliases && handlers[0].Aliases.Contains(commandName, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + if (!regex.IsMatch(commandName)) + { + continue; + } + + return commandName; + } + return null; + } + + private void RegisterHandler(CommandHandlerEntry handler) + { + logger.LogCommandHandlerAdded(handler); + + string ownerName = handler.Name; + if (!HandlerLookup.TryGetValue(ownerName, out List handlers)) + { + HandlerLookup[ownerName] = handlers = []; + foreach (string ownerAlias in handler.Aliases) + { + if (HandlerLookup.TryGetValue(ownerAlias, out List existingHandlers)) + { + if (existingHandlers.Count < 1) + { + throw new Exception($"Alias {ownerAlias} from {handler.Owner.GetType().FullName} conflicts with its own name"); + } + throw new Exception($"Alias {ownerAlias} from {handler.Owner.GetType().FullName} conflicts with {existingHandlers.First().Owner.GetType().FullName}"); + } + HandlerLookup[ownerAlias] = handlers; + } + } + + if (handlers.Count > 0) + { + CommandHandlerEntry otherHandler = handlers.First(); + if (otherHandler.Owner != handler.Owner) + { + throw new Exception($"Handler {otherHandler.Name} on type {otherHandler.Owner.GetType().FullName} conflicts with command name {handler.Owner.GetType().FullName}"); + } + } + handlers.Add(handler); + } + + private record ArgConverterInfo(IArgConverter Converter, Type From, Type To); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs new file mode 100644 index 0000000000..d0719f4d3d --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +internal sealed record HostToServerCommandContext : ICommandContext +{ + private readonly IPacketSender packetSender; + + public ILogger Logger { get; set; } = NullLogger.Instance; + public CommandOrigin Origin { get; init; } = CommandOrigin.SERVER; + public string OriginName => "SERVER"; + public SessionId OriginId { get; init; } = 0; + public Perms Permissions { get; init; } = Perms.HOST; + + public HostToServerCommandContext(IPacketSender packetSender) + { + this.packetSender = packetSender; + } + + public async Task ReplyAsync(T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketToAllAsync(packet); + break; + case string message when !string.IsNullOrWhiteSpace(message): + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + Logger.LogInformation(message); + return; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } + + /// + /// Sends packet to all connected players. + /// + /// + /// Same as because the origin is the server. + /// + public async ValueTask SendToAllAsync(T data) => await SendToOthersAsync(data); + + /// + /// Sends packet to all connected players. + /// + public async ValueTask SendToOthersAsync(T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketToOthersAsync(packet, OriginId); + break; + case string message when !string.IsNullOrWhiteSpace(message): + await packetSender.SendPacketToOthersAsync(new ChatMessage(SessionId.SERVER_ID, message), OriginId); + break; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } + + public async ValueTask SendAsync(SessionId sessionId, T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketAsync(packet, sessionId); + break; + case string message when !string.IsNullOrWhiteSpace(message): + await packetSender.SendPacketAsync(new ChatMessage(SessionId.SERVER_ID, message), sessionId); + break; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandContext.cs new file mode 100644 index 0000000000..64c9822af5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandContext.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures.GameLogic; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +/// +/// A context that can be provided to a command as it is called. +/// +internal interface ICommandContext +{ + public ILogger Logger { get; set; } + + CommandOrigin Origin { get; init; } + + public string OriginName { get; } + + SessionId OriginId { get; init; } + + /// + /// The permissions of the issuer as they were when the command was issued. + /// + Perms Permissions { get; init; } + + /// + /// Sends a message back to the command issuer. + /// + /// The data to send. + Task ReplyAsync(T data); + + ValueTask SendAsync(SessionId sessionId, T data); + ValueTask SendToAllAsync(T data); + ValueTask SendToOthersAsync(T data); + + [DoesNotReturn] + static void ThrowNotSupportedData(T data) + { + throw new NotSupportedException($"Unsupported data type {data?.GetType()} with value: {data?.ToString() ?? ""}"); + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandHandler.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandHandler.cs new file mode 100644 index 0000000000..9c9de2fb9b --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandHandler.cs @@ -0,0 +1,28 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +internal interface ICommandHandlerBase; + +internal interface ICommandHandler : ICommandHandlerBase +{ + Task Execute(ICommandContext context); +} + +internal interface ICommandHandler : ICommandHandlerBase +{ + Task Execute(ICommandContext context, TArg1 arg); +} + +internal interface ICommandHandler : ICommandHandlerBase +{ + Task Execute(ICommandContext context, TArg1 arg1, TArg2 arg2); +} + +internal interface ICommandHandler : ICommandHandlerBase +{ + Task Execute(ICommandContext context, TArg1 arg1, TArg2 arg2, TArg3 arg3); +} + +internal interface ICommandHandler : ICommandHandlerBase +{ + Task Execute(ICommandContext context, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandSubmit.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandSubmit.cs new file mode 100644 index 0000000000..bc2fd7c5d5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/ICommandSubmit.cs @@ -0,0 +1,12 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +internal interface ICommandSubmit +{ + /// + /// Tries to execute the command that matches the input. + /// + /// The text input which should be interpreted as a command. + /// The context that should be given to the command handler if found. + /// Task that is set if the command is async. + bool ExecuteCommand(ReadOnlySpan inputText, ICommandContext context, out Task? commandTask); +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs new file mode 100644 index 0000000000..bc0143e7b5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/PlayerToServerCommandContext.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +internal sealed record PlayerToServerCommandContext : ICommandContext +{ + private readonly IPacketSender packetSender; + public ILogger Logger { get; set; } = NullLogger.Instance; + public CommandOrigin Origin { get; init; } = CommandOrigin.PLAYER; + public string OriginName => Player.Name; + public SessionId OriginId { get; init; } + public Perms Permissions { get; init; } + + /// + /// Gets the player which issued the command. + /// + public Player Player { get; init; } + + public PlayerToServerCommandContext(IPacketSender packetSender, Player player) + { + ArgumentNullException.ThrowIfNull(player); + this.packetSender = packetSender; + Player = player; + OriginId = player.SessionId; + Permissions = player.Permissions; + } + + public async Task ReplyAsync(T data) => await SendAsync(OriginId, data); + + public async ValueTask SendAsync(SessionId sessionId, T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketAsync(packet, sessionId); + break; + case string message when !string.IsNullOrWhiteSpace(message): + await packetSender.SendPacketAsync(new ChatMessage(SessionId.SERVER_ID, message), sessionId); + break; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } + + public async ValueTask SendToAllAsync(T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketToAllAsync(packet); + break; + case string message when !string.IsNullOrWhiteSpace(message): + await packetSender.SendPacketToAllAsync(new ChatMessage(SessionId.SERVER_ID, message)); + break; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } + + public async ValueTask SendToOthersAsync(T data) + { + switch (data) + { + case Packet packet: + await packetSender.SendPacketToOthersAsync(packet, OriginId); + break; + case string message when !string.IsNullOrWhiteSpace(message): + await packetSender.SendPacketToOthersAsync(new ChatMessage(SessionId.SERVER_ID, message), OriginId); + break; + default: + ICommandContext.ThrowNotSupportedData(data); + break; + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresOrigin.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresOrigin.cs new file mode 100644 index 0000000000..82f21819fd --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresOrigin.cs @@ -0,0 +1,10 @@ +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +internal sealed class RequiresOrigin(CommandOrigin acceptedOrigin) : Attribute +{ + /// + /// Gets the accepted origin for this command. Commands not part of the issuer origin will be hidden and blocked. + /// + public CommandOrigin AcceptedOrigin { get; } = acceptedOrigin; +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresPermissionAttribute.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresPermissionAttribute.cs new file mode 100644 index 0000000000..fa563ed2d5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/RequiresPermissionAttribute.cs @@ -0,0 +1,12 @@ +using Nitrox.Model.DataStructures.GameLogic; + +namespace Nitrox.Server.Subnautica.Models.Commands.Core; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +internal sealed class RequiresPermissionAttribute(Perms minimumPermission) : Attribute +{ + /// + /// Gets the minimum permission needed to use this command. + /// + public Perms MinimumPermission { get; } = minimumPermission; +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/DebugStartMapCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/DebugStartMapCommand.cs deleted file mode 100644 index 6bd565fae4..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/DebugStartMapCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -#if DEBUG -using System.Collections.Generic; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.DataStructures.Unity; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.Resources.Parsers; - -namespace Nitrox.Server.Subnautica.Models.Commands -{ - - internal class DebugStartMapCommand : Command - { - private readonly RandomStartResource randomStartResource; - private readonly IOptions options; - private readonly PlayerManager playerManager; - - public DebugStartMapCommand(PlayerManager playerManager, RandomStartResource randomStartResource, IOptions options) : - base("debugstartmap", Perms.ADMIN, "Spawns blocks at spawn positions") - { - this.playerManager = playerManager; - this.randomStartResource = randomStartResource; - this.options = options; - } - - protected override void Execute(CallArgs args) - { - List randomStartPositions = randomStartResource.RandomStartGenerator.GenerateRandomStartPositions(options.Value.Seed ?? throw new InvalidOperationException()); - - playerManager.SendPacketToAllPlayers(new DebugStartMapPacket(randomStartPositions)); - SendMessage(args.Sender, $"Rendered {randomStartPositions.Count} spawn positions"); - } - } - -} -#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/DebugStartMapCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/DebugStartMapCommand.cs new file mode 100644 index 0000000000..06a23d9bf0 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Debugging/DebugStartMapCommand.cs @@ -0,0 +1,28 @@ +#if DEBUG +using System.ComponentModel; +using System.Linq; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.DataStructures.Unity; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Factories; +using Nitrox.Server.Subnautica.Models.Resources.Parsers; + +namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; + +[RequiresPermission(Perms.ADMIN)] +[Description("Spawns blocks at spawn positions")] +internal sealed class DebugStartMapCommand(RandomFactory randomFactory, RandomStartResource randomStart) : ICommandHandler +{ + private readonly RandomFactory randomFactory = randomFactory; + private readonly RandomStartResource randomStart = randomStart; + + public async Task Execute(ICommandContext context, int amount = 1000, int seed = 0) + { + RandomStartGenerator randomResource = await randomStart.GetRandomStartGeneratorAsync(); + NitroxVector3[] randomStartPositions = randomResource.GenerateAllStartPositions(randomFactory.GetDotnetRandom(seed)).Take(amount).ToArray(); + await context.SendToAllAsync(new DebugStartMapPacket(randomStartPositions)); + await context.ReplyAsync($"Rendered {randomStartPositions.Length} spawn positions"); + } +} +#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/LoadBatchCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/LoadBatchCommand.cs new file mode 100644 index 0000000000..88074b97bf --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Debugging/LoadBatchCommand.cs @@ -0,0 +1,26 @@ +#if DEBUG +using System.Collections.Generic; +using System.ComponentModel; +using Nitrox.Model.DataStructures; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; + +namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class LoadBatchCommand(BatchEntitySpawner batchEntitySpawner) : ICommandHandler +{ + private readonly BatchEntitySpawner batchEntitySpawner = batchEntitySpawner; + + [Description("Loads entities at x y z")] + public async Task Execute(ICommandContext context, int xCoordinate, int yCoordinate, int zCoordinate) + { + NitroxInt3 batchId = new(xCoordinate, yCoordinate, zCoordinate); + List entities = await batchEntitySpawner.LoadUnspawnedEntitiesAsync(batchId); + + await context.ReplyAsync($"Loaded {entities.Count} entities from batch {batchId}"); + } +} +#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/PlayerCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/PlayerCommand.cs new file mode 100644 index 0000000000..805c3f794b --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Debugging/PlayerCommand.cs @@ -0,0 +1,59 @@ +#if DEBUG +using System.Collections.Generic; +using System.ComponentModel; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.DataStructures.Unity; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic.Entities; + +namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; + +[RequiresPermission(Perms.HOST)] +internal sealed class PlayerCommand(SimulationOwnershipData simulationOwnership, WorldEntityManager entityManager, PlayerManager playerManager, ILogger logger) : ICommandHandler +{ + private readonly SimulationOwnershipData simulationOwnership = simulationOwnership; + private readonly WorldEntityManager entityManager = entityManager; + private readonly ILogger logger = logger; + private readonly PlayerManager playerManager = playerManager; + + [Description("Lists all visible cells of a player, their simulated entities per cell and the player's visible out of cell entities")] + public Task Execute(ICommandContext context, [Description("name of the target player")] Player selectedPlayer) + { + if (!playerManager.TryGetPlayerBySessionId(selectedPlayer.SessionId, out Player player)) + { + logger.ZLogError($"Player not found"); + return Task.CompletedTask; + } + + List visibleCells = player.GetVisibleCells(); + + logger.ZLogInformation($"{player}"); + logger.ZLogInformation($"Visible cells [{visibleCells.Count}]:"); + foreach (AbsoluteEntityCell visibleCell in visibleCells) + { + string simulatedEntities = ""; + foreach (WorldEntity worldEntity in entityManager.GetEntities(visibleCell)) + { + if (simulationOwnership.TryGetLock(worldEntity.Id, out SimulationOwnershipData.PlayerLock playerLock) && + playerLock.Player.Id == player.Id) + { + simulatedEntities += $"[{worldEntity.Id}; {worldEntity.TechType?.ToString() ?? worldEntity.ClassId}], "; + } + } + logger.ZLogInformation($"{visibleCell}; {NitroxVector3.Distance(visibleCell.Position, player.Position)}"); + if (simulatedEntities.Length > 0) + { + // Get everything but the last ", " of the string + logger.ZLogInformation($"{simulatedEntities[..^2]}"); + } + } + logger.ZLogInformation($"\nOut of cell entities:\n{string.Join(", ", player.OutOfCellVisibleEntities)}"); + + return Task.CompletedTask; + } +} +#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs new file mode 100644 index 0000000000..fb74d79fa9 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs @@ -0,0 +1,46 @@ +#if DEBUG +using System.ComponentModel; +using Nitrox.Model.DataStructures; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic.Entities; + +namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; + +[RequiresPermission(Perms.HOST)] +internal sealed class QueryCommand(EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) : ICommandHandler +{ + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly ILogger logger = logger; + + [Description("Query the entity associated with the given NitroxId")] + public Task Execute(ICommandContext context, [Description("NitroxId of an entity")] NitroxId entityId) + { + if (!entityRegistry.TryGetEntityById(entityId, out Entity entity)) + { + logger.ZLogError($"Entity with id {entityId} not found"); + return Task.CompletedTask; + } + + logger.ZLogInformation($"{entity}"); + if (entity is WorldEntity worldEntity and not GlobalRootEntity) + { + logger.ZLogInformation($"{worldEntity.AbsoluteEntityCell}"); + } + if (simulationOwnershipData.TryGetLock(entityId, out SimulationOwnershipData.PlayerLock playerLock)) + { + logger.ZLogInformation($"Lock owner: {playerLock.Player.Name}"); + } + else + { + logger.ZLogInformation($"Not locked"); + } + + return Task.CompletedTask; + } +} +#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/SeedNearPositionCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/SeedNearPositionCommand.cs new file mode 100644 index 0000000000..93a49240b7 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/Debugging/SeedNearPositionCommand.cs @@ -0,0 +1,73 @@ +#if DEBUG +using System.ComponentModel; +using System.Linq; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.DataStructures.Unity; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Factories; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Resources.Parsers; + +namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; + +[Alias("seednearpos")] +[RequiresPermission(Perms.ADMIN)] // This command is CPU heavy: admin perms. +[Description("Gets a world seed where the spawn position is the closest to the X and Z coordinates")] +internal sealed class SeedNearPositionCommand(RandomStartResource randomStart) : ICommandHandler, ICommandHandler +{ + private readonly RandomStartResource randomStart = randomStart; + + public async Task Execute(ICommandContext context, int x, int z, int iterations) + { + iterations = int.Max(1000, iterations); + RandomStartGenerator randomResource = await randomStart.GetRandomStartGeneratorAsync(); + NitroxVector3 desiredPos = new(x, 0, z); + LoopState best = new(0, null, ""); + Lock bestLock = new(); + Parallel.For(0, + iterations, + () => new(0, null, RandomFactory.GetCsFilePathFromType(typeof(EscapePodManager))), + (i, _, state) => + { + NitroxVector3 currentPos = randomResource.GenerateAllStartPositions(new Random(RandomFactory.CreateSeedInt32(i.ToString(), state.CsFilePathForSeed))).FirstOrDefault(); + if (state.Position == null || NitroxVector3.Distance(currentPos, desiredPos) < NitroxVector3.Distance(state.Position.Value, desiredPos)) + { + state.Position = currentPos; + state.Seed = i; + } + return state; + }, state => + { + if (!state.Position.HasValue) + { + return; + } + lock (bestLock) + { + if (!best.Position.HasValue || NitroxVector3.Distance(state.Position.Value, desiredPos) < NitroxVector3.Distance(best.Position.Value, desiredPos)) + { + best.Position = state.Position; + best.Seed = state.Seed; + } + } + }); + if (!best.Position.HasValue) + { + await context.ReplyAsync($"Failed to generate a seed close to {desiredPos}"); + return; + } + + await context.ReplyAsync($"Seed \"{best.Seed}\" is near to {desiredPos}. Actual position {best.Position} which is {NitroxVector3.Distance(best.Position.Value, desiredPos)} units off target."); + } + + [RequiresPermission(Perms.PLAYER)] // Lower iteration count makes it OK to call with weak perms. + public async Task Execute(ICommandContext context, int x, int z) => await Execute(context, x, z, 1000); + + private record LoopState(int Seed, NitroxVector3? Position, string CsFilePathForSeed) + { + public NitroxVector3? Position = Position; + public int Seed = Seed; + } +} +#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/DeopCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/DeopCommand.cs index cafcba9ee4..a9ed4ea1fe 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/DeopCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/DeopCommand.cs @@ -1,25 +1,37 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class DeopCommand(PlayerManager playerManager) : ICommandHandler { - internal class DeopCommand : Command - { - public DeopCommand() : base("deop", Perms.ADMIN, "Removes admin rights from user") - { - AddParameter(new TypePlayer("name", true, "Username to remove admin rights from")); - } + private const Perms DEOP_PERMS_DEFAULT = Perms.PLAYER; + private readonly PlayerManager playerManager = playerManager; - protected override void Execute(CallArgs args) + [Description("Removes admin rights from user")] + public async Task Execute(ICommandContext context, [Description("Username to remove admin rights from")] Player targetPlayer) + { + switch (context) { - Player targetPlayer = args.Get(0); - targetPlayer.Permissions = Perms.PLAYER; - - // Need to notify him so that he no longer shows admin stuff on client (which would in any way stop working) - targetPlayer.SendPacket(new PermsChanged(targetPlayer.Permissions)); - SendMessage(targetPlayer, "You were demoted to PLAYER"); - SendMessage(args.Sender, $"Updated {targetPlayer.Name}\'s permissions to PLAYER"); + case not null when targetPlayer.Id == context.OriginId: + await context.ReplyAsync("You can't deop yourself!"); + break; + case not null when targetPlayer.Permissions >= context.Permissions: + await context.ReplyAsync($"You're not allowed to remove admin permissions of {targetPlayer.Name}"); + break; + default: + if (!playerManager.SetPlayerProperty(targetPlayer.SessionId, DEOP_PERMS_DEFAULT, (player, perms) => player.Permissions = perms)) + { + await context.ReplyAsync($"Failed to change permissions to {DEOP_PERMS_DEFAULT}"); + break; + } + await context.SendAsync(targetPlayer.SessionId, new PermsChanged(DEOP_PERMS_DEFAULT)); // Notify so they no longer get admin stuff on client (which would in any way stop working) + await context.SendAsync(targetPlayer.SessionId, $"You were demoted to {DEOP_PERMS_DEFAULT}"); + await context.ReplyAsync($"Updated {targetPlayer.Name}'s permissions to {DEOP_PERMS_DEFAULT}"); + break; } } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/DirectoryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/DirectoryCommand.cs index 078b25a12c..5a111cc9da 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/DirectoryCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/DirectoryCommand.cs @@ -1,47 +1,58 @@ -using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using System.Reflection; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[Alias("dir")] +[RequiresOrigin(CommandOrigin.SERVER)] +internal sealed class DirectoryCommand(IOptions optionsProvider, ILogger logger) : ICommandHandler { - internal class DirectoryCommand : Command - { - private readonly ILogger logger; - public override IEnumerable Aliases { get; } = ["dir"]; + private readonly IOptions optionsProvider = optionsProvider; + private readonly ILogger logger = logger; - public DirectoryCommand(ILogger logger) : base("directory", Perms.HOST, "Opens the current directory of the server") + [Description("Opens save directory or other directory by name")] + public Task Execute(ICommandContext context, [Description("Common name of the directory to open")] CommonDirectory commonDirectory = CommonDirectory.SAVE) + { + string path = GetPathFromCommonDirectory(commonDirectory); + if (!Directory.Exists(path)) { - this.logger = logger; + logger.ZLogError($"Could not find or access directory {commonDirectory}"); + return Task.CompletedTask; } - protected override void Execute(CallArgs args) + logger.ZLogInformation($"Opening directory {path:@Path}"); + using Process proc = Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true, + Verb = "open" + }); + return Task.CompletedTask; + } + + private string? GetPathFromCommonDirectory(CommonDirectory commonDir = CommonDirectory.SELF) => + commonDir switch { - string path; - try - { - path = NitroxUser.ExecutableRootPath; - } - catch (Exception ex) - { - logger.ZLogError(ex, $"Failed to get location of server executable"); - return; - } - path = path.EndsWith(Path.DirectorySeparatorChar) ? path : $"{path}{Path.DirectorySeparatorChar}"; - if (!Directory.Exists(path)) - { - logger.LogOpenDirectoryNotExists(path); - return; - } + CommonDirectory.SELF => Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location), + CommonDirectory.SAVE => optionsProvider.Value.GetServerSavePath(), + CommonDirectory.LOG => optionsProvider.Value.GetServerLogsPath(), + CommonDirectory.GAME => optionsProvider.Value.GamePath, + _ => throw new ArgumentOutOfRangeException(nameof(commonDir), commonDir, null) + }; - logger.LogOpenDirectory(path); - using Process? proc = Process.Start(new ProcessStartInfo - { - FileName = path, - Verb = "open", - UseShellExecute = true - }); - } + public enum CommonDirectory + { + SELF = 0, + EXE = 0, + EXECUTABLE = 0, + SAVE = 1, + SAVES = 1, + LOG = 2, + LOGS = 2, + GAME = 3, + GAMEFILES = 3 } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/FastCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/FastCommand.cs index 1f810488cb..1476f0b596 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/FastCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/FastCommand.cs @@ -1,63 +1,50 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal sealed class FastCommand : Command +[RequiresPermission(Perms.MODERATOR)] +internal sealed class FastCommand(SessionSettings sessionSettings) : ICommandHandler, ICommandHandler { - private readonly PlayerManager playerManager; - private readonly SessionSettings sessionSettings; - - public FastCommand(PlayerManager playerManager, SessionSettings sessionSettings) : base("fast", Perms.MODERATOR, "Enables/disables a fast cheat command, whether it be \"hatch\" or \"grow\"") + private readonly SessionSettings sessionSettings = sessionSettings; + + [Description("Enables/disables a fast cheat command, whether it be \"hatch\" or \"grow\"")] + public async Task Execute(ICommandContext context, + [Description("The name of the fast cheat")] + FastCheatChanged.FastCheat cheat, + [Description("Whether the cheat will be enabled or disabled. Default count as a toggle")] + bool toggle) { - AddParameter(new TypeEnum("cheat", true, "The name of the fast cheat to change: \"hatch\" or \"grow\"")); - AddParameter(new TypeBoolean("value", false, "Whether the cheat will be enabled or disabled. Default count as a toggle")); - this.playerManager = playerManager; - this.sessionSettings = sessionSettings; - } - - protected override void Execute(CallArgs args) - { - FastCheatChanged.FastCheat cheat = args.Get(0); - - bool value = cheat switch + bool currentCheatValue = IsCheatEnabled(cheat); + if (currentCheatValue == toggle) { - FastCheatChanged.FastCheat.HATCH => sessionSettings.FastHatch, - FastCheatChanged.FastCheat.GROW => sessionSettings.FastGrow, - _ => throw new ArgumentException("Must provide a valid cheat name: \"hatch\" or \"grow\""), - }; - - - if (args.IsValid(1)) - { - bool newValue = args.Get(1); - if (newValue == value) - { - SendMessage(args.Sender, $"Fast {cheat} already set to {newValue}"); - return; - } - value = newValue; - } - else - { - // If the value wasn't provided then we toggle it - value = !value; + await context.ReplyAsync($"Fast {cheat} already set to {currentCheatValue}"); + return; } switch (cheat) { case FastCheatChanged.FastCheat.HATCH: - sessionSettings.FastHatch = value; + sessionSettings.FastHatch = toggle; break; case FastCheatChanged.FastCheat.GROW: - sessionSettings.FastGrow = value; + sessionSettings.FastGrow = toggle; break; } - playerManager.SendPacketToAllPlayers(new FastCheatChanged(cheat, value)); - SendMessageToAllPlayers($"Fast {cheat} changed to {value} by {args.SenderName}"); + await context.SendToAllAsync(new FastCheatChanged(cheat, currentCheatValue)); + await context.SendToAllAsync($"Fast {cheat} changed to {currentCheatValue} by {context.OriginName}"); } + + public async Task Execute(ICommandContext context, FastCheatChanged.FastCheat cheat) => await Execute(context, cheat, !IsCheatEnabled(cheat)); + + private bool IsCheatEnabled(FastCheatChanged.FastCheat cheat) => + cheat switch + { + FastCheatChanged.FastCheat.HATCH => sessionSettings.FastHatch, + FastCheatChanged.FastCheat.GROW => sessionSettings.FastGrow, + _ => false + }; } diff --git a/Nitrox.Server.Subnautica/Models/Commands/GameModeCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/GameModeCommand.cs index e996ea74bd..c1515b6468 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/GameModeCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/GameModeCommand.cs @@ -1,50 +1,42 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal class GameModeCommand : Command +[RequiresPermission(Perms.ADMIN)] +internal sealed class GameModeCommand : ICommandHandler { - private readonly PlayerManager playerManager; - private readonly ILogger logger; - - public GameModeCommand(PlayerManager playerManager, ILogger logger) : base("gamemode", Perms.ADMIN, "Changes a player's gamemode") - { - AddParameter(new TypeEnum("gamemode", true, "Gamemode to change to")); - AddParameter(new TypePlayer("name", false, "Username to whom change the game mode (defaults to self)")); - - this.playerManager = playerManager; - this.logger = logger; - } - - protected override void Execute(CallArgs args) + [Description("Changes a player's gamemode")] + public async Task Execute(ICommandContext context, SubnauticaGameMode gameMode, Player? targetPlayer = null) { - SubnauticaGameMode gameMode = args.Get(0); - Player targetPlayer = args.Get(1); - - if (args.IsConsole && targetPlayer == null) + switch (context.Origin) { - logger.ZLogError($"Console can't use the gamemode command without providing a player name to it."); - return; - } - // The target player if not set, is the player who sent the command - targetPlayer ??= args.Sender.Value; + case CommandOrigin.SERVER when targetPlayer == null: + await context.ReplyAsync("Console can't use the gamemode command without providing a player name."); + return; + case CommandOrigin.PLAYER when context is PlayerToServerCommandContext playerContext: + // The target player (if not set), is the player who sent the command. + targetPlayer ??= playerContext.Player; + goto default; + default: + if (targetPlayer == null) + { + throw new ArgumentException("Target player must not be null"); + } - targetPlayer.GameMode = gameMode; - playerManager.SendPacketToAllPlayers(GameModeChanged.ForPlayer(targetPlayer.Id, gameMode)); - SendMessage(targetPlayer, $"GameMode changed to {gameMode}"); - if (args.IsConsole) - { - logger.ZLogInformation($"Changed {targetPlayer.Name} [{targetPlayer.Id}]'s gamemode to {gameMode}"); - } - else - { - if (targetPlayer != args.Sender.Value) - { - SendMessage(args.Sender.Value, $"GameMode of {targetPlayer.Name} changed to {gameMode}"); - } + targetPlayer.GameMode = gameMode; + await context.SendToAllAsync(GameModeChanged.ForPlayer(targetPlayer.SessionId, gameMode)); + await context.SendAsync(targetPlayer.SessionId, $"GameMode changed to {gameMode}"); + if (context.Origin == CommandOrigin.SERVER) + { + await context.ReplyAsync($"Changed {targetPlayer.Name} [{targetPlayer.Id}]'s gamemode to {gameMode}"); + } + else if (targetPlayer.Id != context.OriginId) + { + await context.ReplyAsync($"GameMode of {targetPlayer.Name} changed to {gameMode}"); + } + break; } } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/HelpCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/HelpCommand.cs index a85f15ccbb..e1f502b134 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/HelpCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/HelpCommand.cs @@ -1,67 +1,98 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Linq; -using Nitrox.Model.Core; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.Text; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +/// +/// Shows helpful information about available commands. +/// +[Alias("?")] +internal sealed class HelpCommand(Func registryProvider) : ICommandHandler, ICommandHandler { - internal class HelpCommand : Command - { - private readonly ILogger logger; - public override IEnumerable Aliases { get; } = new[] { "?" }; + /// + /// is used to lazily retrieve the registry as otherwise it would cause cyclic-dependency + /// error if immediately requested by the constructor. + /// + private readonly Func registryProvider = registryProvider; - public HelpCommand(ILogger logger) : base("help", Perms.PLAYER, "Displays this") + [Description("Shows this help page")] + public async Task Execute(ICommandContext context) + { + StringBuilder sb = new(); + sb.AppendLine("~~~ COMMAND HELP PAGE ~~~"); + CommandRegistry registry = registryProvider(); + foreach (CommandHandlerEntry handler in registry + .Handlers + .OrderBy(h => h.Owner is HelpCommand ? 0 : 1) + .ThenBy(h => h.Name) + .ThenBy(h => h.ParameterTypes.Length)) { - this.logger = logger; - AddParameter(new TypeString("command", false, "Command to see help information for")); + if (!registry.IsValidHandlerForContext(handler, context)) + { + continue; + } + sb.AppendLine(handler.ToString()); } + await context.ReplyAsync(sb.Remove(sb.Length - Environment.NewLine.Length, Environment.NewLine.Length).ToString()); + } - protected override void Execute(CallArgs args) + [Description("Shows the help page of the given command")] + public async Task Execute(ICommandContext context, string commandName) + { + if (!await TryShowHelpForCommandAsync(context, commandName)) { - List cmdsText; - if (args.IsConsole) - { - cmdsText = GetHelpText(Perms.HOST, false, args.IsValid(0) ? args.Get(0) : null); - using (logger.BeginPlainScope()) - { - foreach (string cmdText in cmdsText) - { - logger.ZLogInformation($"{cmdText}"); - } - } - } - else - { - cmdsText = GetHelpText(args.Sender.Value.Permissions, true, args.IsValid(0) ? args.Get(0) : null); + await context.ReplyAsync($"No command exists with the name {commandName}"); + } + } - foreach (string cmdText in cmdsText) - { - SendMessageToPlayer(args.Sender, cmdText); - } - } + private async ValueTask TryShowHelpForCommandAsync(ICommandContext context, string commandName) + { + if (!registryProvider().TryGetHandlersByCommandName(context, commandName, out List handlers)) + { + return false; + } + CommandHandlerEntry baseHandler = handlers.FirstOrDefault(); + if (baseHandler == null) + { + return false; } - private List GetHelpText(Perms permThreshold, bool cropText, string singleCommand) + StringBuilder sb = new(); + sb.Append("~~~ ") + .Append(baseHandler.Name.ToUpperInvariant()) + .Append(" COMMAND HAS ") + .Append(handlers.Count) + .Append(" HANDLER(S) ~~~"); + if (baseHandler.Aliases.Length > 0) { - //Runtime query to avoid circular dependencies - IEnumerable commands = NitroxServiceLocator.LocateService>(); - if (singleCommand != null && !commands.Any(cmd => cmd.Name.Equals(singleCommand))) + sb.AppendLine() + .Append("Aliases: ") + .AppendLine(string.Join(", ", baseHandler.Aliases)); + } + sb.AppendLine(); + bool first = true; + foreach (CommandHandlerEntry handler in handlers.OrderBy(h => h.Parameters.Length)) + { + if (!first) { - return ["Command does not exist"]; + sb.AppendLine(); } - List cmdsText = []; - cmdsText.Add(singleCommand != null ? $"=== Showing help for {singleCommand} ===" : "=== Showing command list ==="); - cmdsText.AddRange(commands.Where(cmd => CanExecuteAndProcess(cmd, permThreshold) && (singleCommand == null || cmd.Name.Equals(singleCommand))) - .OrderByDescending(cmd => cmd.Name) - .Select(cmd => cmd.ToHelpText(singleCommand != null, cropText))); - return cmdsText; + first = false; - static bool CanExecuteAndProcess(Command cmd, Perms perms) + if (handler.Parameters.Length == 0) + { + sb.Append("no args - ") + .Append(handler.Description); + } + else { - return cmd.CanExecute(perms) && !(perms == Perms.HOST && cmd.Flags.HasFlag(PermsFlag.NO_CONSOLE)); + sb.Append(handler.ToDisplayString(false)); } } + await context.ReplyAsync(sb.ToString()); + return true; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/KickCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/KickCommand.cs index db387324b5..17ece32a4f 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/KickCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/KickCommand.cs @@ -1,57 +1,35 @@ -using System.Collections.Generic; -using Nitrox.Model.DataStructures; +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Administration; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.MODERATOR)] +internal sealed class KickCommand(IKickPlayer playerKicker) : ICommandHandler { - internal class KickCommand : Command - { - private readonly EntitySimulation entitySimulation; - private readonly PlayerManager playerManager; + private readonly IKickPlayer playerKicker = playerKicker; - public KickCommand(PlayerManager playerManager, EntitySimulation entitySimulation) : base("kick", Perms.MODERATOR, "Kicks a player from the server") + [Description("Kicks a player from the server")] + public async Task Execute(ICommandContext context, Player playerToKick, string reason = "") + { + if (context.OriginId == playerToKick.SessionId) { - AddParameter(new TypePlayer("name", true, "Name of the player to kick")); - AddParameter(new TypeString("reason", false, "Reason for kicking the player")); - - AllowedArgOverflow = true; - - this.playerManager = playerManager; - this.entitySimulation = entitySimulation; + await context.ReplyAsync("You can't kick yourself"); + return; } - protected override void Execute(CallArgs args) + switch (context.Origin) { - Player playerToKick = args.Get(0); - - if (args.SenderName == playerToKick.Name) - { - SendMessage(args.Sender, "You can't kick yourself"); - return; - } - - if (!args.IsConsole && playerToKick.Permissions >= args.Sender.Value.Permissions) - { - SendMessage(args.Sender, $"You're not allowed to kick {playerToKick.Name}"); - return; - } - - playerToKick.SendPacket(new PlayerKicked(args.GetTillEnd(1))); - playerManager.PlayerDisconnected(playerToKick.Connection); - - List revokedEntities = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(playerToKick); - if (revokedEntities.Count > 0) - { - SimulationOwnershipChange ownershipChange = new(revokedEntities); - playerManager.SendPacketToAllPlayers(ownershipChange); - } - - playerManager.SendPacketToOtherPlayers(new Disconnect(playerToKick.Id), playerToKick); - SendMessage(args.Sender, $"The player {playerToKick.Name} has been disconnected"); + case CommandOrigin.PLAYER when playerToKick.Permissions >= context.Permissions: + await context.ReplyAsync($"You're not allowed to kick {playerToKick.Name}"); + break; + default: + if (!await playerKicker.KickPlayer(playerToKick.SessionId, reason)) + { + await context.ReplyAsync($"Failed to kick '{playerToKick.Name}'"); + } + break; } } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ListCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/ListCommand.cs index 1ec50d0e70..f4c33b46f4 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/ListCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/ListCommand.cs @@ -1,31 +1,25 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class ListCommand : Command - { - private readonly PlayerManager playerManager; - private readonly IOptions options; +namespace Nitrox.Server.Subnautica.Models.Commands; - public ListCommand(IOptions options, PlayerManager playerManager) : base("list", Perms.PLAYER, "Shows who's online") - { - this.playerManager = playerManager; - this.options = options; - } +internal sealed class ListCommand(IOptions options, PlayerManager playerManager) : ICommandHandler +{ + private readonly PlayerManager playerManager = playerManager; + private readonly IOptions options = options; - protected override void Execute(CallArgs args) - { - IList players = playerManager.GetConnectedPlayers().Select(player => player.Name).ToList(); + [Description("Shows who's online")] + public async Task Execute(ICommandContext context) + { + IList players = playerManager.GetConnectedPlayers().Select(player => $"{player.Name} #{player.SessionId}").ToList(); - StringBuilder builder = new($"List of players ({players.Count}/{options.Value.MaxConnections}):\n"); - builder.Append(string.Join(", ", players)); + StringBuilder builder = new($"List of players ({players.Count}/{options.Value.MaxConnections}):\n"); + builder.Append(string.Join(", ", players)); - SendMessage(args.Sender, builder.ToString()); - } + await context.ReplyAsync(builder.ToString()); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/LoadBatchCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/LoadBatchCommand.cs deleted file mode 100644 index 3f46b1bda1..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/LoadBatchCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if DEBUG -using System.Collections.Generic; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; - -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class LoadBatchCommand : Command - { - private readonly BatchEntitySpawner batchEntitySpawner; - - public LoadBatchCommand(BatchEntitySpawner batchEntitySpawner) : base("loadbatch", Perms.ADMIN, "Loads entities at x y z") - { - AddParameter(new TypeInt("x", true, "x coordinate")); - AddParameter(new TypeInt("y", true, "y coordinate")); - AddParameter(new TypeInt("z", true, "z coordinate")); - - this.batchEntitySpawner = batchEntitySpawner; - } - - protected override void Execute(CallArgs args) - { - NitroxInt3 batchId = new(args.Get(0), args.Get(1), args.Get(2)); - List entities = batchEntitySpawner.LoadUnspawnedEntities(batchId); - - SendMessage(args.Sender, $"Loaded {entities.Count} entities from batch {batchId}"); - } - } -} -#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/LoginCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/LoginCommand.cs index 02c6802133..6503162671 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/LoginCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/LoginCommand.cs @@ -1,31 +1,40 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresOrigin(CommandOrigin.PLAYER)] +internal sealed class LoginCommand(IOptions optionsProvider) : ICommandHandler { - internal class LoginCommand : Command - { - private readonly IOptions options; + private readonly IOptions optionsProvider = optionsProvider; - public LoginCommand(IOptions options) : base("login", Perms.PLAYER, PermsFlag.NO_CONSOLE, "Log in to server as admin (requires password)") + [Description("Log in to server as admin (requires password)")] + public async Task Execute(ICommandContext context, [Description("The admin password for the server")] string adminPassword) + { + string activePassword = optionsProvider.Value.AdminPassword; + if (string.IsNullOrWhiteSpace(activePassword)) { - AddParameter(new TypeString("password", true, "The admin password for the server")); - - this.options = options; + await context.ReplyAsync("Logging in with admin password is disabled"); + return; } - protected override void Execute(CallArgs args) + switch (context) { - if (args.Get(0) == options.Value.AdminPassword) - { - args.Sender.Value.Permissions = Perms.ADMIN; - SendMessage(args.Sender, $"Updated permissions to ADMIN for {args.SenderName}"); - } - else - { - SendMessage(args.Sender, "Incorrect Password"); - } + case PlayerToServerCommandContext { Player: { Permissions: < Perms.ADMIN } player }: + if (activePassword == adminPassword) + { + player.Permissions = Perms.ADMIN; + await context.ReplyAsync($"You've been made {nameof(Perms.ADMIN)} on this server!"); + } + else + { + await context.ReplyAsync("Incorrect Password"); + } + break; + default: + await context.ReplyAsync("You already have admin permissions"); + break; } } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/MuteCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/MuteCommand.cs index 020f00d65a..7382e102c0 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/MuteCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/MuteCommand.cs @@ -1,47 +1,36 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.MODERATOR)] +internal sealed class MuteCommand : ICommandHandler { - internal class MuteCommand : Command + public async Task Execute(ICommandContext context, [Description("Player to mute")] Player targetPlayer) { - private readonly PlayerManager playerManager; - - public MuteCommand(PlayerManager playerManager) : base("mute", Perms.MODERATOR, "Prevents a user from chatting") + if (context.OriginId == targetPlayer.SessionId) { - this.playerManager = playerManager; - AddParameter(new TypePlayer("name", true, "Player to mute")); + await context.ReplyAsync("You can't mute yourself"); + return; } - - protected override void Execute(CallArgs args) + if (context.Permissions <= targetPlayer.Permissions) { - Player targetPlayer = args.Get(0); - - if (args.SenderName == targetPlayer.Name) - { - SendMessage(args.Sender, "You can't mute yourself"); - return; - } - - if (!args.IsConsole && targetPlayer.Permissions >= args.Sender.Value.Permissions) - { - SendMessage(args.Sender, $"You're not allowed to mute {targetPlayer.Name}"); - return; - } - - if (targetPlayer.PlayerContext.IsMuted) - { - SendMessage(args.Sender, $"{targetPlayer.Name} is already muted"); - args.Sender.Value.SendPacket(new MutePlayer(targetPlayer.Id, targetPlayer.PlayerContext.IsMuted)); - return; - } + await context.ReplyAsync($"You're not allowed to mute {targetPlayer.Name}"); + return; + } - targetPlayer.PlayerContext.IsMuted = true; - playerManager.SendPacketToAllPlayers(new MutePlayer(targetPlayer.Id, targetPlayer.PlayerContext.IsMuted)); - SendMessage(targetPlayer, "You're now muted"); - SendMessage(args.Sender, $"Muted {targetPlayer.Name}"); + if (targetPlayer.PlayerContext.IsMuted) + { + await context.ReplyAsync($"{targetPlayer.Name} is already muted"); + // Send state anyway in case it got desynced. + await context.ReplyAsync(new MutePlayer(targetPlayer.SessionId, targetPlayer.PlayerContext.IsMuted)); + return; } + + targetPlayer.PlayerContext.IsMuted = true; + await context.SendToAllAsync(new MutePlayer(targetPlayer.SessionId, targetPlayer.PlayerContext.IsMuted)); + await context.SendAsync(targetPlayer.SessionId, "You're now muted"); + await context.ReplyAsync($"Muted {targetPlayer.Name}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/OpCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/OpCommand.cs index 477227642f..ae7702f51e 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/OpCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/OpCommand.cs @@ -1,25 +1,20 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class OpCommand : ICommandHandler { - internal class OpCommand : Command + public async Task Execute(ICommandContext context, [Description("The players name to make an admin")] Player targetPlayer) { - public OpCommand() : base("op", Perms.ADMIN, "Sets a user as admin") - { - AddParameter(new TypePlayer("name", true, "The players name to make an admin")); - } - - protected override void Execute(CallArgs args) - { - Player targetPlayer = args.Get(0); - targetPlayer.Permissions = Perms.ADMIN; + Perms newPerms = Perms.ADMIN; + targetPlayer.Permissions = newPerms; - // We need to notify this player that he can show all the admin-related stuff - targetPlayer.SendPacket(new PermsChanged(targetPlayer.Permissions)); - SendMessage(targetPlayer, "You were promoted to ADMIN"); - SendMessage(args.Sender, $"Updated {targetPlayer.Name}\'s permissions to ADMIN"); - } + // We need to notify this player that he can show all the admin-related stuff + await context.SendAsync(targetPlayer.SessionId, new PermsChanged(newPerms)); + await context.SendAsync(targetPlayer.SessionId, $"You were promoted to {newPerms}"); + await context.ReplyAsync($"Updated {targetPlayer.Name}\'s permissions to {newPerms}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/PlayerCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/PlayerCommand.cs deleted file mode 100644 index a0133fad4c..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/PlayerCommand.cs +++ /dev/null @@ -1,66 +0,0 @@ -#if DEBUG -using System.Collections.Generic; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities; - -namespace Nitrox.Server.Subnautica.Models.Commands; - -internal class PlayerCommand : Command -{ - private readonly PlayerManager playerManager; - private readonly WorldEntityManager worldEntityManager; - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly ILogger logger; - - public PlayerCommand(PlayerManager playerManager, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData, ILogger logger) : base("player", Perms.HOST, "Lists all visible cells of a player, their simulated entities per cell and the player's visible out of cell entities") - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.simulationOwnershipData = simulationOwnershipData; - this.logger = logger; - AddParameter(new TypeString("player name", true, "name of the target player")); - } - - protected override void Execute(CallArgs args) - { - string playerName = args.Get(0); - - if (playerManager.TryGetPlayerByName(playerName, out Player player)) - { - List visibleCells = player.GetVisibleCells(); - - logger.ZLogInformation($"{player}"); - logger.ZLogInformation($"Visible cells [{visibleCells.Count}]:"); - foreach (AbsoluteEntityCell visibleCell in visibleCells) - { - string simulatedEntities = ""; - foreach (WorldEntity worldEntity in worldEntityManager.GetEntities(visibleCell)) - { - if (simulationOwnershipData.TryGetLock(worldEntity.Id, out SimulationOwnershipData.PlayerLock playerLock) && - playerLock.Player.Id == player.Id) - { - simulatedEntities += $"[{worldEntity.Id}; {worldEntity.TechType?.ToString() ?? worldEntity.ClassId}], "; - } - } - logger.ZLogInformation($"{visibleCell}; {NitroxVector3.Distance(visibleCell.Position, player.Position)}"); - if (simulatedEntities.Length > 0) - { - // Get everything but the last ", " of the string - logger.ZLogInformation($"{simulatedEntities[..^2]}"); - } - } - logger.ZLogInformation($"\nOut of cell entities:\n{string.Join(", ", player.OutOfCellVisibleEntities)}"); - } - else - { - logger.ZLogError($"Player with name {playerName} not found"); - } - } -} -#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/Processor/TextCommandProcessor.cs b/Nitrox.Server.Subnautica/Models/Commands/Processor/TextCommandProcessor.cs deleted file mode 100644 index 84dac9d752..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Processor/TextCommandProcessor.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Exceptions; - -namespace Nitrox.Server.Subnautica.Models.Commands.Processor; - -public class TextCommandProcessor -{ - private readonly ILogger logger; - private readonly Dictionary commands = new(); - private readonly char[] splitChar = [' ']; - - public TextCommandProcessor(IEnumerable cmds, ILogger logger) - { - this.logger = logger; - foreach (Command cmd in cmds) - { - if (!commands.TryAdd(cmd.Name, cmd)) - { - throw new DuplicateRegistrationException($"Command {cmd.Name} is registered multiple times."); - } - - foreach (string alias in cmd.Aliases) - { - if (!commands.TryAdd(alias, cmd)) - { - throw new DuplicateRegistrationException($"Command {alias} is registered multiple times."); - } - } - } - if (commands.Count < 1) - { - logger.ZLogWarning($"No commands registered"); - } - } - - public void ProcessCommand(string msg, Optional sender, Perms permissions) - { - if (string.IsNullOrWhiteSpace(msg)) - { - return; - } - Span parts = msg.Split(splitChar, StringSplitOptions.RemoveEmptyEntries); - if (!commands.TryGetValue(parts[0], out Command command)) - { - Command.SendMessage(sender, $"Command not found: {parts[0]}"); - return; - } - if (!sender.HasValue && command.Flags.HasFlag(PermsFlag.NO_CONSOLE)) - { - logger.ZLogError($"This command cannot be used by CONSOLE"); - return; - } - - if (command.CanExecute(permissions)) - { - try - { - command.TryExecute(sender, parts[1..]); - - } - catch (ArgumentException ex) - { - Command.SendMessage(sender, $"Error: {ex.Message}"); - } - catch (Exception ex) - { - logger.ZLogError(ex, $"Fatal error while trying to execute the command"); - } - } - else - { - Command.SendMessage(sender, "You do not have the required permissions for this command !"); - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/PromoteCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/PromoteCommand.cs index 124f9cf770..4955d20b47 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/PromoteCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/PromoteCommand.cs @@ -1,41 +1,33 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +internal sealed class PromoteCommand : ICommandHandler { - internal class PromoteCommand : Command + [Description("Sets specific permissions to a user")] + public async Task Execute(ICommandContext context, [Description("The username to change the permissions of")] Player targetPlayer, [Description("Permission level")] Perms newPerms) { - public PromoteCommand() : base("promote", Perms.MODERATOR, "Sets specific permissions to a user") + if (context.OriginId == targetPlayer.SessionId) { - AddParameter(new TypePlayer("name", true, "The username to change the permissions of")); - AddParameter(new TypeEnum("perms", true, "Permission level")); + await context.ReplyAsync("You can't promote yourself"); + return; } - - protected override void Execute(CallArgs args) + if (context.Permissions < newPerms) { - Player targetPlayer = args.Get(0); - Perms permissions = args.Get(1); - - if (args.SenderName == targetPlayer.Name) - { - SendMessage(args.Sender, "You can't promote yourself"); - return; - } - - //Allows a bounded permission hierarchy - if (args.IsConsole || permissions < args.Sender.Value.Permissions) - { - targetPlayer.Permissions = permissions; - - targetPlayer.SendPacket(new PermsChanged(targetPlayer.Permissions)); - SendMessage(args.Sender, $"Updated {targetPlayer.Name}\'s permissions to {permissions}"); - SendMessageToPlayer(targetPlayer, $"You've been promoted to {permissions}"); - } - else - { - SendMessage(args.Sender, $"You're not allowed to update {targetPlayer.Name}\'s permissions"); - } + await context.ReplyAsync($"Your permissions ({context.Permissions}) must be higher than the perms you want to assign ({newPerms})"); + return; + } + if (context.Permissions < targetPlayer.Permissions) + { + await context.ReplyAsync($"You're not allowed to update {targetPlayer.Name}\'s permissions"); + return; } + + targetPlayer.Permissions = newPerms; + await context.SendAsync(targetPlayer.SessionId, new PermsChanged(newPerms)); + await context.ReplyAsync($"Updated {targetPlayer.Name}\'s permissions to {newPerms}"); + await context.SendAsync(targetPlayer.SessionId, $"You've been promoted to {newPerms}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/PvpCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/PvpCommand.cs index ef03a76a11..751f94df04 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/PvpCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/PvpCommand.cs @@ -1,43 +1,24 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal class PvpCommand : Command +[RequiresPermission(Perms.ADMIN)] +internal sealed class PvpCommand(IOptions options) : ICommandHandler { - private readonly IOptions options; + private readonly IOptions options = options; - public PvpCommand(IOptions options) : base("pvp", Perms.ADMIN, "Enables/Disables PvP") + [Description("Enables/Disables PvP")] + public async Task Execute(ICommandContext context, bool state) { - AddParameter(new TypeString("state", true, "on/off")); - - this.options = options; - } - - protected override void Execute(CallArgs args) - { - string state = args.Get(0).ToLower(); - - bool pvpEnabled = false; - switch (state) + if (options.Value.PvpEnabled == state) { - case "on": - pvpEnabled = true; - break; - case "off": - break; - default: - SendMessage(args.Sender, "Parameter must be \"on\" or \"off\""); - return; - } - - if (options.Value.PvpEnabled == pvpEnabled) - { - SendMessage(args.Sender, $"PvP is already {state}"); + await context.ReplyAsync($"PvP is already {state}"); return; } - options.Value.PvpEnabled = pvpEnabled; - SendMessageToAllPlayers($"PvP is now {state}"); + + options.Value.PvpEnabled = state; + await context.SendToAllAsync($"PvP is now {state}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs deleted file mode 100644 index 58ffd7edd1..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -#if DEBUG -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities; - -namespace Nitrox.Server.Subnautica.Models.Commands; - -internal class QueryCommand : Command -{ - private readonly EntityRegistry entityRegistry; - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly ILogger logger; - - public QueryCommand(EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) : base("query", Perms.HOST, "Query the entity associated with the given NitroxId") - { - this.entityRegistry = entityRegistry; - this.simulationOwnershipData = simulationOwnershipData; - this.logger = logger; - AddParameter(new TypeNitroxId("entityId", true, "NitroxId of the queried entity")); - } - - protected override void Execute(CallArgs args) - { - NitroxId nitroxId = args.Get(0); - - if (entityRegistry.TryGetEntityById(nitroxId, out Entity entity)) - { - logger.ZLogInformation($"{entity}"); - if (entity is WorldEntity worldEntity && worldEntity.Transform != null && worldEntity is not GlobalRootEntity) - { - logger.ZLogInformation($"{worldEntity.AbsoluteEntityCell}"); - } - if (simulationOwnershipData.TryGetLock(nitroxId, out SimulationOwnershipData.PlayerLock playerLock)) - { - logger.ZLogInformation($"Lock owner: {playerLock.Player.Name}"); - } - else - { - logger.ZLogInformation($"Not locked"); - } - } - else - { - logger.ZLogError($"Entity with id {nitroxId} not found"); - } - } -} -#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/SaveCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SaveCommand.cs index 087cf422d3..ae5bde49f8 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/SaveCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/SaveCommand.cs @@ -1,22 +1,20 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Services; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class SaveCommand : Command - { - private readonly SaveService saveService; +namespace Nitrox.Server.Subnautica.Models.Commands; - public SaveCommand(SaveService saveService) : base("save", Perms.MODERATOR, "Saves the map") - { - this.saveService = saveService; - } +[RequiresPermission(Perms.MODERATOR)] +internal sealed class SaveCommand(SaveService saveService, ILogger logger) : ICommandHandler +{ + private readonly SaveService saveService = saveService; + private readonly ILogger logger = logger; - protected override void Execute(CallArgs args) - { - saveService.QueueSave(); - SendMessageToPlayer(args.Sender, "Saving world..."); - } + [Description("Saves the map")] + public async Task Execute(ICommandContext context) + { + logger.LogSaveRequest(context.OriginName, context.OriginId); + await saveService.QueueActionAsync(SaveService.ServiceAction.SAVE); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/SetKeepInventoryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SetKeepInventoryCommand.cs index 9be42248cf..cf6e937c63 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/SetKeepInventoryCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/SetKeepInventoryCommand.cs @@ -1,33 +1,25 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal class SetKeepInventoryCommand : Command -{ - private readonly PlayerManager playerManager; - private readonly IOptions options; - public SetKeepInventoryCommand(PlayerManager playerManager, IOptions options) : base("keepinventory", Perms.ADMIN, "Sets \"keep inventory\" setting to on/off. If \"on\", players won't lose items when they die.") - { - AddParameter(new TypeBoolean("state", true, "The true/false state to set keep inventory on death to")); - this.playerManager = playerManager; - this.options = options; - } +[RequiresPermission(Perms.ADMIN)] +internal sealed class SetKeepInventoryCommand(IOptions options) : ICommandHandler +{ + private readonly IOptions options = options; - protected override void Execute(CallArgs args) + [Description("Sets \"keep inventory\" setting to on/off. If \"on\", players won't lose items when they die.")] + public async Task Execute(ICommandContext context, [Description("The true/false state to set keep inventory on death to")] bool newState) { - bool newKeepInventoryState = args.Get(0); - if (options.Value.KeepInventoryOnDeath != newKeepInventoryState) + if (options.Value.KeepInventoryOnDeath == newState) { - options.Value.KeepInventoryOnDeath = newKeepInventoryState; - playerManager.SendPacketToAllPlayers(new KeepInventoryChanged(newKeepInventoryState)); - SendMessageToAllPlayers($"KeepInventoryOnDeath changed to \"{newKeepInventoryState}\" by {args.SenderName}"); - } - else - { - SendMessage(args.Sender, $"KeepInventoryOnDeath already set to {newKeepInventoryState}"); + await context.ReplyAsync($"KeepInventoryOnDeath already set to {newState}"); + return; } + + options.Value.KeepInventoryOnDeath = newState; + await context.SendToAllAsync(new KeepInventoryChanged(newState)); + await context.SendToAllAsync($"KeepInventoryOnDeath changed to \"{newState}\" by {context.OriginName}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/StopCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/StopCommand.cs index aac39a5c07..f565d94f4c 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/StopCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/StopCommand.cs @@ -1,16 +1,19 @@ -using System.Collections.Generic; +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Models.Commands; -internal class StopCommand(IHostApplicationLifetime lifetimeService) : Command("stop", Perms.ADMIN, "Stops the server") +[Alias("exit", "halt", "quit", "close")] +[RequiresPermission(Perms.ADMIN)] +internal sealed class StopCommand(IHostApplicationLifetime lifetimeService) : ICommandHandler { private readonly IHostApplicationLifetime lifetimeService = lifetimeService; - public override IEnumerable Aliases { get; } = ["exit", "halt", "quit", "close"]; - protected override void Execute(CallArgs args) + [Description("Stops the server")] + public Task Execute(ICommandContext context) { lifetimeService.StopApplication(); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/SummaryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SummaryCommand.cs index 6affb14377..2ed06bff29 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/SummaryCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/SummaryCommand.cs @@ -1,38 +1,26 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.Logging.Scopes; using Nitrox.Server.Subnautica.Services; -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class SummaryCommand : Command - { - private readonly StatusService statusService; - private readonly ILogger logger; - - public SummaryCommand(StatusService statusService, ILogger logger) : base("summary", Perms.MODERATOR, "Shows persisted data") - { - this.statusService = statusService; - this.logger = logger; +namespace Nitrox.Server.Subnautica.Models.Commands; - AllowedArgOverflow = true; - } +[RequiresPermission(Perms.MODERATOR)] +internal sealed class SummaryCommand(StatusService statusService, ILogger logger) : ICommandHandler +{ + private readonly StatusService statusService = statusService; + private readonly ILogger logger = logger; - protected override void Execute(CallArgs args) + [Description("Shows persisted data")] + public async Task Execute(ICommandContext context) + { + string summary; + using (CaptureScope scope = logger.BeginCaptureScope()) { - Perms viewerPerms = args.Sender.OrNull()?.Permissions ?? Perms.HOST; - Player? sender = args.Sender.OrNull(); - // TODO: Make command execute async - Task.Run(async () => - { - string summary; - using (CaptureScope scope = logger.BeginCaptureScope()) - { - await statusService.LogServerSummary(viewerPerms); - summary = string.Join("", scope.Logs); - } - SendMessage(sender, summary.TrimEnd('\n')); - }).ContinueWithHandleError(ex => logger.ZLogError(ex, $"Error while trying to capture server summary")); + await statusService.LogServerSummary(context.Permissions); + summary = string.Join("", scope.Logs); } + await context.ReplyAsync(summary.TrimEnd('\n')); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/SunbeamCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SunbeamCommand.cs index 1b4183ea07..50f0be63fe 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/SunbeamCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/SunbeamCommand.cs @@ -1,47 +1,24 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; namespace Nitrox.Server.Subnautica.Models.Commands; -// TODO: When we make the new command system, move this stuff to it -internal sealed class SunbeamCommand : Command +/// +/// We shouldn't let the server use this command because it needs some stuff to happen client-side like goals. +/// +[RequiresPermission(Perms.ADMIN)] +[RequiresOrigin(CommandOrigin.PLAYER)] +internal sealed class SunbeamCommand(StoryManager storyManager) : ICommandHandler { - private readonly StoryManager storyManager; + private readonly StoryManager storyManager = storyManager; - // We shouldn't let the server use this command because it needs some stuff to happen client-side like goals - public SunbeamCommand(StoryManager storyManager) : base("sunbeam", Perms.ADMIN, PermsFlag.NO_CONSOLE, "Start sunbeam events") + [Description("Start sunbeam events")] + public Task Execute(ICommandContext context, [Description("Which Sunbeam event to start")] PlaySunbeamEvent.SunbeamEvent sunbeamEvent) { - AddParameter(new TypeString("storystart/countdown/gunaim", true, "Which Sunbeam event to start")); + storyManager.StartSunbeamEvent(sunbeamEvent.ToStoryKey()); - this.storyManager = storyManager; - } - - protected override void Execute(CallArgs args) - { - if (!args.Sender.HasValue) - { - SendMessage(args.Sender, "This command can't be used by CONSOLE"); - return; - } - string action = args.Get(0); - - switch (action.ToLower()) - { - case "storystart": - storyManager.StartSunbeamEvent(PlaySunbeamEvent.SunbeamEvent.STORYSTART); - break; - case "countdown": - storyManager.StartSunbeamEvent(PlaySunbeamEvent.SunbeamEvent.COUNTDOWN); - break; - case "gunaim": - storyManager.StartSunbeamEvent(PlaySunbeamEvent.SunbeamEvent.GUNAIM); - break; - default: - // Same message as in the abstract class, in method TryExecute - SendMessage(args.Sender, $"Error: Invalid Parameters\nUsage: {ToHelpText(false, true)}"); - break; - } + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/SwapSerializerCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/SwapSerializerCommand.cs deleted file mode 100644 index 3fc8be64e9..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/SwapSerializerCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Server; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.Serialization.World; - -namespace Nitrox.Server.Subnautica.Models.Commands -{ - internal class SwapSerializerCommand : Command - { - private readonly WorldService worldService; - private readonly IOptions options; - - public SwapSerializerCommand(IOptions options, WorldService worldService) : base("swapserializer", Perms.HOST, "Allows to change the save format") - { - AddParameter(new TypeEnum("serializer", true, "Save format to change to")); - - this.worldService = worldService; - this.options = options; - } - - protected override void Execute(CallArgs args) - { - ServerSerializerMode serializerMode = args.Get(0); - - if (serializerMode != options.Value.SerializerMode) - { - options.Value.SerializerMode = serializerMode; - worldService.UpdateSerializer(serializerMode); - SendMessage(args.Sender, $"Server save format swapped to {options.Value.SerializerMode}"); - } - else - { - SendMessage(args.Sender, "Server is already using this save format"); - } - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Commands/TeleportCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/TeleportCommand.cs index e3093fb5c0..5b90bc00e4 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/TeleportCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/TeleportCommand.cs @@ -1,29 +1,29 @@ -using System.Collections.Generic; +using System.ComponentModel; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[Alias("tp")] +[RequiresOrigin(CommandOrigin.PLAYER)] +[RequiresPermission(Perms.MODERATOR)] +internal sealed class TeleportCommand(IPacketSender packetSender) : ICommandHandler { - internal class TeleportCommand : Command - { - public override IEnumerable Aliases { get; } = new[] { "tp" }; + private readonly IPacketSender packetSender = packetSender; - public TeleportCommand() : base("teleport", Perms.MODERATOR, PermsFlag.NO_CONSOLE, "Teleports you on a specific location") + [Description("Teleports you on a specific location")] + public async Task Execute(ICommandContext context, [Description("x coordinate")] int x, [Description("y coordinate")] int y, [Description("z coordinate")] int z) + { + if (context is not PlayerToServerCommandContext playerContext) { - AddParameter(new TypeInt("x", true, "x coordinate")); - AddParameter(new TypeInt("y", true, "y coordinate")); - AddParameter(new TypeInt("z", true, "z coordinate")); + return; } - protected override void Execute(CallArgs args) - { - NitroxVector3 position = new(args.Get(0), args.Get(1), args.Get(2)); - args.Sender.Value.Teleport(position, Optional.Empty); - - SendMessage(args.Sender, $"Teleported to {position}"); - } + NitroxVector3 position = new(x, y, z); + playerContext.Player.Teleport(position, Optional.Empty, packetSender); + await playerContext.ReplyAsync($"Teleported to {position}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/TimeCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/TimeCommand.cs index 82333b83e7..de0b2a6584 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/TimeCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/TimeCommand.cs @@ -1,40 +1,31 @@ +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; namespace Nitrox.Server.Subnautica.Models.Commands; -internal sealed class TimeCommand : Command +[RequiresPermission(Perms.MODERATOR)] +internal sealed class TimeCommand(TimeService timeService) : ICommandHandler { - private readonly TimeService timeService; + private readonly TimeService timeService = timeService; - public TimeCommand(TimeService timeService) : base("time", Perms.MODERATOR, "Changes the map time") + [Description("Changes the map time")] + public async Task Execute(ICommandContext context, [Description("Changes the map time")] StoryManager.TimeModification time) { - AddParameter(new TypeString("day/night", false, "Time to change to")); - - this.timeService = timeService; - } - - protected override void Execute(CallArgs args) - { - string time = args.Get(0); - - switch (time?.ToLower()) + switch (time) { - case "day": + case StoryManager.TimeModification.DAY: timeService.ChangeTime(StoryManager.TimeModification.DAY); - SendMessageToAllPlayers("Time set to day"); + await context.SendToAllAsync("Time set to day"); break; - - case "night": + case StoryManager.TimeModification.NIGHT: timeService.ChangeTime(StoryManager.TimeModification.NIGHT); - SendMessageToAllPlayers("Time set to night"); + await context.SendToAllAsync("Time set to night"); break; - default: timeService.ChangeTime(StoryManager.TimeModification.SKIP); - SendMessageToAllPlayers("Skipped time"); + await context.SendToAllAsync("Skipped time"); break; } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/UnmuteCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/UnmuteCommand.cs index 54ee152caa..e19e17a2de 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/UnmuteCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/UnmuteCommand.cs @@ -1,47 +1,35 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; -using Nitrox.Server.Subnautica.Models.GameLogic; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.MODERATOR)] +internal sealed class UnmuteCommand : ICommandHandler { - internal class UnmuteCommand : Command + [Description("Removes a mute from a player")] + public async Task Execute(ICommandContext context, [Description("Player to unmute")] Player targetPlayer) { - private readonly PlayerManager playerManager; - - public UnmuteCommand(PlayerManager playerManager) : base("unmute", Perms.MODERATOR, "Removes a mute from a player") + if (context.OriginId == targetPlayer.SessionId) { - this.playerManager = playerManager; - AddParameter(new TypePlayer("name", true, "Player to unmute")); + await context.ReplyAsync("You can't unmute yourself"); + return; } - - protected override void Execute(CallArgs args) + if (targetPlayer.Permissions >= context.Permissions) { - Player targetPlayer = args.Get(0); - - if (args.SenderName == targetPlayer.Name) - { - SendMessage(args.Sender, "You can't unmute yourself"); - return; - } - - if (!args.IsConsole && targetPlayer.Permissions >= args.Sender.Value.Permissions) - { - SendMessage(args.Sender, $"You're not allowed to unmute {targetPlayer.Name}"); - return; - } - - if (!targetPlayer.PlayerContext.IsMuted) - { - SendMessage(args.Sender, $"{targetPlayer.Name} is already unmuted"); - args.Sender.Value.SendPacket(new MutePlayer(targetPlayer.Id, targetPlayer.PlayerContext.IsMuted)); - return; - } - - targetPlayer.PlayerContext.IsMuted = false; - playerManager.SendPacketToAllPlayers(new MutePlayer(targetPlayer.Id, targetPlayer.PlayerContext.IsMuted)); - SendMessage(targetPlayer, "You're no longer muted"); - SendMessage(args.Sender, $"Unmuted {targetPlayer.Name}"); + await context.ReplyAsync($"You're not allowed to unmute {targetPlayer.Name}"); + return; } + if (!targetPlayer.PlayerContext.IsMuted) + { + await context.ReplyAsync($"{targetPlayer.Name} is already unmuted"); + await context.ReplyAsync(new MutePlayer(targetPlayer.SessionId, false)); + return; + } + + targetPlayer.PlayerContext.IsMuted = false; + await context.SendToAllAsync(new MutePlayer(targetPlayer.SessionId, targetPlayer.PlayerContext.IsMuted)); + await context.SendAsync(targetPlayer.SessionId, "You're no longer muted"); + await context.ReplyAsync($"Unmuted {targetPlayer.Name}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/WarpCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/WarpCommand.cs index e0fb227a6e..f34f9ee76b 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/WarpCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/WarpCommand.cs @@ -1,37 +1,32 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using System.ComponentModel; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.MODERATOR)] +internal sealed class WarpCommand(IPacketSender packetSender) : ICommandHandler, ICommandHandler { - internal class WarpCommand : Command + private readonly IPacketSender packetSender = packetSender; + + [RequiresOrigin(CommandOrigin.PLAYER)] + [Description("Teleports you to the target player")] + public async Task Execute(ICommandContext context, [Description("Player to teleport to")] Player targetPlayer) { - public WarpCommand() : base("warp", Perms.MODERATOR, "Allows to teleport players") + if (context is not PlayerToServerCommandContext playerContext) { - AddParameter(new TypePlayer("name", true, "Player to teleport to (or a player specified to teleport)")); - AddParameter(new TypePlayer("name", false, "The players name to teleport to")); + return; } - protected override void Execute(CallArgs args) - { - Player destination; - Player sender; - - //Allows the console to teleport two players - if (args.IsValid(1)) - { - destination = args.Get(1); - sender = args.Get(0); - } - else - { - Validate.IsFalse(args.IsConsole, "This command can't be used by CONSOLE"); - destination = args.Get(0); - sender = args.Sender.Value; - } + playerContext.Player.Teleport(targetPlayer.Position, targetPlayer.SubRootId, packetSender); + await context.ReplyAsync($"Teleported to {targetPlayer.Name}"); + } - sender.Teleport(destination.Position, destination.SubRootId); - SendMessage(sender, $"Teleported to {destination.Name}"); - } + [Description("Teleports first player to the second player")] + public async Task Execute(ICommandContext context, Player warpingPlayer, Player targetPlayer) + { + warpingPlayer.Teleport(targetPlayer.Position, targetPlayer.SubRootId, packetSender); + await context.ReplyAsync($"Teleported to {targetPlayer.Name}"); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/WhisperCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/WhisperCommand.cs index a17e433b3b..9cab120b50 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/WhisperCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/WhisperCommand.cs @@ -1,28 +1,16 @@ -using System.Collections.Generic; +using System.ComponentModel; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[Alias("w", "msg", "m")] +[RequiresPermission(Perms.PLAYER)] +internal sealed class WhisperCommand : ICommandHandler { - internal class WhisperCommand : Command + [Description("Sends a private message to a player")] + public async Task Execute(ICommandContext context, [Description("The players name to message")] Player targetPlayer, [Description("The message to send")] string message) { - public override IEnumerable Aliases { get; } = new[] { "w", "msg", "m" }; - - public WhisperCommand() : base("whisper", Perms.PLAYER, "Sends a private message to a player") - { - AddParameter(new TypePlayer("name", true, "The players name to message")); - AddParameter(new TypeString("msg", true, "The message to send")); - - AllowedArgOverflow = true; - } - - protected override void Execute(CallArgs args) - { - Player foundPlayer = args.Get(0); - string message = $"[{args.SenderName} -> YOU]: {args.GetTillEnd(1)}"; - - SendMessageToPlayer(foundPlayer, message); - } + await context.SendAsync(targetPlayer.SessionId, new ChatMessage(context.OriginId, $"[{context.OriginName} -> YOU]: {message}")); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/WhoisCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/WhoisCommand.cs index c06868be64..26c18c7d61 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/WhoisCommand.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/WhoisCommand.cs @@ -1,31 +1,25 @@ -using System.Text; +using System.ComponentModel; +using System.Text; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; -using Nitrox.Server.Subnautica.Models.Commands.Abstract.Type; +using Nitrox.Server.Subnautica.Models.Commands.Core; -namespace Nitrox.Server.Subnautica.Models.Commands +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.PLAYER)] +internal sealed class WhoisCommand : ICommandHandler { - internal class WhoisCommand : Command + [Description("Shows informations over a player")] + public async Task Execute(ICommandContext context, Player targetPlayer) { - public WhoisCommand() : base("whois", Perms.PLAYER, "Shows informations over a player") - { - AddParameter(new TypePlayer("name", true, "The players name")); - } - - protected override void Execute(CallArgs args) - { - Player player = args.Get(0); - - StringBuilder builder = new($"==== {player.Name} ====\n"); - builder.AppendLine($"ID: {player.Id}"); - builder.AppendLine($"Role: {player.Permissions}"); - builder.AppendLine($"Position: {player.Position.X}, {player.Position.Y}, {player.Position.Z}"); - builder.AppendLine($"Oxygen: {player.Stats.Oxygen}/{player.Stats.MaxOxygen}"); - builder.AppendLine($"Food: {player.Stats.Food}"); - builder.AppendLine($"Water: {player.Stats.Water}"); - builder.AppendLine($"Infection: {player.Stats.InfectionAmount}"); + StringBuilder builder = new($"==== {targetPlayer.Name} ====\n"); + builder.AppendLine($"SessionId: {targetPlayer.SessionId}"); + builder.AppendLine($"Role: {targetPlayer.Permissions}"); + builder.AppendLine($"Position: {targetPlayer.Position.X}, {targetPlayer.Position.Y}, {targetPlayer.Position.Z}"); + builder.AppendLine($"Oxygen: {targetPlayer.Stats.Oxygen}/{targetPlayer.Stats.MaxOxygen}"); + builder.AppendLine($"Food: {targetPlayer.Stats.Food}"); + builder.AppendLine($"Water: {targetPlayer.Stats.Water}"); + builder.AppendLine($"Infection: {targetPlayer.Stats.InfectionAmount}"); - SendMessage(args.Sender, builder.ToString()); - } + await context.ReplyAsync(builder.ToString()); } } diff --git a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibConnection.cs b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibConnection.cs deleted file mode 100644 index 55bd127247..0000000000 --- a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibConnection.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Net; -using LiteNetLib; -using LiteNetLib.Utils; -using Nitrox.Model.Networking; - -namespace Nitrox.Server.Subnautica.Models.Communication; - -public class LiteNetLibConnection : INitroxConnection, IEquatable -{ - private readonly NetDataWriter dataWriter = new(); - private readonly NetPeer peer; - private readonly ILogger logger; - - public IPEndPoint Endpoint => peer; - public NitroxConnectionState State => peer.ConnectionState.ToNitrox(); - - public LiteNetLibConnection(NetPeer peer, ILogger logger) - { - this.peer = peer; - this.logger = logger; - } - - public void SendPacket(Packet packet) - { - if (peer.ConnectionState == ConnectionState.Connected) - { - byte[] packetData = packet.Serialize(); - dataWriter.Reset(); - dataWriter.Put(packetData.Length); - dataWriter.Put(packetData); - - peer.Send(dataWriter, (byte)packet.UdpChannel, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); - } - else - { - logger.ZLogWarning($"Cannot send packet {packet?.GetType()} to a closed connection {peer as IPEndPoint}"); - } - } - - public static bool operator ==(LiteNetLibConnection left, LiteNetLibConnection right) - { - return Equals(left, right); - } - - public static bool operator !=(LiteNetLibConnection left, LiteNetLibConnection right) - { - return !Equals(left, right); - } - - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((LiteNetLibConnection)obj); - } - - public override int GetHashCode() - { - return peer?.Id.GetHashCode() ?? 0; - } - - public bool Equals(LiteNetLibConnection other) - { - return peer?.Id == other?.peer?.Id; - } -} diff --git a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs index d56ba2a925..c55ebadb52 100644 --- a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs +++ b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs @@ -1,66 +1,56 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Threading.Channels; using LiteNetLib; using LiteNetLib.Layers; using LiteNetLib.Utils; using Nitrox.Model.Core; -using Nitrox.Model.DataStructures; +using Nitrox.Model.Networking; +using Nitrox.Model.Packets.Core; +using Nitrox.Server.Subnautica.Models.Administration; +using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets; +using Nitrox.Server.Subnautica.Models.Helper; +using Nitrox.Server.Subnautica.Models.Packets.Core; +using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.Communication; -internal sealed class LiteNetLibServer : IHostedService +internal sealed class LiteNetLibServer : IHostedService, IPacketSender, IKickPlayer, ISessionCleaner { - private readonly PacketHandler packetHandler; - private readonly PlayerManager playerManager; - private readonly JoiningManager joiningManager; - private readonly EntitySimulation entitySimulation; - private readonly SleepManager sleepManager; - private readonly IOptions options; - private readonly ILogger logger; - private readonly Dictionary connectionsByRemoteIdentifier = []; + private readonly Dictionary contextByPeerId = []; + private readonly Dictionary contextBySessionId = []; + private readonly Lock contextLock = new(); + private readonly NetDataWriter dataWriter = new(); private readonly EventBasedNetListener listener; + private readonly ILogger logger; + private readonly IOptions options; + private readonly PacketRegistryService packetRegistryService; + private readonly PacketSerializationService packetSerializationService; + private readonly PlayerManager playerManager; private readonly NetManager server; + private readonly SessionManager sessionManager; + private readonly Channel taskChannel = Channel.CreateUnbounded(); - static LiteNetLibServer() + public LiteNetLibServer(PlayerManager playerManager, SessionManager sessionManager, PacketSerializationService packetSerializationService, PacketRegistryService packetRegistryService, IOptions options, + ILogger logger) { - Packet.InitSerializer(); - } - - public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, JoiningManager joiningManager, EntitySimulation entitySimulation, SleepManager sleepManager, IOptions options, ILogger logger) - { - this.packetHandler = packetHandler; this.playerManager = playerManager; - this.joiningManager = joiningManager; - this.entitySimulation = entitySimulation; - this.sleepManager = sleepManager; + this.sessionManager = sessionManager; + this.packetSerializationService = packetSerializationService; + this.packetRegistryService = packetRegistryService; this.options = options; this.logger = logger; listener = new EventBasedNetListener(); - server = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) - { - UseNativeSockets = true, - IPv6Enabled = true - }; - } - - public void OnConnectionRequest(ConnectionRequest request) - { - if (server.ConnectedPeersCount < options.Value.MaxConnections) - { - request.AcceptIfKey("nitrox"); - } - else - { - request.Reject(); - } + server = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) { + UseNativeSockets = true, IPv6Enabled = true }; } public Task StartAsync(CancellationToken cancellationToken) { - listener.PeerConnectedEvent += PeerConnected; listener.PeerDisconnectedEvent += PeerDisconnected; listener.NetworkReceiveEvent += NetworkDataReceived; listener.ConnectionRequestEvent += OnConnectionRequest; @@ -85,65 +75,161 @@ public async Task StopAsync(CancellationToken cancellationToken) return; } - playerManager.SendPacketToAllPlayers(new ServerStopped()); - // We want every player to receive this packet - await Task.Delay(500, cancellationToken); - server.Stop(); + await SendPacketToAllAsync(new ServerStopped()); + try + { + await Task.Delay(100, CancellationToken.None); // Gives some time for the last few tasks to be queued up. + taskChannel.Writer.TryComplete(); + await foreach (Task task in taskChannel.Reader.ReadAllAsync(cancellationToken)) + { + await task; + } + } + finally + { + server.Stop(); + } } - private void ClientDisconnected(INitroxConnection connection) + public ValueTask SendPacketAsync(T packet, SessionId sessionId) where T : Packet { - Player? player = playerManager.GetPlayer(connection); - if (player == null) + PeerContext? context; + lock (contextLock) { - joiningManager.JoiningPlayerDisconnected(connection); - return; + contextBySessionId.TryGetValue(sessionId, out context); } + if (context == null) + { + logger.ZLogWarning($"Unable to send packet {typeof(T)} because no context is set for session #{sessionId}"); + return ValueTask.CompletedTask; + } + SendPacket(packet, context.Peer); + return ValueTask.CompletedTask; + } - sleepManager.PlayerDisconnected(player); - playerManager.PlayerDisconnected(connection); - - Disconnect disconnect = new(player.Id); - playerManager.SendPacketToAllPlayers(disconnect); - - List ownershipChanges = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(player); - - if (ownershipChanges.Count > 0) + public ValueTask SendPacketToAllAsync(T packet) where T : Packet + { + PeerContext[] contexts = []; + int i = 0; + try + { + lock (contextLock) + { + int count = contextBySessionId.Count; + contexts = ArrayPool.Shared.Rent(count); + foreach (PeerContext? peerContext in contextBySessionId.Values) + { + contexts[i++] = peerContext; + } + } + for (int j = 0; j < i; j++) + { + SendPacket(packet, contexts[j].Peer); + } + } + finally { - SimulationOwnershipChange ownershipChange = new(ownershipChanges); - playerManager.SendPacketToAllPlayers(ownershipChange); + ArrayPool.Shared.Return(contexts); } + return ValueTask.CompletedTask; } - public void ProcessIncomingData(INitroxConnection connection, Packet packet) + public ValueTask SendPacketToOthersAsync(T packet, SessionId excludedSessionId) where T : Packet { + PeerContext[] contexts = []; + int i = 0; try { - packetHandler.Process(packet, connection); + lock (contextLock) + { + int count = contextBySessionId.Count; + contexts = ArrayPool.Shared.Rent(count); + foreach (PeerContext? peerContext in contextBySessionId.Values) + { + contexts[i++] = peerContext; + } + } + for (int j = 0; j < i; j++) + { + if (contexts[j].SessionId == excludedSessionId) + { + continue; + } + SendPacket(packet, contexts[j].Peer); + } } - catch (Exception ex) + finally + { + ArrayPool.Shared.Return(contexts); + } + return ValueTask.CompletedTask; + } + + public async Task KickPlayer(SessionId sessionId, string reason = "") + { + PeerContext context; + lock (contextLock) { - logger.ZLogError(ex, $"Exception while processing packet: {packet}"); + if (!contextBySessionId.TryGetValue(sessionId, out context)) + { + return false; + } } + await SendPacketAsync(new PlayerKicked(reason), sessionId); + await Task.Delay(100); // Give time for LiteNetLib to send the packet out before disconnecting. Otherwise, no kick modal will show on client. + server.DisconnectPeer(context.Peer); // This will trigger client disconnect, which will handle the session (data) migration. + return true; + } + + public async Task OnEventAsync(ISessionCleaner.Args args) + { + Disconnect disconnect = new(args.Session.Id); + await SendPacketToAllAsync(disconnect); } - private void PeerConnected(NetPeer peer) + private void OnConnectionRequest(ConnectionRequest request) { - LiteNetLibConnection connection = new(peer, logger); - lock (connectionsByRemoteIdentifier) + if (request.Data.GetString() != "nitrox") + { + request.Reject(); + return; + } + if (server.ConnectedPeersCount >= options.Value.MaxConnections) { - connectionsByRemoteIdentifier[peer.Id] = connection; + request.Reject(); + return; + } + + SessionManager.Session session = sessionManager.GetOrCreateSession(request.RemoteEndPoint); + NetPeer peer = request.Accept(); + PeerContext context = new(session.Id, peer); + lock (contextLock) + { + contextBySessionId.TryAdd(session.Id, context); + contextByPeerId.TryAdd(peer.Id, context); } } private void PeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { - INitroxConnection connection = GetConnection(peer.Id); - if (connection == null) + PeerContext? context; + lock (contextLock) { + if (contextByPeerId.Remove(peer.Id, out context) && context != null) + { + contextBySessionId.Remove(context.SessionId); + } + } + if (context == null) + { + logger.ZLogWarning($"Disconnected peer id {peer.Id} did not have an associated session id!"); return; } - ClientDisconnected(connection); + + if (!taskChannel.Writer.TryWrite(sessionManager.RemoveSessionAsync(context.SessionId))) + { + logger.ZLogWarning($"Failed to queue client disconnect task for {peer as EndPoint:@EndPoint}"); + } } private void NetworkDataReceived(NetPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) @@ -153,9 +239,25 @@ private void NetworkDataReceived(NetPeer peer, NetDataReader reader, byte channe try { reader.GetBytes(packetData, packetDataLength); - Packet packet = Packet.Deserialize(packetData); - INitroxConnection connection = GetConnection(peer.Id); - ProcessIncomingData(connection, packet); + Packet? packet = Packet.Deserialize(packetData); + if (packet == null) + { + return; + } + PeerContext context; + lock (contextLock) + { + contextByPeerId.TryGetValue(peer.Id, out context); + } + if (context == null) + { + return; + } + + if (!taskChannel.Writer.TryWrite(ProcessPacket(context, packet))) + { + logger.ZLogError($"Failed to queue packet processor task for packet type {packet.GetType().Name:@TypeName} from {peer.Address:@Address}:{peer.Port:@Port}"); + } } finally { @@ -163,14 +265,105 @@ private void NetworkDataReceived(NetPeer peer, NetDataReader reader, byte channe } } - private INitroxConnection? GetConnection(int remoteIdentifier) + private async Task ProcessPacket(PeerContext peerContext, Packet packet) + { + Type packetType = packet.GetType(); + PacketProcessorsInvoker.Entry processor = packetRegistryService.GetProcessor(packetType); + + try + { + switch (GetProcessorTarget(processor, peerContext.SessionId, playerManager, out Player? player)) + { + case ProcessorTarget.ANONYMOUS: + using (EasyPool.Lease lease = EasyPool.Rent()) + { + ref AnonProcessorContext context = ref lease.GetRef(); + if (context == null) + { + context = new AnonProcessorContext((peerContext.SessionId, peerContext.Peer), this); + } + else + { + context.Sender = (peerContext.SessionId, peerContext.Peer); + } + await processor.Execute(context, packet); + } + break; + case ProcessorTarget.AUTHENTICATED: + using (EasyPool.Lease lease = EasyPool.Rent()) + { + ref AuthProcessorContext context = ref lease.GetRef(); + if (context == null) + { + context = new AuthProcessorContext(player, this); + } + else + { + context.Sender = player; + } + await processor.Execute(context, packet); + } + break; + default: + logger.ZLogWarning($"Received invalid, unauthenticated packet: {packetType.Name:@TypeName}"); + break; + } + } + catch (Exception ex) + { + logger.ZLogError(ex, $"Error in packet processor {processor.GetType().Name:@TypeName}"); + } + + static ProcessorTarget GetProcessorTarget(PacketProcessorsInvoker.Entry? processor, SessionId sessionId, PlayerManager playerManager, [NotNullIfNotNull(nameof(player))] out Player? player) + { + player = null; + if (processor == null) + { + return ProcessorTarget.INVALID; + } + if (typeof(IAuthPacketProcessor).IsAssignableFrom(processor.InterfaceType) && sessionId is { IsPlayer: true } && playerManager.TryGetPlayerBySessionId(sessionId, out player)) + { + return ProcessorTarget.AUTHENTICATED; + } + if (typeof(IAnonPacketProcessor).IsAssignableFrom(processor.InterfaceType)) + { + return ProcessorTarget.ANONYMOUS; + } + return ProcessorTarget.INVALID; + } + } + + private void SendPacket(Packet packet, NetPeer peer) { - INitroxConnection connection; - lock (connectionsByRemoteIdentifier) + using EasyPool.Lease lease = EasyPool.Rent(); + ref MemoryStream stream = ref lease.GetRef(); + stream ??= new MemoryStream(ushort.MaxValue); + + int startPos = (int)stream.Position; + packetSerializationService.SerializeInto(packet, stream); + int bytesWritten = (int)(stream.Position - startPos); + Span packetData = stream.GetBuffer().AsSpan().Slice(startPos, bytesWritten); + + lock (dataWriter) { - connectionsByRemoteIdentifier.TryGetValue(remoteIdentifier, out connection); + dataWriter.Reset(); + dataWriter.Put(packetData.Length); + dataWriter.ResizeIfNeed(packetData.Length + 4); + packetData.CopyTo(dataWriter.Data.AsSpan().Slice(4)); + dataWriter.SetPosition(packetData.Length + 4); + peer.Send(dataWriter, (byte)packet.UdpChannel, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod)); } - return connection; + // Cleanup pooled data. + stream.Position = 0; } + + private enum ProcessorTarget + { + INVALID, + ANONYMOUS, + AUTHENTICATED + } + + private record PeerContext(SessionId SessionId, NetPeer Peer); } diff --git a/Nitrox.Server.Subnautica/Models/Communication/NitroxConnection.cs b/Nitrox.Server.Subnautica/Models/Communication/NitroxConnection.cs deleted file mode 100644 index 940062e605..0000000000 --- a/Nitrox.Server.Subnautica/Models/Communication/NitroxConnection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using Nitrox.Model.Packets.Processors.Abstract; - -namespace Nitrox.Server.Subnautica.Models.Communication; - -public interface INitroxConnection : IProcessorContext -{ - IPEndPoint Endpoint { get; } - - NitroxConnectionState State { get; } - - void SendPacket(Packet packet); -} diff --git a/Nitrox.Server.Subnautica/Models/Communication/SessionManager.cs b/Nitrox.Server.Subnautica/Models/Communication/SessionManager.cs new file mode 100644 index 0000000000..92753de96b --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Communication/SessionManager.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using Nitrox.Model.Core; +using Nitrox.Server.Subnautica.Models.AppEvents; + +namespace Nitrox.Server.Subnautica.Models.Communication; + +/// +/// Assigns session ids to player connections. Each session id is unique for the current server instance. +/// A session id will be reused when connections are lost. It will take at least 10 minutes before a session id is +/// reused to prevent impersonation and other bugs. +/// +internal sealed class SessionManager(ISessionCleaner.Trigger sessionCleanTrigger, ILogger logger) +{ + private readonly ILogger logger = logger; + private readonly Queue<(TimeSpan ReturnedTimeStamp, SessionId Id)> returnedSessionIds = []; + private readonly ISessionCleaner.Trigger sessionCleanTrigger = sessionCleanTrigger; + private readonly Dictionary sessionIdByEndpoint = []; + private readonly Lock sessionLock = new(); + private readonly Dictionary sessions = []; + private readonly Stopwatch time = Stopwatch.StartNew(); + + /// + /// The next session id. Returns a prior id if are available. + /// + private SessionId NextSessionId + { + get + { + SessionId id = 0; + lock (sessionLock) + { + if (returnedSessionIds.Count > 0 && returnedSessionIds.TryPeek(out (TimeSpan ReturnedTimeStamp, SessionId Id) entry) && (time.Elapsed - entry.ReturnedTimeStamp).TotalMinutes >= SessionId.DELAY_REUSE_MINUTES) + { + id = returnedSessionIds.Dequeue().Id; + } + if (id == 0) + { + id = ++field; + } + } + Debug.Assert(id > 0); + return id; + } + } + + public Session GetOrCreateSession(IPEndPoint endPoint) + { + Session session; + EndpointKey key = ToKey(endPoint); + bool created = false; + lock (sessionLock) + { + if (!sessionIdByEndpoint.TryGetValue(key, out SessionId id)) + { + id = NextSessionId; + if (sessionIdByEndpoint.TryAdd(key, id)) + { + session = new Session(id, endPoint); + sessions.Add(id, session); + created = true; + } + } + + session = sessions[id]; + } + if (created) + { + logger.ZLogInformation($"Created session #{session.Id} for connection {endPoint}"); + } + return session; + } + + public IPEndPoint? GetEndPoint(SessionId sessionId) + { + lock (sessionLock) + { + sessions.TryGetValue(sessionId, out Session session); + return session?.EndPoint; + } + } + + public bool IsConnected(SessionId sessionId) + { + lock (sessionLock) + { + return sessions.ContainsKey(sessionId); + } + } + + public async Task RemoveSessionAsync(SessionId sessionId) + { + Session session; + int sessionCountAfter; + lock (sessionLock) + { + if (!sessions.Remove(sessionId, out session)) + { + return false; + } + sessionCountAfter = sessions.Count; + sessionIdByEndpoint.Remove(ToKey(session.EndPoint)); + returnedSessionIds.Enqueue((time.Elapsed, session.Id)); + } + logger.ZLogTrace($"Removing session #{sessionId}"); + await sessionCleanTrigger.InvokeAsync(new ISessionCleaner.Args(session, sessionCountAfter)); + logger.ZLogTrace($"Removed session #{sessionId}"); + return true; + } + + public int GetSessionCount() + { + lock (sessionLock) + { + return sessions.Count; + } + } + + private EndpointKey ToKey(IPEndPoint endPoint) => new(endPoint.Address, (ushort)endPoint.Port); + + public record Session(SessionId Id, IPEndPoint EndPoint); + + private record EndpointKey(IPAddress Address, ushort Port); +} diff --git a/Nitrox.Server.Subnautica/Models/Factories/RandomFactory.cs b/Nitrox.Server.Subnautica/Models/Factories/RandomFactory.cs new file mode 100644 index 0000000000..fe57b205a5 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Factories/RandomFactory.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; +using Nitrox.Server.Subnautica.Models.Helper; + +namespace Nitrox.Server.Subnautica.Models.Factories; + +internal sealed class RandomFactory(IOptions options) +{ + private readonly IOptions options = options; + + public static int CreateSeedInt32(string worldSeed, string csFilePath, int seedId = 0) => $"{worldSeed}{csFilePath}:{seedId}".ToMd5HashedInt32(); + + public static string GetCsFilePathFromType(Type type) + { + string assemblyName = type.Assembly.GetName().Name ?? throw new Exception($"Failed to get assembly from type {type}"); + string nameSpaceStr = type.Namespace ?? throw new Exception($"Namespace for {type} is unknown"); + Span nameSpace = stackalloc char[nameSpaceStr.Length]; + nameSpaceStr.CopyTo(nameSpace); + nameSpace = nameSpace.Slice(assemblyName.Length + 1); + nameSpace.Replace('.', '/'); + return $"{assemblyName}/{nameSpace}/{type.Name}.cs"; + } + + /// Sets the unique id for this seed based on the calling .NET code file. + /// File path to the calling .NET code file. + public Random GetDotnetRandom(int seedId = 0, [CallerFilePath] string filePath = "") => new(CreateSeedInt32(options.Value.Seed, filePath, seedId)); + + /// + public XorRandom GetUnityLikeRandom(int seedId = 0, [CallerFilePath] string filePath = "") => new(CreateSeedInt32(options.Value.Seed, filePath, seedId)); +} diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Bases/BuildingManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Bases/BuildingManager.cs index 5b5640bb1f..30ef2eb079 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Bases/BuildingManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Bases/BuildingManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -6,18 +7,21 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic.Bases; internal sealed class BuildingManager { + private readonly IPacketSender packetSender; private readonly EntityRegistry entityRegistry; private readonly WorldEntityManager worldEntityManager; private readonly IOptions options; private readonly ILogger logger; - public BuildingManager(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, IOptions options, ILogger logger) + public BuildingManager(IPacketSender packetSender, EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, IOptions options, ILogger logger) { + this.packetSender = packetSender; this.entityRegistry = entityRegistry; this.worldEntityManager = worldEntityManager; this.options = options; @@ -277,7 +281,7 @@ public bool ReplaceBaseByGhost(BaseDeconstructed baseDeconstructed) return true; } - public bool ReplacePieceByGhost(Player player, PieceDeconstructed pieceDeconstructed, out Entity? removedEntity, out int operationId) + public bool ReplacePieceByGhost(Player player, PieceDeconstructed pieceDeconstructed, [NotNullWhen(true)] out Entity? removedEntity, out int operationId) { if (!entityRegistry.TryGetEntityById(pieceDeconstructed.BaseId, out BuildEntity buildEntity)) { @@ -391,7 +395,7 @@ public bool SeparateChildrenToWaterParks(LargeWaterParkDeconstructed packet) private void NotifyPlayerDesync(Player player) { Dictionary operations = GetEntitiesOperations(worldEntityManager.GetGlobalRootEntities(true)); - player.SendPacket(new BuildingDesyncWarning(operations)); + packetSender.SendPacketAsync(new BuildingDesyncWarning(operations), player.SessionId); } public static Dictionary GetEntitiesOperations(List entities) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntityRegistry.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntityRegistry.cs index b202e65075..6ab272a57a 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntityRegistry.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntityRegistry.cs @@ -146,8 +146,13 @@ public void AddToParent(Entity entity) } } - public void RemoveFromParent(Entity entity) + public void RemoveFromParent(Entity? entity) { + if (entity == null) + { + return; + } + if (entity.ParentId != null && TryGetEntityById(entity.ParentId, out Entity parentEntity)) { parentEntity.ChildEntities.RemoveAll(childEntity => childEntity.Id.Equals(entity.Id)); @@ -171,7 +176,7 @@ public void CleanChildren(Entity entity) } } - public void ReparentEntity(NitroxId entityId, NitroxId newParentId) + public void ReparentEntity(NitroxId? entityId, NitroxId? newParentId) { if (entityId == null || !TryGetEntityById(entityId, out Entity entity)) { @@ -191,13 +196,13 @@ public void ReparentEntity(NitroxId entityId, Entity newParent) ReparentEntity(entity, newParent); } - public void ReparentEntity(Entity entity, NitroxId newParentId) + public void ReparentEntity(Entity? entity, NitroxId? newParentId) { Entity parentEntity = newParentId != null ? GetEntityById(newParentId).Value : null; ReparentEntity(entity, parentEntity); } - public void ReparentEntity(Entity entity, Entity newParent) + public void ReparentEntity(Entity? entity, Entity? newParent) { RemoveFromParent(entity); if (newParent == null) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntitySimulation.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntitySimulation.cs index d7d2c9465f..659e7b1e52 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntitySimulation.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/EntitySimulation.cs @@ -1,23 +1,29 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.AppEvents; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; -sealed class EntitySimulation +internal sealed class EntitySimulation : ISessionCleaner { private const SimulationLockType DEFAULT_ENTITY_SIMULATION_LOCKTYPE = SimulationLockType.TRANSIENT; - private readonly EntityRegistry entityRegistry; - private readonly WorldEntityManager worldEntityManager; - private readonly PlayerManager playerManager; private readonly ILogger logger; + + private readonly IPacketSender packetSender; + private readonly PlayerManager playerManager; private readonly SimulationOwnershipData simulationOwnershipData; + private readonly WorldEntityManager worldEntityManager; - public EntitySimulation(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, ILogger logger) + public EntitySimulation(IPacketSender packetSender, EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, ILogger logger) { + this.packetSender = packetSender; this.entityRegistry = entityRegistry; this.worldEntityManager = worldEntityManager; this.simulationOwnershipData = simulationOwnershipData; @@ -35,7 +41,7 @@ public List GetSimulationChangesForCell(Player player, Absolute foreach (WorldEntity entity in addedEntities) { bool doesEntityMove = ShouldSimulateEntityMovement(entity); - ownershipChanges.Add(new SimulatedEntity(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)); + ownershipChanges.Add(new SimulatedEntity(entity.Id, player.SessionId, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)); } return ownershipChanges; @@ -45,7 +51,7 @@ public void FillWithRemovedCells(Player player, AbsoluteEntityCell removedCell, { List entities = worldEntityManager.GetEntities(removedCell); IEnumerable revokedEntities = entities.Where(entity => !player.CanSee(entity) && simulationOwnershipData.RevokeIfOwner(entity.Id, player)); - AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); + AssignEntitiesToOtherPlayers(player.SessionId, revokedEntities, ownershipChanges); } public void BroadcastSimulationChanges(List ownershipChanges) @@ -53,28 +59,16 @@ public void BroadcastSimulationChanges(List ownershipChanges) if (ownershipChanges.Count > 0) { SimulationOwnershipChange ownershipChange = new(ownershipChanges); - playerManager.SendPacketToAllPlayers(ownershipChange); + packetSender.SendPacketToAllAsync(ownershipChange); } } - public List CalculateSimulationChangesFromPlayerDisconnect(Player player) - { - List ownershipChanges = new(); - - List revokedEntityIds = simulationOwnershipData.RevokeAllForOwner(player); - List revokedEntities = entityRegistry.GetEntities(revokedEntityIds); - - AssignEntitiesToOtherPlayers(player, revokedEntities, ownershipChanges); - - return ownershipChanges; - } - - public bool TryAssignEntityToPlayer(Entity entity, Player player, bool shouldEntityMove, out SimulatedEntity simulatedEntity) + public bool TryAssignEntityToPlayer(Entity entity, Player player, bool shouldEntityMove, [NotNullWhen(true)] out SimulatedEntity? simulatedEntity) { if (simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)) { bool doesEntityMove = shouldEntityMove && entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity); - simulatedEntity = new(entity.Id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); + simulatedEntity = new(entity.Id, player.SessionId, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); return true; } @@ -93,29 +87,16 @@ public List AssignGlobalRootEntitiesAndGetData(Player player) continue; } bool doesEntityMove = ShouldSimulateEntityMovement(entity); - SimulatedEntity simulatedEntity = new(entity.Id, playerLock.Player.Id, doesEntityMove, playerLock.LockType); + SimulatedEntity simulatedEntity = new(entity.Id, playerLock.Player.SessionId, doesEntityMove, playerLock.LockType); simulatedEntities.Add(simulatedEntity); } return simulatedEntities; } - private void AssignEntitiesToOtherPlayers(Player oldPlayer, IEnumerable entities, List ownershipChanges) - { - List otherPlayers = playerManager.GetConnectedPlayersExcept(oldPlayer); - - foreach (Entity entity in entities) - { - if (TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity)) - { - ownershipChanges.Add(simulatedEntity); - } - } - } - - public bool TryAssignEntityToPlayers(List players, Entity entity, out SimulatedEntity simulatedEntity) + public bool TryAssignEntityToPlayers(List players, Entity entity, [NotNullWhen(true)] out SimulatedEntity? simulatedEntity) { NitroxId id = entity.Id; - + foreach (Player player in players) { if (player.CanSee(entity) && simulationOwnershipData.TryToAcquire(id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE)) @@ -123,23 +104,15 @@ public bool TryAssignEntityToPlayers(List players, Entity entity, out Si bool doesEntityMove = entity is WorldEntity worldEntity && ShouldSimulateEntityMovement(worldEntity); logger.ZLogTrace($"Player {player.Name} has taken over simulating {id}"); - simulatedEntity = new(id, player.Id, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); + simulatedEntity = new(id, player.SessionId, doesEntityMove, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); return true; } } - + simulatedEntity = null; return false; } - private List FilterSimulatableEntities(Player player, List entities) - { - return entities.Where(entity => { - bool isEligibleForSimulation = player.CanSee(entity) && ShouldSimulateEntity(entity); - return isEligibleForSimulation && simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); - }).ToList(); - } - public bool ShouldSimulateEntity(WorldEntity entity) { return SimulationWhitelist.UtilityWhitelist.Contains(entity.TechType) || ShouldSimulateEntityMovement(entity); @@ -159,4 +132,47 @@ public void EntityDestroyed(NitroxId id) { simulationOwnershipData.RevokeOwnerOfId(id); } + + public async Task OnEventAsync(ISessionCleaner.Args args) + { + List ownershipChanges = CalculateSimulationChangesFromPlayerDisconnect(args.Session.Id); + if (ownershipChanges.Count > 0) + { + SimulationOwnershipChange ownershipChange = new(ownershipChanges); + await packetSender.SendPacketToAllAsync(ownershipChange); + } + } + + private List CalculateSimulationChangesFromPlayerDisconnect(SessionId sessionId) + { + List ownershipChanges = new(); + + List revokedEntityIds = simulationOwnershipData.RevokeAllForOwner(sessionId); + List revokedEntities = entityRegistry.GetEntities(revokedEntityIds); + + AssignEntitiesToOtherPlayers(sessionId, revokedEntities, ownershipChanges); + + return ownershipChanges; + } + + private void AssignEntitiesToOtherPlayers(SessionId oldSessionId, IEnumerable entities, List ownershipChanges) + { + List otherPlayers = playerManager.GetConnectedPlayersExcept(oldSessionId); + foreach (Entity entity in entities) + { + if (TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity)) + { + ownershipChanges.Add(simulatedEntity); + } + } + } + + private List FilterSimulatableEntities(Player player, List entities) + { + return entities.Where(entity => + { + bool isEligibleForSimulation = player.CanSee(entity) && ShouldSimulateEntity(entity); + return isEligibleForSimulation && simulationOwnershipData.TryToAcquire(entity.Id, player, DEFAULT_ENTITY_SIMULATION_LOCKTYPE); + }).ToList(); + } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs index 5a46fa7efa..9031fbbafa 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs @@ -4,6 +4,7 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Factories; using Nitrox.Server.Subnautica.Models.Helper; using Nitrox.Server.Subnautica.Models.Resources; using Nitrox.Server.Subnautica.Models.Resources.Core; @@ -12,19 +13,29 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -sealed class BatchEntitySpawner : IEntitySpawner +internal sealed class BatchEntitySpawner( + RandomFactory randomFactory, + SubnauticaUweWorldEntityFactory worldEntityFactory, + IUwePrefabFactory prefabFactory, + IEntityBootstrapperManager entityBootstrapperManager, + PdaManager pdaManager, + PrefabPlaceholderGroupsResource prefabPlaceholderGroupsResource, + BatchCellsParser batchCellsParser, + IOptions options, + ILogger logger) { - private readonly BatchCellsParser batchCellsParser; + private readonly BatchCellsParser batchCellsParser = batchCellsParser; private readonly HashSet emptyBatches = []; - private readonly PrefabPlaceholderGroupsResource prefabPlaceholderGroupsResource; - private readonly IOptions options; - private readonly ILogger logger; - private readonly IUwePrefabFactory prefabFactory; - private readonly IEntityBootstrapperManager entityBootstrapperManager; - private readonly PdaManager pdaManager; + private readonly PrefabPlaceholderGroupsResource prefabPlaceholderGroupsResource = prefabPlaceholderGroupsResource; + private readonly IOptions options = options; + private readonly ILogger logger = logger; + private readonly IUwePrefabFactory prefabFactory = prefabFactory; + private readonly IEntityBootstrapperManager entityBootstrapperManager = entityBootstrapperManager; + private readonly PdaManager pdaManager = pdaManager; - private readonly IUweWorldEntityFactory worldEntityFactory; + private readonly XorRandom random = randomFactory.GetUnityLikeRandom(); + private readonly SubnauticaUweWorldEntityFactory worldEntityFactory = worldEntityFactory; private readonly Lock parsedBatchesLock = new(); private readonly Lock emptyBatchesLock = new(); @@ -60,26 +71,6 @@ public List SerializableParsedBatches private static readonly NitroxQuaternion prefabZUpRotation = NitroxQuaternion.FromEuler(new(-90f, 0f, 0f)); - public BatchEntitySpawner( - IUweWorldEntityFactory worldEntityFactory, - IUwePrefabFactory prefabFactory, - IEntityBootstrapperManager entityBootstrapperManager, - PdaManager pdaManager, - PrefabPlaceholderGroupsResource prefabPlaceholderGroupsResource, - BatchCellsParser batchCellsParser, - IOptions options, - ILogger logger) - { - this.worldEntityFactory = worldEntityFactory; - this.prefabFactory = prefabFactory; - this.entityBootstrapperManager = entityBootstrapperManager; - this.pdaManager = pdaManager; - this.prefabPlaceholderGroupsResource = prefabPlaceholderGroupsResource; - this.batchCellsParser = batchCellsParser; - this.options = options; - this.logger = logger; - } - public bool IsBatchSpawned(NitroxInt3 batchId) { lock (parsedBatches) @@ -88,7 +79,7 @@ public bool IsBatchSpawned(NitroxInt3 batchId) } } - public List LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCreation = false) + public async Task> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool fullCacheCreation = false) { lock (parsedBatches) { @@ -98,9 +89,9 @@ public List LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCrea } } - DeterministicGenerator deterministicBatchGenerator = new(options.Value.Seed, batchId); + DeterministicGenerator deterministicBatchGenerator = new(options.Value.Seed, batchId.ToString()); List spawnPoints = batchCellsParser.ParseBatchData(batchId); - List entities = SpawnEntities(spawnPoints, deterministicBatchGenerator); + List entities = await SpawnEntitiesAsync(spawnPoints, deterministicBatchGenerator); if (entities.Count == 0) { @@ -128,11 +119,11 @@ public List LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCrea return entities; } - /// - private List SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entitySpawnPoint, List prefabs, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null) + /// + private async Task> SpawnEntitiesUsingRandomDistributionAsync(EntitySpawnPoint entitySpawnPoint, List prefabs, DeterministicGenerator deterministicBatchGenerator, Entity? parentEntity = null) { // See CSVEntitySpawner.GetPrefabForSlot for reference - List allowedPrefabs = FilterAllowedPrefabs(prefabs, entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability); + (List? allowedPrefabs, float fragmentProbability, float completeFragmentProbability) = await FilterAllowedPrefabsAsync(prefabs, entitySpawnPoint); bool areFragmentProbabilitiesNonNull = fragmentProbability > 0f && completeFragmentProbability > 0f; float probabilityMultiplier = areFragmentProbabilitiesNonNull ? (completeFragmentProbability + fragmentProbability) / fragmentProbability : 1f; @@ -151,7 +142,7 @@ private List SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entit UwePrefab chosenPrefab = default; if (weightedFragmentProbability > 0f) { - float probabilityThreshold = XorRandom.NextFloat(); + float probabilityThreshold = random.NextFloat(); if (weightedFragmentProbability > 1f) { probabilityThreshold *= weightedFragmentProbability; @@ -174,12 +165,12 @@ private List SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entit } List spawnedEntities = []; - if (worldEntityFactory.TryFind(chosenPrefab.ClassId, out UweWorldEntity uweWorldEntity)) + if (await worldEntityFactory.FindAsync(chosenPrefab.ClassId) is { } uweWorldEntity) { for (int i = 0; i < chosenPrefab.Count; i++) { // Random position in sphere is only possible after first spawn, see EntitySlot.Spawn - List entities = [.. CreateEntityWithChildren(entitySpawnPoint, + List entities = [.. await CreateEntityWithChildrenAsync(entitySpawnPoint, chosenPrefab.ClassId, uweWorldEntity.TechType, uweWorldEntity.PrefabZUp, @@ -203,17 +194,17 @@ private List SpawnEntitiesUsingRandomDistribution(EntitySpawnPoint entit return spawnedEntities; } - private List FilterAllowedPrefabs(List prefabs, EntitySpawnPoint entitySpawnPoint, out float fragmentProbability, out float completeFragmentProbability) + private async Task<(List allowedPrefabs, float fragmentProbability, float completeFragmentProbability)> FilterAllowedPrefabsAsync(List prefabs, EntitySpawnPoint entitySpawnPoint) { List allowedPrefabs = []; - fragmentProbability = 0; - completeFragmentProbability = 0; + float fragmentProbability = 0; + float completeFragmentProbability = 0; for (int i = 0; i < prefabs.Count; i++) { UwePrefab prefab = prefabs[i]; // Adapted code from the while loop in CSVEntitySpawner.GetPrefabForSlot - if (prefab.ClassId != "None" && worldEntityFactory.TryFind(prefab.ClassId, out UweWorldEntity uweWorldEntity) && + if (prefab.ClassId != "None" && await worldEntityFactory.FindAsync(prefab.ClassId) is {} uweWorldEntity && entitySpawnPoint.AllowedTypes.Contains(uweWorldEntity.SlotType)) { float weightedProbability = prefab.Probability / entitySpawnPoint.Density; @@ -237,19 +228,19 @@ private List FilterAllowedPrefabs(List prefabs, EntitySpaw } } - return allowedPrefabs; + return (allowedPrefabs, fragmentProbability, completeFragmentProbability); } /// /// Spawns the regular (can be children of PrefabPlaceholdersGroup) which are always the same thus context independent. /// - /// - private List SpawnEntitiesStaticly(EntitySpawnPoint entitySpawnPoint, DeterministicGenerator deterministicBatchGenerator, WorldEntity parentEntity = null) + /// + private async Task> SpawnEntitiesStaticallyAsync(EntitySpawnPoint entitySpawnPoint, DeterministicGenerator deterministicBatchGenerator, WorldEntity? parentEntity = null) { - if (worldEntityFactory.TryFind(entitySpawnPoint.ClassId, out UweWorldEntity uweWorldEntity)) + if (await worldEntityFactory.FindAsync(entitySpawnPoint.ClassId) is { } uweWorldEntity) { // prefabZUp should not be taken into account for statically spawned entities - return CreateEntityWithChildren(entitySpawnPoint, + return await CreateEntityWithChildrenAsync(entitySpawnPoint, entitySpawnPoint.ClassId, uweWorldEntity.TechType, false, @@ -263,7 +254,7 @@ private List SpawnEntitiesStaticly(EntitySpawnPoint entitySpawnPoint, De } /// The first entity is a and the following are its children - private List CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, string classId, NitroxTechType techType, bool prefabZUp, int cellLevel, NitroxVector3 localScale, DeterministicGenerator deterministicBatchGenerator, Entity parentEntity = null, bool randomPosition = false) + private async Task> CreateEntityWithChildrenAsync(EntitySpawnPoint entitySpawnPoint, string classId, NitroxTechType techType, bool prefabZUp, int cellLevel, NitroxVector3 localScale, DeterministicGenerator deterministicBatchGenerator, Entity? parentEntity = null, bool randomPosition = false) { WorldEntity spawnedEntity; NitroxVector3 position = entitySpawnPoint.LocalPosition; @@ -275,7 +266,7 @@ private List CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, } if (randomPosition) { - position += XorRandom.NextInsideSphere(4f); + position += random.NextInsideSphere(4f); } if (classId == CellRootEntity.CLASS_ID) @@ -304,9 +295,10 @@ private List CreateEntityWithChildren(EntitySpawnPoint entitySpawnPoint, } // See EntitySlotsPlaceholder.Spawn - if (!TryCreatePrefabPlaceholdersGroupWithChildren(ref spawnedEntity, classId, deterministicBatchGenerator)) + (bool createResult, spawnedEntity) = await TryCreatePrefabPlaceholdersGroupWithChildrenAsync(spawnedEntity, classId, deterministicBatchGenerator); + if (!createResult) { - spawnedEntity.ChildEntities = SpawnEntities(entitySpawnPoint.Children, deterministicBatchGenerator, spawnedEntity); + spawnedEntity.ChildEntities = await SpawnEntitiesAsync(entitySpawnPoint.Children, deterministicBatchGenerator, spawnedEntity); } entityBootstrapperManager.PrepareEntityIfRequired(ref spawnedEntity, deterministicBatchGenerator); @@ -349,7 +341,7 @@ private static List AllChildren(Entity entity) return allChildren; } - private List SpawnEntities(List entitySpawnPoints, DeterministicGenerator deterministicBatchGenerator, WorldEntity? parentEntity = null) + private async Task> SpawnEntitiesAsync(List entitySpawnPoints, DeterministicGenerator deterministicBatchGenerator, WorldEntity? parentEntity = null) { List entities = []; foreach (EntitySpawnPoint esp in entitySpawnPoints) @@ -365,13 +357,13 @@ private List SpawnEntities(List entitySpawnPoints, Det if (esp.Density > 0) { - if (prefabFactory.TryGetPossiblePrefabs(esp.BiomeType, out List prefabs) && prefabs.Count > 0) + if (await prefabFactory.TryGetPossiblePrefabsAsync(esp.BiomeType) is [_, ..] prefabs) { - entities.AddRange(SpawnEntitiesUsingRandomDistribution(esp, prefabs, deterministicBatchGenerator, parentEntity)); + entities.AddRange(await SpawnEntitiesUsingRandomDistributionAsync(esp, prefabs, deterministicBatchGenerator, parentEntity)); } else if (!string.IsNullOrEmpty(esp.ClassId)) { - entities.AddRange(SpawnEntitiesStaticly(esp, deterministicBatchGenerator, parentEntity)); + entities.AddRange(await SpawnEntitiesStaticallyAsync(esp, deterministicBatchGenerator, parentEntity)); } } } @@ -385,11 +377,11 @@ private List SpawnEntities(List entitySpawnPoints, Det /// This is suppressed on the client so we don't get virtual entities that the server doesn't know about. /// /// If this Entity is a PrefabPlaceholdersGroup - private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity, string classId, DeterministicGenerator deterministicBatchGenerator) + private async Task<(bool Success, WorldEntity ChangedEntity)> TryCreatePrefabPlaceholdersGroupWithChildrenAsync(WorldEntity entity, string classId, DeterministicGenerator deterministicBatchGenerator) { if (!prefabPlaceholderGroupsResource.GroupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset groupAsset)) { - return false; + return (false, entity); } entity = new PlaceholderGroupWorldEntity(entity); @@ -403,7 +395,7 @@ private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity // Two cases, either the PrefabPlaceholder holds a visible GameObject or an EntitySlot (a MB which has a chance of spawning a prefab) if (prefabAsset is PrefabPlaceholderAsset placeholderAsset && placeholderAsset.EntitySlot.HasValue) { - WorldEntity? spawnedEntity = SpawnPrefabAssetInEntitySlot(placeholderAsset.Transform, placeholderAsset.EntitySlot.Value, deterministicBatchGenerator, entity.AbsoluteEntityCell, entity); + WorldEntity? spawnedEntity = await SpawnPrefabAssetInEntitySlotAsync(placeholderAsset.Transform, placeholderAsset.EntitySlot.Value, deterministicBatchGenerator, entity.AbsoluteEntityCell, entity); if (spawnedEntity != null) { // Spawned child will not be of the same type as the current prefabAsset @@ -420,12 +412,12 @@ private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity string prefabClassId = prefabAsset.ClassId; if (prefabAsset is PrefabPlaceholderRandomAsset randomAsset && randomAsset.ClassIds.Count > 0) { - int randomIndex = XorRandom.NextIntRange(0, randomAsset.ClassIds.Count); + int randomIndex = random.NextIntRange(0, randomAsset.ClassIds.Count); prefabClassId = randomAsset.ClassIds[randomIndex]; } EntitySpawnPoint esp = new(entity.AbsoluteEntityCell, prefabAsset.Transform.LocalPosition, prefabAsset.Transform.LocalRotation, prefabAsset.Transform.LocalScale, prefabClassId); - WorldEntity spawnedEntity = (WorldEntity)SpawnEntitiesStaticly(esp, deterministicBatchGenerator, entity).First(); + WorldEntity spawnedEntity = (WorldEntity)(await SpawnEntitiesStaticallyAsync(esp, deterministicBatchGenerator, entity)).First(); if (prefabAsset is PrefabPlaceholdersGroupAsset) { spawnedEntity = new PlaceholderGroupWorldEntity(spawnedEntity, i); @@ -439,19 +431,19 @@ private bool TryCreatePrefabPlaceholdersGroupWithChildren(ref WorldEntity entity } } - return true; + return (true, entity); } - private WorldEntity? SpawnPrefabAssetInEntitySlot(NitroxTransform transform, NitroxEntitySlot entitySlot, DeterministicGenerator deterministicBatchGenerator, AbsoluteEntityCell cell, Entity parentEntity) + private async Task SpawnPrefabAssetInEntitySlotAsync(NitroxTransform transform, NitroxEntitySlot entitySlot, DeterministicGenerator deterministicBatchGenerator, AbsoluteEntityCell cell, Entity parentEntity) { - if (!prefabFactory.TryGetPossiblePrefabs(entitySlot.BiomeType, out List prefabs) || prefabs.Count == 0) + if (await prefabFactory.TryGetPossiblePrefabsAsync(entitySlot.BiomeType) is not [_, ..] prefabs) { return null; } List entities = []; EntitySpawnPoint entitySpawnPoint = new(cell, transform.LocalPosition, transform.LocalRotation, entitySlot.AllowedTypes.ToList(), 1f, entitySlot.BiomeType); - entities.AddRange(SpawnEntitiesUsingRandomDistribution(entitySpawnPoint, prefabs, deterministicBatchGenerator, parentEntity)); + entities.AddRange(await SpawnEntitiesUsingRandomDistributionAsync(entitySpawnPoint, prefabs, deterministicBatchGenerator, parentEntity)); if (entities.Count > 0) { return (WorldEntity)entities[0]; diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/CrashHomeBootstrapper.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/CrashHomeBootstrapper.cs index 0595101083..0310061b68 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/CrashHomeBootstrapper.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/CrashHomeBootstrapper.cs @@ -4,7 +4,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public class CrashHomeBootstrapper : IEntityBootstrapper +internal sealed class CrashHomeBootstrapper : IEntityBootstrapper { public void Prepare(ref WorldEntity entity, DeterministicGenerator deterministicBatchGenerator) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/GeyserBootstrapper.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/GeyserBootstrapper.cs index ba27d57a15..40db9dfa1d 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/GeyserBootstrapper.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/GeyserBootstrapper.cs @@ -3,14 +3,16 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public class GeyserBootstrapper : IEntityBootstrapper +internal sealed class GeyserBootstrapper(XorRandom random) : IEntityBootstrapper { + private readonly XorRandom random = random; + public void Prepare(ref WorldEntity entity, DeterministicGenerator deterministicBatchGenerator) { entity = new GeyserWorldEntity(entity.Transform, entity.Level, entity.ClassId, entity.SpawnedByServer, entity.Id, entity.TechType, entity.Metadata, entity.ParentId, entity.ChildEntities, - XorRandom.NextFloat(), 15 * XorRandom.NextFloat()); + random.NextFloat(), 15 * random.NextFloat()); // The value 15 doesn't mean anything in particular, it's just an initial eruption time window so geysers don't all erupt at the same time at first } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapper.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapper.cs index 031d3ac177..68d0b50390 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapper.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapper.cs @@ -3,7 +3,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public interface IEntityBootstrapper +internal interface IEntityBootstrapper { public void Prepare(ref WorldEntity spawnedEntity, DeterministicGenerator generator); } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapperManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapperManager.cs index 53f8447605..3991ca07e5 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapperManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntityBootstrapperManager.cs @@ -3,7 +3,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public interface IEntityBootstrapperManager +internal interface IEntityBootstrapperManager { public void PrepareEntityIfRequired(ref WorldEntity spawnedEntity, DeterministicGenerator generator); } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntitySpawner.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntitySpawner.cs deleted file mode 100644 index a64bc051dc..0000000000 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/IEntitySpawner.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures.GameLogic; - -namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning -{ - public interface IEntitySpawner - { - List LoadUnspawnedEntities(NitroxInt3 batchId, bool fullCacheCreation = false); - } -} diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/ReefbackBootstrapper.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/ReefbackBootstrapper.cs index 8663cd2746..8b44d1ffe1 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/ReefbackBootstrapper.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/ReefbackBootstrapper.cs @@ -7,18 +7,20 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public class ReefbackBootstrapper : IEntityBootstrapper +internal sealed class ReefbackBootstrapper : IEntityBootstrapper { + private readonly XorRandom random; private readonly float creatureProbabilitySum = 0; private readonly float plantsProbabilitySum = 0; - public ReefbackBootstrapper() + public ReefbackBootstrapper(XorRandom random) { - foreach (ReefbackSpawnData.ReefbackSlotCreature creature in SpawnableCreatures) + this.random = random; + foreach (ReefbackSlotCreature creature in SpawnableCreatures) { creatureProbabilitySum += creature.Probability; } - foreach (ReefbackSpawnData.ReefbackSlotPlant plant in SpawnablePlants) + foreach (ReefbackSlotPlant plant in SpawnablePlants) { plantsProbabilitySum += plant.Probability; } @@ -33,7 +35,7 @@ public void Prepare(ref WorldEntity entity, DeterministicGenerator generator) } // In case the grassIndex is chosen randomly - int grassIndex = XorRandom.NextIntRange(1, GRASS_VARIANTS_COUNT); + int grassIndex = random.NextIntRange(1, GRASS_VARIANTS_COUNT); entity = new ReefbackEntity(entity.Transform, entity.Level, entity.ClassId, entity.SpawnedByServer, entity.Id, entity.TechType, @@ -49,21 +51,21 @@ public void Prepare(ref WorldEntity entity, DeterministicGenerator generator) NitroxTransform slotTransform = DuplicateTransform(PlantSlotsCoordinates[i]); slotTransform.SetParent(plantSlotsRootTransform, false); - float random = XorRandom.NextFloat() * plantsProbabilitySum; + float r = random.NextFloat() * plantsProbabilitySum; float totalProbability = 0f; int chosenPlantIndex = 0; for (int k = 0; k < SpawnablePlants.Count; k++) { totalProbability += SpawnablePlants[k].Probability; - if (random <= totalProbability) + if (r <= totalProbability) { chosenPlantIndex = k; break; } } - ReefbackSpawnData.ReefbackSlotPlant slotPlant = SpawnablePlants[chosenPlantIndex]; - string randomId = slotPlant.ClassIds[XorRandom.NextIntRange(0, slotPlant.ClassIds.Count)]; + ReefbackSlotPlant slotPlant = SpawnablePlants[chosenPlantIndex]; + string randomId = slotPlant.ClassIds[random.NextIntRange(0, slotPlant.ClassIds.Count)]; NitroxId id = generator.NextId(); NitroxTransform plantTransform = new(slotTransform.Position, slotPlant.StartRotationQuaternion, NitroxVector3.One); @@ -86,25 +88,25 @@ public void Prepare(ref WorldEntity entity, DeterministicGenerator generator) NitroxTransform slotTransform = DuplicateTransform(CreatureSlotsCoordinates[i]); slotTransform.SetParent(creatureSlotsRootTransform, false); - float random = XorRandom.NextFloat() * creatureProbabilitySum; + float creatureProbability = random.NextFloat() * creatureProbabilitySum; float totalProbability = 0f; int chosenCreatureIndex = 0; for (int k = 0; k < SpawnableCreatures.Count; k++) { totalProbability += SpawnableCreatures[k].Probability; - if (random <= totalProbability) + if (creatureProbability <= totalProbability) { chosenCreatureIndex = k; break; } } - ReefbackSpawnData.ReefbackSlotCreature slotCreature = SpawnableCreatures[chosenCreatureIndex]; - int spawnCount = XorRandom.NextIntRange(slotCreature.MinNumber, slotCreature.MaxNumber + 1); + ReefbackSlotCreature slotCreature = SpawnableCreatures[chosenCreatureIndex]; + int spawnCount = random.NextIntRange(slotCreature.MinNumber, slotCreature.MaxNumber + 1); for (int j = 0; j < spawnCount; j++) { NitroxId id = generator.NextId(); - NitroxTransform creatureTransform = new(slotTransform.LocalPosition + XorRandom.NextInsideSphere(5f), slotTransform.LocalRotation, NitroxVector3.One); + NitroxTransform creatureTransform = new(slotTransform.LocalPosition + random.NextInsideSphere(5f), slotTransform.LocalRotation, NitroxVector3.One); creatureTransform.SetParent(CreatureSlotsRootTransform, false); creatureTransform.SetParent(null, false); diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/StayAtLeashPositionBootstrapper.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/StayAtLeashPositionBootstrapper.cs index 6aac88beed..b5980fd712 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/StayAtLeashPositionBootstrapper.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/StayAtLeashPositionBootstrapper.cs @@ -4,7 +4,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public class StayAtLeashPositionBootstrapper : IEntityBootstrapper +internal sealed class StayAtLeashPositionBootstrapper : IEntityBootstrapper { public void Prepare(ref WorldEntity spawnedEntity, DeterministicGenerator generator) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/SubnauticaEntityBootstrapperManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/SubnauticaEntityBootstrapperManager.cs index 2d6e35dae6..528ad4b8bb 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/SubnauticaEntityBootstrapperManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/SubnauticaEntityBootstrapperManager.cs @@ -1,25 +1,27 @@ using System.Collections.Generic; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Factories; using Nitrox.Server.Subnautica.Models.Helper; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; -public class SubnauticaEntityBootstrapperManager : IEntityBootstrapperManager +internal sealed class SubnauticaEntityBootstrapperManager(RandomFactory randomFactory) : IEntityBootstrapperManager { - private static readonly Dictionary entityBootstrappersByTechType = new() + private readonly Dictionary entityBootstrappersByClassId = new() + { + ["ce0b4131-86e2-444b-a507-45f7b824a286"] = new GeyserBootstrapper(randomFactory.GetUnityLikeRandom(1)), // Geyser.prefab + ["63462cb4-d177-4551-822f-1904f809ec1f"] = new GeyserBootstrapper(randomFactory.GetUnityLikeRandom(2)), // GeyserShort.prefab + ["8d3d3c8b-9290-444a-9fea-8e5493ecd6fe"] = new ReefbackBootstrapper(randomFactory.GetUnityLikeRandom(3)) + }; + + private readonly Dictionary entityBootstrappersByTechType = new() { [TechType.CrashHome.ToDto()] = new CrashHomeBootstrapper(), [TechType.ReaperLeviathan.ToDto()] = new StayAtLeashPositionBootstrapper(), [TechType.SeaDragon.ToDto()] = new StayAtLeashPositionBootstrapper(), [TechType.GhostLeviathan.ToDto()] = new StayAtLeashPositionBootstrapper(), }; - private static readonly Dictionary entityBootstrappersByClassId = new() - { - ["ce0b4131-86e2-444b-a507-45f7b824a286"] = new GeyserBootstrapper(), // Geyser.prefab - ["63462cb4-d177-4551-822f-1904f809ec1f"] = new GeyserBootstrapper(), // GeyserShort.prefab - ["8d3d3c8b-9290-444a-9fea-8e5493ecd6fe"] = new ReefbackBootstrapper() - }; public void PrepareEntityIfRequired(ref WorldEntity spawnedEntity, DeterministicGenerator generator) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs index a132eef9be..aebab7cc08 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs @@ -1,45 +1,55 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.Resources.Parsers; using static LootDistributionData; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; -internal class SubnauticaUwePrefabFactory(EntityDistributionsResource distributionData) : IUwePrefabFactory +internal sealed class SubnauticaUwePrefabFactory(EntityDistributionsResource distributionData) : IUwePrefabFactory { private readonly EntityDistributionsResource resource = distributionData; private readonly Dictionary> cache = new(); + private readonly Lock cacheLock = new(); - public bool TryGetPossiblePrefabs(string? biome, [NotNullWhen(true)] out List? prefabs) + public async Task> TryGetPossiblePrefabsAsync(string? biome) { if (biome == null) { - prefabs = null; - return false; + return []; } - if (cache.TryGetValue(biome, out prefabs)) + List prefabs; + lock (cacheLock) { - return true; + if (cache.TryGetValue(biome, out prefabs)) + { + return prefabs; + } } prefabs = new(); BiomeType biomeType = (BiomeType)Enum.Parse(typeof(BiomeType), biome); - if (resource.LootDistribution.GetBiomeLoot(biomeType, out DstData dstData)) + LootDistributionData distributionData = await resource.GetLootDistributionDataAsync(); + if (distributionData.GetBiomeLoot(biomeType, out DstData dstData)) { foreach (PrefabData prefabData in dstData.prefabs) { - if (resource.LootDistribution.srcDistribution.TryGetValue(prefabData.classId, out SrcData srcData)) + if (distributionData.srcDistribution.TryGetValue(prefabData.classId, out SrcData srcData)) { // Manually went through the list of those to make this "filter" // You can verify this by looping through all of SrcData (e.g in LootDistributionData.Initialize) // print the prefabPath and check the TechType related to the provided classId (WorldEntityDatabase.TryGetInfo) with PDAScanner.IsFragment bool isFragment = srcData.prefabPath.Contains("Fragment") || srcData.prefabPath.Contains("BaseGlassDome"); - prefabs.Add(new(prefabData.classId, prefabData.count, prefabData.probability, isFragment)); + lock (cacheLock) + { + prefabs.Add(new(prefabData.classId, prefabData.count, prefabData.probability, isFragment)); + } } } } - cache[biome] = prefabs; - return true; + lock (cacheLock) + { + cache[biome] = prefabs; + } + return prefabs; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs index 5449710042..f257833d96 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUweWorldEntityFactory.cs @@ -1,28 +1,26 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.Resources.Parsers; using UWE; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; -internal class SubnauticaUweWorldEntityFactory(WorldEntitiesResource resource) : IUweWorldEntityFactory +internal class SubnauticaUweWorldEntityFactory(WorldEntitiesResource resource) { private readonly WorldEntitiesResource resource = resource; - public bool TryFind(string classId, [NotNullWhen(true)] out UweWorldEntity? uweWorldEntity) + public async Task FindAsync(string classId) { - if (resource.WorldEntitiesByClassId.TryGetValue(classId, out WorldEntityInfo worldEntityInfo)) + Dictionary worldEntitiesByClassId = await resource.GetWorldEntitiesByClassIdAsync(); + if (worldEntitiesByClassId.TryGetValue(classId, out WorldEntityInfo worldEntityInfo)) { - uweWorldEntity = new(worldEntityInfo.classId, + return new(worldEntityInfo.classId, worldEntityInfo.techType.ToDto(), worldEntityInfo.slotType.ToString(), worldEntityInfo.prefabZUp, (int)worldEntityInfo.cellLevel, worldEntityInfo.localScale.ToDto()); - - return true; } - uweWorldEntity = null; - return false; + return null; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs index 4b64b8ab3c..c9b30d0013 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs @@ -8,6 +8,7 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Helper; using Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; @@ -21,6 +22,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; internal sealed class WorldEntityManager { private readonly BatchEntitySpawner batchEntitySpawner; + private readonly IPacketSender packetSender; private readonly EntityRegistry entityRegistry; /// @@ -39,8 +41,9 @@ internal sealed class WorldEntityManager /// internal Dictionary> worldEntitiesByCell = []; - public WorldEntityManager(EntityRegistry entityRegistry, BatchEntitySpawner batchEntitySpawner, PlayerManager playerManager, ILogger logger) + public WorldEntityManager(IPacketSender packetSender, EntityRegistry entityRegistry, BatchEntitySpawner batchEntitySpawner, PlayerManager playerManager, ILogger logger) { + this.packetSender = packetSender; this.entityRegistry = entityRegistry; this.batchEntitySpawner = batchEntitySpawner; this.playerManager = playerManager; @@ -215,7 +218,7 @@ public void UnregisterWorldEntityFromCell(NitroxId entityId, AbsoluteEntityCell } } - public void LoadAllUnspawnedEntities(CancellationToken token) + public async Task LoadAllUnspawnedEntitiesAsync(CancellationToken token) { int totalBatches = SubnauticaMap.DimensionsInBatches.X * SubnauticaMap.DimensionsInBatches.Y * SubnauticaMap.DimensionsInBatches.Z; int batchesLoaded = 0; @@ -227,7 +230,7 @@ public void LoadAllUnspawnedEntities(CancellationToken token) { for (int z = 0; z < SubnauticaMap.DimensionsInBatches.Z; z++) { - int spawned = LoadUnspawnedEntities(new(x, y, z), true); + int spawned = await LoadUnspawnedEntitiesAsync(new(x, y, z), true); logger.ZLogDebug($"Loaded {spawned} entities from batch ({x}, {y}, {z})"); @@ -242,9 +245,9 @@ public void LoadAllUnspawnedEntities(CancellationToken token) } } - public int LoadUnspawnedEntities(NitroxInt3 batchId, bool suppressLogs) + public async Task LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool suppressLogs) { - List spawnedEntities = batchEntitySpawner.LoadUnspawnedEntities(batchId, suppressLogs); + List spawnedEntities = await batchEntitySpawner.LoadUnspawnedEntitiesAsync(batchId, suppressLogs); List entitiesInCells = spawnedEntities.Where(entity => typeof(WorldEntity).IsAssignableFrom(entity.GetType()) && entity.GetType() != typeof(CellRootEntity) && @@ -348,7 +351,7 @@ private void EntitySwitchedCells(WorldEntity entity, AbsoluteEntityCell oldCell, { if (player.HasCellLoaded(newCell) && !player.HasCellLoaded(oldCell)) { - player.SendPacket(new SpawnEntities(entity)); + packetSender.SendPacketAsync(new SpawnEntities(entity), player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/EscapePodManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/EscapePodManager.cs index 079ec39027..f5c7846e30 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/EscapePodManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/EscapePodManager.cs @@ -1,47 +1,50 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; +using Nitrox.Server.Subnautica.Models.Factories; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.Resources.Parsers; namespace Nitrox.Server.Subnautica.Models.GameLogic; -internal class EscapePodManager(EntityRegistry entityRegistry, RandomStartResource randomStartResource, IOptions options) +internal class EscapePodManager(RandomFactory randomFactory, EntityRegistry entityRegistry, RandomStartResource randomStartResource, IOptions options) { private const int PLAYERS_PER_ESCAPEPOD = 50; private readonly EntityRegistry entityRegistry = entityRegistry; private readonly RandomStartResource randomStartResource = randomStartResource; private readonly IOptions options = options; - private readonly ThreadSafeDictionary escapePodsByPlayerId = []; + private readonly ThreadSafeDictionary escapePodsByPlayerId = []; private EscapePodEntity? podForNextPlayer; + private readonly Random random = randomFactory.GetDotnetRandom(); - public NitroxId AssignPlayerToEscapePod(ushort playerId, out Optional newlyCreatedPod) + public async Task<(NitroxId escapePodId, EscapePodEntity? newlyCreatedPod)> AssignPlayerToEscapePodAsync(PeerId playerId) { - newlyCreatedPod = Optional.Empty; if (escapePodsByPlayerId.TryGetValue(playerId, out EscapePodEntity podEntity)) { - return podEntity.Id; + return (podEntity.Id, null); } - if (podForNextPlayer == null || IsPodFull(podForNextPlayer)) + if (!HasEmptySlot(podForNextPlayer)) { - newlyCreatedPod = Optional.Of(CreateNewEscapePod()); - podForNextPlayer = newlyCreatedPod.Value; + podForNextPlayer = await CreateNewEscapePodAsync(); } podForNextPlayer.Players.Add(playerId); escapePodsByPlayerId[playerId] = podForNextPlayer; - return podForNextPlayer.Id; + return (podForNextPlayer.Id, podForNextPlayer); } - private EscapePodEntity CreateNewEscapePod() + private async Task CreateNewEscapePodAsync() { - EscapePodEntity escapePod = new(GetStartPosition(), new NitroxId(), new EscapePodMetadata(false, false)); + EscapePodEntity escapePod = new(await GetStartPositionAsync(), new NitroxId(), new EscapePodMetadata(false, false)); escapePod.ChildEntities.Add(new PrefabChildEntity(new NitroxId(), "5c06baec-0539-4f26-817d-78443548cc52", new NitroxTechType("Radio"), 0, null, escapePod.Id)); escapePod.ChildEntities.Add(new PrefabChildEntity(new NitroxId(), "c0175cf7-0b6a-4a1d-938f-dad0dbb6fa06", new NitroxTechType("MedicalCabinet"), 0, null, escapePod.Id)); @@ -53,7 +56,7 @@ private EscapePodEntity CreateNewEscapePod() return escapePod; } - private NitroxVector3 GetStartPosition() + private async Task GetStartPositionAsync() { List escapePods = entityRegistry.GetEntities(); @@ -62,8 +65,8 @@ private NitroxVector3 GetStartPosition() { throw new InvalidOperationException(); } - Random rnd = new(seed.GetHashCode()); - NitroxVector3 position = randomStartResource.RandomStartGenerator.GenerateRandomStartPosition(rnd); + RandomStartGenerator randomStartGenerator = await randomStartResource.GetRandomStartGeneratorAsync(); + NitroxVector3 position = randomStartGenerator.GenerateAllStartPositions(random).FirstOrDefault(); if (escapePods.Count == 0) { @@ -83,8 +86,8 @@ private NitroxVector3 GetStartPosition() } } - float xNormed = (float)rnd.NextDouble(); - float zNormed = (float)rnd.NextDouble(); + float xNormed = (float)random.NextDouble(); + float zNormed = (float)random.NextDouble(); if (xNormed < 0.3f) { @@ -104,7 +107,7 @@ private NitroxVector3 GetStartPosition() zNormed = 0.7f; } - NitroxVector3 lastEscapePodPosition = escapePods[escapePods.Count - 1].Transform.Position; + NitroxVector3 lastEscapePodPosition = escapePods[^1].Transform.Position; float x = xNormed * 100 - 50; float z = zNormed * 100 - 50; @@ -112,30 +115,30 @@ private NitroxVector3 GetStartPosition() return new NitroxVector3(lastEscapePodPosition.X + x, 0, lastEscapePodPosition.Z + z); } - public void AddKnownPods(IReadOnlyCollection escapePods) + public async Task AddKnownPodsAsync(IReadOnlyCollection escapePods) { - InitializePodForNextPlayer(); + await InitializePodForNextPlayerAsync(); InitializeEscapePodsByPlayerId(); - void InitializePodForNextPlayer() + async Task InitializePodForNextPlayerAsync() { foreach (EscapePodEntity pod in escapePods) { - if (!IsPodFull(pod)) + if (HasEmptySlot(pod)) { podForNextPlayer = pod; return; } } - podForNextPlayer = CreateNewEscapePod(); + podForNextPlayer = await CreateNewEscapePodAsync(); } void InitializeEscapePodsByPlayerId() { foreach (EscapePodEntity pod in escapePods) { - foreach (ushort playerId in pod.Players) + foreach (PeerId playerId in pod.Players) { escapePodsByPlayerId[playerId] = pod; } @@ -143,8 +146,12 @@ void InitializeEscapePodsByPlayerId() } } - private static bool IsPodFull(EscapePodEntity pod) + private static bool HasEmptySlot([NotNullWhen(true)] EscapePodEntity? pod) { - return pod.Players.Count >= PLAYERS_PER_ESCAPEPOD; + if (pod == null) + { + return false; + } + return pod.Players.Count < PLAYERS_PER_ESCAPEPOD; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/GameData.cs b/Nitrox.Server.Subnautica/Models/GameLogic/GameData.cs index 1a048fc470..dcf988f3c6 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/GameData.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/GameData.cs @@ -20,7 +20,7 @@ public static GameData From(PdaManager pdaManager, StoryGoalData storyGoals, Sto { return new GameData { - PDAState = pdaManager.PdaState, + PDAState = pdaManager.GetPdaStateCopy(), StoryGoals = StoryGoalData.From(storyGoals, storyScheduler), StoryTiming = StoryTimingData.From(storyManager, timeService) }; diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs index 6b699d6948..b53c184842 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs @@ -1,51 +1,55 @@ using System.Collections.Generic; using System.Linq; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.MultiplayerSession; +using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.Communication; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic; -internal sealed class JoiningManager +internal sealed class JoiningManager( + IPacketSender packetSender, + PlayerManager playerManager, + SessionManager sessionManager, + WorldEntityManager worldEntityManager, + PdaManager pdaManager, + StoryManager storyManager, + StoryScheduler storyScheduler, + EntitySimulation entitySimulation, + EscapePodManager escapePodManager, + EntityRegistry entityRegistry, + SessionSettings sessionSettings, + IOptions options, + ILogger logger) + : ISessionCleaner { - private readonly PlayerManager playerManager; - private readonly WorldEntityManager worldEntityManager; - private readonly PdaManager pdaManager; - private readonly StoryManager storyManager; - private readonly StoryScheduler storyScheduler; - private readonly EntitySimulation entitySimulation; - private readonly IOptions options; - private readonly ILogger logger; - private readonly EscapePodManager escapePodManager; - private readonly EntityRegistry entityRegistry; - private readonly SessionSettings sessionSettings; - - private readonly ThreadSafeQueue<(INitroxConnection, string)> joinQueue = new(); + private readonly IPacketSender packetSender = packetSender; + private readonly PlayerManager playerManager = playerManager; + private readonly SessionManager sessionManager = sessionManager; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; + private readonly PdaManager pdaManager = pdaManager; + private readonly StoryManager storyManager = storyManager; + private readonly StoryScheduler storyScheduler = storyScheduler; + private readonly EntitySimulation entitySimulation = entitySimulation; + private readonly IOptions options = options; + private readonly ILogger logger = logger; + private readonly EscapePodManager escapePodManager = escapePodManager; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly SessionSettings sessionSettings = sessionSettings; + + private readonly ThreadSafeQueue<(SessionId, string)> joinQueue = new(); private readonly Lock queueLocker = new(); // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue private bool queueActive; public Action? SyncFinishedCallback { get; private set; } - public JoiningManager(PlayerManager playerManager, WorldEntityManager worldEntityManager, PdaManager pdaManager, StoryManager storyManager, StoryScheduler storyScheduler, EntitySimulation entitySimulation, EscapePodManager escapePodManager, EntityRegistry entityRegistry, SessionSettings sessionSettings, IOptions options, ILogger logger) - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.pdaManager = pdaManager; - this.storyManager = storyManager; - this.storyScheduler = storyScheduler; - this.entitySimulation = entitySimulation; - this.options = options; - this.logger = logger; - this.escapePodManager = escapePodManager; - this.entityRegistry = entityRegistry; - this.sessionSettings = sessionSettings; - } - private async Task JoinQueueLoop() { while (true) @@ -61,33 +65,35 @@ private async Task JoinQueueLoop() try { - (INitroxConnection connection, string reservationKey) = joinQueue.Dequeue(); - string name = playerManager.GetPlayerContext(reservationKey).PlayerName; + (SessionId sessionId, string reservationKey) = joinQueue.Dequeue(); + string? name = playerManager.GetPlayerContext(reservationKey)?.PlayerName; + if (name == null) + { + continue; + } // Do this after dequeuing because everyone's position shifts forward - (INitroxConnection, string)[] array = [.. joinQueue]; + (SessionId, string)[] array = [.. joinQueue]; for (int i = 0; i < array.Length; i++) { - (INitroxConnection c, _) = array[i]; - c.SendPacket(new JoinQueueInfo(i + 1, options.Value.InitialSyncTimeout)); + (SessionId s, _) = array[i]; + await packetSender.SendPacketAsync(new JoinQueueInfo(i + 1, options.Value.InitialSyncTimeout), s); } logger.ZLogInformation($"Starting sync for player {name}"); - SendInitialSync(connection, reservationKey); + await SendInitialSyncAsync(sessionId, reservationKey); using CancellationTokenSource source = new(options.Value.InitialSyncTimeout); bool syncFinished = false; SyncFinishedCallback = () => { syncFinished = true; }; - while (!syncFinished && - connection.State != NitroxConnectionState.Disconnected && - !source.IsCancellationRequested) + while (!syncFinished && sessionManager.IsConnected(sessionId) && !source.IsCancellationRequested) { - await Task.Delay(10); + await Task.Delay(10, source.Token); } - if (connection.State == NitroxConnectionState.Disconnected) + if (!sessionManager.IsConnected(sessionId)) { logger.ZLogInformation($"Player {name} disconnected while syncing"); } @@ -96,16 +102,19 @@ private async Task JoinQueueLoop() logger.ZLogInformation($"Initial sync timed out for player {name}"); SyncFinishedCallback = null; - if (connection.State == NitroxConnectionState.Connected) + if (sessionManager.IsConnected(sessionId)) { - connection.SendPacket(new PlayerKicked("Initial sync took too long and timed out")); + await packetSender.SendPacketAsync(new PlayerKicked("Initial sync took too long and timed out"), sessionId); } - playerManager.PlayerDisconnected(connection); } else { logger.ZLogInformation($"Player {name} joined successfully. Remaining requests: {joinQueue.Count}"); - BroadcastPlayerJoined(playerManager.GetPlayer(connection)); + if (!playerManager.TryGetPlayerBySessionId(sessionId, out Player? player)) + { + throw new Exception($"Failed to get player object for session #{sessionId}"); + } + BroadcastPlayerJoined(player); } } catch (Exception e) @@ -115,17 +124,17 @@ private async Task JoinQueueLoop() } } - public void AddToJoinQueue(INitroxConnection connection, string reservationKey) + public void AddToJoinQueue(SessionId sessionId, string reservationKey) { // Necessary to avoid race conditions between JoinQueueLoop and AddToJoinQueue lock (queueLocker) { logger.ZLogInformation($"Added player {playerManager.GetPlayerContext(reservationKey)?.PlayerName} to queue"); - joinQueue.Enqueue((connection, reservationKey)); + joinQueue.Enqueue((sessionId, reservationKey)); if (queueActive) { - connection.SendPacket(new JoinQueueInfo(joinQueue.Count, options.Value.InitialSyncTimeout)); + packetSender.SendPacketAsync(new JoinQueueInfo(joinQueue.Count, options.Value.InitialSyncTimeout), sessionId); } else { @@ -137,20 +146,20 @@ public void AddToJoinQueue(INitroxConnection connection, string reservationKey) } } - private void SendInitialSync(INitroxConnection connection, string reservationKey) + private async Task SendInitialSyncAsync(SessionId sessionId, string reservationKey) { - Player player = playerManager.PlayerConnected(connection, reservationKey, out bool wasBrandNewPlayer); - NitroxId assignedEscapePodId = escapePodManager.AssignPlayerToEscapePod(player.Id, out Optional newlyCreatedEscapePod); + Player player = playerManager.CreatePlayerData(sessionId, reservationKey, out bool wasBrandNewPlayer); + (NitroxId assignedEscapePodId, EscapePodEntity? newlyCreatedEscapePod) = await escapePodManager.AssignPlayerToEscapePodAsync(player.Id); if (wasBrandNewPlayer) { player.SubRootId = assignedEscapePodId; } - if (newlyCreatedEscapePod.HasValue) + if (newlyCreatedEscapePod is { } validEscapePod) { - SpawnEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value); - playerManager.SendPacketToOtherPlayers(spawnNewEscapePod, player); + SpawnEntities spawnNewEscapePod = new(validEscapePod); + await packetSender.SendPacketToOthersAsync(spawnNewEscapePod, sessionId); } // TODO: Remove this code when security of player login is improved by https://github.com/SubnauticaNitrox/Nitrox/issues/1996 @@ -158,7 +167,7 @@ private void SendInitialSync(INitroxConnection connection, string reservationKey player.Permissions = options.Value.DefaultPlayerPerm; // Make players on localhost admin by default. - if (options.Value.LocalhostIsAdmin && connection.Endpoint.Address.IsLocalhost()) + if (options.Value.LocalhostIsAdmin && sessionManager.GetEndPoint(sessionId)?.Address.IsLocalhost() == true) { logger.ZLogInformation($"Granted admin to '{player.Name}' because they're playing on the host machine"); player.Permissions = Perms.ADMIN; @@ -199,7 +208,7 @@ private void SendInitialSync(INitroxConnection connection, string reservationKey player.DisplaySurfaceWater ); - player.SendPacket(initialPlayerSync); + await packetSender.SendPacketAsync(initialPlayerSync, player.SessionId); IEnumerable GetOtherPlayers(Player player) { @@ -227,15 +236,16 @@ PlayerEntity RespawnExistingEntity(Player player) } } - public void JoiningPlayerDisconnected(INitroxConnection connection) + private void BroadcastPlayerJoined(Player player) { - // They may have been queued, so just erase their entry - joinQueue.RemoveWhere(tuple => Equals(tuple.Item1, connection)); + PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity); + packetSender.SendPacketToOthersAsync(playerJoinedPacket, player.SessionId); } - public void BroadcastPlayerJoined(Player player) + public Task OnEventAsync(ISessionCleaner.Args args) { - PlayerJoinedMultiplayerSession playerJoinedPacket = new(player.PlayerContext, player.SubRootId, player.Entity); - playerManager.SendPacketToOtherPlayers(playerJoinedPacket, player); + // They may have been queued, so just erase their entry + joinQueue.RemoveWhere(tuple => Equals(tuple.Item1, args.Session.Id)); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/PdaManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/PdaManager.cs index 10b85aebc3..8845d0d4a6 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/PdaManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/PdaManager.cs @@ -9,78 +9,153 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic; /// -/// Manager for . +/// Manager for thread-safe access to . /// -/// internal sealed class PdaManager(ILogger logger) : ISummarize { private readonly ILogger logger = logger; - public PdaStateData PdaState { get; set; } = new(); + + private readonly Lock pdaStateLock = new(); + + /// + /// Any access to this state must use the same thread-safe lock to avoid one list being updated based on the invalid + /// state of another. + /// + public PdaStateData PdaState + { + private get; + set + { + lock (pdaStateLock) + { + field = value; + } + } + } = new(); + + public PdaStateData GetPdaStateCopy() + { + lock (pdaStateLock) + { + return PdaState.GetFullCopy(); + } + } public void AddKnownTechType(NitroxTechType techType, List partialTechTypesToRemove) { - PdaState.ScannerPartial.RemoveAll(entry => partialTechTypesToRemove.Contains(entry.TechType)); - if (!PdaState.KnownTechTypes.Contains(techType)) + bool duplicateAttempt = false; + try { - PdaState.KnownTechTypes.Add(techType); + lock (pdaStateLock) + { + PdaState.ScannerPartial.RemoveAllFast(partialTechTypesToRemove, static (entry, list) => list.Contains(entry.TechType)); + if (PdaState.KnownTechTypes.Contains(techType)) + { + duplicateAttempt = true; + return; + } + PdaState.KnownTechTypes.Add(techType); + } } - else + finally { - logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the KnownTechTypes: [{techType.Name}]"); + if (duplicateAttempt) + { + logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the {nameof(PdaState.KnownTechTypes)}: [{techType.Name}]"); + } } } public void AddAnalyzedTechType(NitroxTechType techType) { - if (!PdaState.AnalyzedTechTypes.Contains(techType)) + bool duplicateAttempt = false; + try { - PdaState.AnalyzedTechTypes.Add(techType); + lock (pdaStateLock) + { + if (PdaState.AnalyzedTechTypes.Contains(techType)) + { + duplicateAttempt = true; + return; + } + PdaState.AnalyzedTechTypes.Add(techType); + } } - else + finally { - logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the AnalyzedTechTypes: [{techType.Name}]"); + if (duplicateAttempt) + { + logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the {nameof(PdaState.AnalyzedTechTypes)}: [{techType.Name}]"); + } } } public void AddEncyclopediaEntry(string entry) { - if (!PdaState.EncyclopediaEntries.Contains(entry)) + bool duplicateAttempt = false; + try { - PdaState.EncyclopediaEntries.Add(entry); + lock (pdaStateLock) + { + if (PdaState.EncyclopediaEntries.Contains(entry)) + { + duplicateAttempt = true; + return; + } + PdaState.EncyclopediaEntries.Add(entry); + } } - else + finally { - logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the EncyclopediaEntries: [{entry}]"); + if (duplicateAttempt) + { + logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the {nameof(PdaState.EncyclopediaEntries)}: [{entry}]"); + } } } public void AddPDALogEntry(PDALogEntry entry) { - if (PdaState.PdaLog.All(logEntry => logEntry.Key != entry.Key)) + bool duplicateAttempt = false; + try { - PdaState.PdaLog.Add(entry); + lock (pdaStateLock) + { + if (PdaState.PdaLog.Any(logEntry => logEntry.Key == entry.Key)) + { + duplicateAttempt = true; + return; + } + PdaState.PdaLog.Add(entry); + } } - else + finally { - logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the PDALog: [{entry.Key}]"); + if (duplicateAttempt) + { + logger.ZLogDebug($"There was an attempt of adding a duplicated entry in the {nameof(PdaState.PdaLog)}: [{entry.Key}]"); + } } } public void AddScannerFragment(NitroxId id) { - PdaState.ScannerFragments.Add(id); + lock (pdaStateLock) + { + PdaState.ScannerFragments.Add(id); + } } public void UpdateEntryUnlockedProgress(NitroxTechType techType, int unlockedAmount, bool fullyResearched) { - if (fullyResearched) + lock (pdaStateLock) { - PdaState.ScannerPartial.RemoveAll(entry => entry.TechType.Equals(techType)); - PdaState.ScannerComplete.Add(techType); - } - else - { - lock (PdaState.ScannerPartial) + if (fullyResearched) + { + PdaState.ScannerPartial.RemoveAllFast(techType, static (entry, toRemove) => entry.TechType.Equals(toRemove)); + PdaState.ScannerComplete.Add(techType); + } + else { if (PdaState.ScannerPartial.FirstOrDefault(e => e.TechType.Equals(techType)) is { } entry) { @@ -96,25 +171,54 @@ public void UpdateEntryUnlockedProgress(NitroxTechType techType, int unlockedAmo public InitialPDAData GetInitialPDAData() { - return new(PdaState.KnownTechTypes.ToList(), - PdaState.AnalyzedTechTypes.ToList(), - PdaState.PdaLog.ToList(), - PdaState.EncyclopediaEntries.ToList(), - PdaState.ScannerFragments.ToList(), - PdaState.ScannerPartial.ToList(), - PdaState.ScannerComplete.ToList()); + lock (pdaStateLock) + { + return new(PdaState.KnownTechTypes.ToList(), + PdaState.AnalyzedTechTypes.ToList(), + PdaState.PdaLog.ToList(), + PdaState.EncyclopediaEntries.ToList(), + PdaState.ScannerFragments.ToList(), + PdaState.ScannerPartial.ToList(), + PdaState.ScannerComplete.ToList()); + } } - public void RemovePdaLogsByKey(string eventKey) => PdaState.PdaLog.RemoveAll(entry => entry.Key == eventKey); + public void RemovePdaLogsByKey(string eventKey) + { + lock (pdaStateLock) + { + PdaState.PdaLog.RemoveAllFast(eventKey, static (entry, keyToRemove) => entry.Key == keyToRemove); + } + } - public bool ContainsCompletelyScannedTech(NitroxTechType techType) => PdaState.ScannerComplete.Contains(techType); + public bool ContainsCompletelyScannedTech(NitroxTechType techType) + { + lock (pdaStateLock) + { + return PdaState.ScannerComplete.Contains(techType); + } + } - public bool ContainsLog(string pdaEntryId) => PdaState.PdaLog.Any(entry => entry.Key == pdaEntryId); + public bool ContainsLog(string pdaEntryId) + { + lock (pdaStateLock) + { + return PdaState.PdaLog.Any(entry => entry.Key == pdaEntryId); + } + } Task IEvent.OnEventAsync(ISummarize.Args args) { - logger.ZLogInformation($"Known tech: {PdaState.KnownTechTypes.Count}"); - logger.ZLogInformation($"Encyclopedia entries: {PdaState.EncyclopediaEntries.Count}"); + int knownTechCount; + int encyclopediaEntriesCount; + lock (pdaStateLock) + { + knownTechCount = PdaState.KnownTechTypes.Count; + encyclopediaEntriesCount = PdaState.EncyclopediaEntries.Count; + } + logger.ZLogInformation($"Known tech: {knownTechCount}"); + logger.ZLogInformation($"Encyclopedia entries: {encyclopediaEntriesCount}"); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs index bc1f450118..626faf4cec 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs @@ -1,6 +1,10 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; using System.Text.RegularExpressions; +using Nitrox.Model.Constants; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.DataStructures.Unity; @@ -8,42 +12,34 @@ using Nitrox.Model.MultiplayerSession; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.MultiplayerSession; +using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.Communication; -using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.GameLogic; -// TODO: These methods are a little chunky. Need to look at refactoring just to clean them up and get them around 30 lines a piece. -internal sealed partial class PlayerManager +// TODO: This manager should only handle player data. Move connection related state to other managers. +internal sealed partial class PlayerManager(SessionManager sessionManager, IOptions options, ILogger logger) : ISessionCleaner { - // https://regex101.com/r/eTWiEs/2/ - [GeneratedRegex(@"^[a-zA-Z0-9._-]{3,25}$", RegexOptions.NonBacktracking)] + [GeneratedRegex(NitroxConstants.PLAYER_NAME_VALID_REGEX, RegexOptions.NonBacktracking)] private static partial Regex PlayerNameRegex(); private readonly ThreadSafeDictionary allPlayersByName = []; - private readonly ThreadSafeDictionary connectedPlayersById = []; - private readonly ThreadSafeDictionary assetsByConnection = []; + private readonly ThreadSafeDictionary connectedPlayersBySessionId = []; + private readonly ThreadSafeDictionary assetsBySessionId = []; private readonly ThreadSafeDictionary reservations = []; private readonly ThreadSafeSet reservedPlayerNames = new("Player"); // "Player" is often used to identify the local player and should not be used by any user - private readonly IOptions options; - private readonly HibernateService hibernateService; - private readonly ILogger logger; - private ushort currentPlayerId; - - public PlayerManager(IOptions options, HibernateService hibernateService, ILogger logger) - { - this.options = options; - this.hibernateService = hibernateService; - this.logger = logger; - } + private readonly SessionManager sessionManager = sessionManager; + private readonly IOptions options = options; + private readonly ILogger logger = logger; + private PeerId currentPlayerId; /// All players that have joined since the server started, even if they disconnected public IEnumerable GetAllPlayers() => allPlayersByName.Values; public IEnumerable ConnectedPlayers() { - return assetsByConnection.Values + return assetsBySessionId.Values .Where(assetPackage => assetPackage.Player != null) .Select(assetPackage => assetPackage.Player); } @@ -55,9 +51,9 @@ public List GetConnectedPlayersExcept(Player excludePlayer) return ConnectedPlayers().Where(player => player != excludePlayer).ToList(); } - public Player? GetPlayer(INitroxConnection connection) + public List GetConnectedPlayersExcept(SessionId excludeSessionId) { - return assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage) ? assetPackage.Player : null; + return ConnectedPlayers().Where(player => player.SessionId != excludeSessionId).ToList(); } public PlayerContext? GetPlayerContext(string reservationKey) @@ -65,14 +61,15 @@ public List GetConnectedPlayersExcept(Player excludePlayer) return reservations.TryGetValue(reservationKey, out PlayerContext playerContext) ? playerContext : null; } - public void AddPlayer(Player player) + public void AddSavedPlayer(Player player) { allPlayersByName.Add(player.Name, player); currentPlayerId = allPlayersByName.Values.Max(x => x.Id); } public MultiplayerSessionReservation ReservePlayerContext( - INitroxConnection connection, + SessionId sessionId, + IPEndPoint endPoint, PlayerSettings playerSettings, AuthenticationContext authenticationContext, string correlationId) @@ -98,7 +95,7 @@ public MultiplayerSessionReservation ReservePlayerContext( string playerName = authenticationContext.Username; - allPlayersByName.TryGetValue(playerName, out Player player); + allPlayersByName.TryGetValue(playerName, out Player? player); if (player?.IsPermaDeath == true && options.Value.IsHardcore()) { MultiplayerSessionReservationState rejectedState = MultiplayerSessionReservationState.REJECTED | MultiplayerSessionReservationState.HARDCORE_PLAYER_DEAD; @@ -111,71 +108,75 @@ public MultiplayerSessionReservation ReservePlayerContext( return new MultiplayerSessionReservation(correlationId, rejectedState); } - assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage); + assetsBySessionId.TryGetValue(sessionId, out ConnectionAssets assetPackage); if (assetPackage == null) { assetPackage = new ConnectionAssets(); - assetsByConnection.Add(connection, assetPackage); + assetsBySessionId.Add(sessionId, assetPackage); reservedPlayerNames.Add(playerName); } bool hasSeenPlayerBefore = player != null; - ushort playerId = hasSeenPlayerBefore ? player.Id : ++currentPlayerId; NitroxId playerNitroxId = hasSeenPlayerBefore ? player.GameObjectId : new NitroxId(); SubnauticaGameMode gameMode = hasSeenPlayerBefore ? player.GameMode : options.Value.GameMode; IntroCinematicMode introCinematicMode = hasSeenPlayerBefore ? IntroCinematicMode.COMPLETED : IntroCinematicMode.LOADING; PlayerAnimation animation = new(AnimChangeType.UNDERWATER, AnimChangeState.ON); + SessionManager.Session session = sessionManager.GetOrCreateSession(endPoint); + // TODO: At some point, store the muted state of a player - PlayerContext playerContext = new(playerName, playerId, playerNitroxId, !hasSeenPlayerBefore, playerSettings, false, gameMode, null, introCinematicMode, animation); + PlayerContext playerContext = new(playerName, session.Id, playerNitroxId, !hasSeenPlayerBefore, playerSettings, false, gameMode, null, introCinematicMode, animation); string reservationKey = Guid.NewGuid().ToString(); reservations.Add(reservationKey, playerContext); assetPackage.ReservationKey = reservationKey; - return new MultiplayerSessionReservation(correlationId, playerId, reservationKey); + return new MultiplayerSessionReservation(correlationId, session.Id, reservationKey); } - public Player PlayerConnected(INitroxConnection connection, string reservationKey, out bool wasBrandNewPlayer) + public Player CreatePlayerData(SessionId sessionId, string reservationKey, out bool wasBrandNewPlayer) { PlayerContext playerContext = reservations[reservationKey]; Validate.NotNull(playerContext); - ConnectionAssets assetPackage = assetsByConnection[connection]; + ConnectionAssets assetPackage = assetsBySessionId[sessionId]; Validate.NotNull(assetPackage); wasBrandNewPlayer = playerContext.WasBrandNewPlayer; if (!allPlayersByName.TryGetValue(playerContext.PlayerName, out Player player)) { - player = new Player(playerContext.PlayerId, - playerContext.PlayerName, - false, - playerContext, - connection, - NitroxVector3.Zero, - NitroxQuaternion.Identity, - playerContext.PlayerNitroxId, - Optional.Empty, - options.Value.DefaultPlayerPerm, - new(options.Value.DefaultOxygenValue, options.Value.DefaultMaxOxygenValue, options.Value.DefaultHealthValue, options.Value.DefaultHungerValue, options.Value.DefaultThirstValue, options.Value.DefaultInfectionValue), - options.Value.GameMode, - [], - [], - new Dictionary(), - new Dictionary(), - new Dictionary(), - [], - false, - true + player = new Player(++currentPlayerId, + sessionId, + playerContext.PlayerName, + false, + playerContext, + NitroxVector3.Zero, + NitroxQuaternion.Identity, + playerContext.PlayerNitroxId, + Optional.Empty, + options.Value.DefaultPlayerPerm, + new(options.Value.DefaultOxygenValue, options.Value.DefaultMaxOxygenValue, options.Value.DefaultHealthValue, options.Value.DefaultHungerValue, options.Value.DefaultThirstValue, options.Value.DefaultInfectionValue), + options.Value.GameMode, + [], + [], + new Dictionary(), + new Dictionary(), + new Dictionary(), + [], + false, + true ); allPlayersByName[playerContext.PlayerName] = player; } + else + { + player.SessionId = sessionId; + } - connectedPlayersById.Add(playerContext.PlayerId, player); + connectedPlayersBySessionId.Add(playerContext.SessionId, player); // TODO: make a ConnectedPlayer wrapper so this is not stateful player.PlayerContext = playerContext; - player.Connection = connection; // reconnecting players need to have their cell visibility refreshed player.ClearVisibleCells(); @@ -187,40 +188,20 @@ public Player PlayerConnected(INitroxConnection connection, string reservationKe return player; } - public int PlayerCount => connectedPlayersById.Count; + public int PlayerCount => connectedPlayersBySessionId.Count; - public void PlayerDisconnected(INitroxConnection connection) + public bool SetPlayerProperty(SessionId sessionId, T value, Action action) { - if (!assetsByConnection.TryGetValue(connection, out ConnectionAssets assetPackage)) + if (!TryGetPlayerBySessionId(sessionId, out Player? player)) { - return; + return false; } - if (assetPackage.ReservationKey != null) - { - PlayerContext playerContext = reservations[assetPackage.ReservationKey]; - reservedPlayerNames.Remove(playerContext.PlayerName); - reservations.Remove(assetPackage.ReservationKey); - } - - if (assetPackage.Player != null) - { - Player player = assetPackage.Player; - reservedPlayerNames.Remove(player.Name); - connectedPlayersById.Remove(player.Id); - logger.ZLogInformation($"{player.Name} left the game"); - } - - assetsByConnection.Remove(connection); - - if (!ConnectedPlayers().Any()) - { - // TODO: Make this function async - _ = hibernateService.SleepAsync().ContinueWithHandleError(); - } + action(player, value); + return true; } - public bool TryGetPlayerByName(string playerName, out Player foundPlayer) + public bool TryGetPlayerByName(string playerName, [NotNullWhen(true)] out Player? foundPlayer) { foundPlayer = null; foreach (Player player in ConnectedPlayers()) @@ -235,27 +216,37 @@ public bool TryGetPlayerByName(string playerName, out Player foundPlayer) return false; } - public bool TryGetPlayerById(ushort playerId, out Player player) + public bool TryGetPlayerBySessionId(SessionId sessionId, [NotNullWhen(true)] out Player? player) { - return connectedPlayersById.TryGetValue(playerId, out player); + return connectedPlayersBySessionId.TryGetValue(sessionId, out player); } - public void SendPacketToAllPlayers(Packet packet) + public Task OnEventAsync(ISessionCleaner.Args args) { - foreach (Player player in ConnectedPlayers()) + if (!connectedPlayersBySessionId.TryGetValue(args.Session.Id, out Player player)) { - player.SendPacket(packet); + return Task.CompletedTask; + } + if (!assetsBySessionId.TryGetValue(player.SessionId, out ConnectionAssets assetPackage)) + { + return Task.CompletedTask; } - } - public void SendPacketToOtherPlayers(Packet packet, Player sendingPlayer) - { - foreach (Player player in ConnectedPlayers()) + if (assetPackage.ReservationKey != null) { - if (player != sendingPlayer) - { - player.SendPacket(packet); - } + PlayerContext playerContext = reservations[assetPackage.ReservationKey]; + reservedPlayerNames.Remove(playerContext.PlayerName); + reservations.Remove(assetPackage.ReservationKey); + } + + if (assetPackage.Player != null) + { + reservedPlayerNames.Remove(player.Name); + connectedPlayersBySessionId.Remove(player.SessionId); + logger.ZLogInformation($"{player.Name} left the game"); } + + assetsBySessionId.Remove(player.SessionId); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Players/PersistedPlayerData.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Players/PersistedPlayerData.cs index 13870333aa..6b7aac20a1 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Players/PersistedPlayerData.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Players/PersistedPlayerData.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.DataStructures.Unity; @@ -9,7 +10,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Players; [DataContract] -public class PersistedPlayerData +internal sealed class PersistedPlayerData { [DataMember(Order = 1)] public string Name { get; set; } @@ -24,7 +25,7 @@ public class PersistedPlayerData public Dictionary EquippedItems { get; set; } = []; [DataMember(Order = 5)] - public ushort Id { get; set; } + public PeerId Id { get; set; } [DataMember(Order = 6)] public NitroxVector3 SpawnPosition { get; set; } @@ -68,10 +69,10 @@ public class PersistedPlayerData public Player ToPlayer() { return new Player(Id, + 0, Name, IsPermaDeath, null, //no connection/context as this player is not connected. - null, SpawnPosition, SpawnRotation, NitroxId, diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Players/PlayerData.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Players/PlayerData.cs index 4113019f85..b2e9e78c24 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Players/PlayerData.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Players/PlayerData.cs @@ -5,7 +5,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Players { [DataContract] - public class PlayerData + internal sealed class PlayerData { [DataMember(Order = 1)] public List Players = []; diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs b/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs index a1263fd598..2693c01a81 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; namespace Nitrox.Server.Subnautica.Models.GameLogic { - public class SimulationOwnershipData + internal sealed class SimulationOwnershipData { public struct PlayerLock { @@ -65,15 +66,15 @@ public bool RevokeIfOwner(NitroxId id, Player player) } } - public List RevokeAllForOwner(Player player) + public List RevokeAllForOwner(SessionId sessionId) { lock (playerLocksById) { - List revokedIds = new List(); + List revokedIds = []; foreach (KeyValuePair idWithPlayerLock in playerLocksById) { - if (idWithPlayerLock.Value.Player == player) + if (idWithPlayerLock.Value.Player.SessionId == sessionId) { revokedIds.Add(idWithPlayerLock.Key); } @@ -96,7 +97,7 @@ public bool RevokeOwnerOfId(NitroxId id) } } - public Player GetPlayerForLock(NitroxId id) + public Player? GetPlayerForLock(NitroxId id) { lock (playerLocksById) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs index 78f11b09e9..8e2f8787dd 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs @@ -1,25 +1,28 @@ -using System; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; +using Nitrox.Server.Subnautica.Models.AppEvents; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Timer = System.Timers.Timer; namespace Nitrox.Server.Subnautica.Models.GameLogic; -internal sealed class SleepManager(PlayerManager playerManager, TimeService timeService) +internal sealed class SleepManager(IPacketSender packetSender, PlayerManager playerManager, TimeService timeService) : ISessionCleaner { /// Duration of the sleep animation/screen fade in seconds. private const float SLEEP_DURATION = 5f; /// Time to skip when sleeping. From Bed.kSleepEndTime - Bed.kSleepStartTime (1188 - 792 = 396). private const float SLEEP_TIME_SKIP_SECONDS = 396f; - private readonly PlayerManager playerManager = playerManager; + private readonly IPacketSender packetSender = packetSender; private readonly TimeService timeService = timeService; - private readonly ThreadSafeSet playerIdsInBed = []; + private readonly ThreadSafeSet sessionIdsInBed = []; private bool isSleepInProgress; private Timer? sleepTimer; + private readonly PlayerManager playerManager = playerManager; public void PlayerEnteredBed(Player player) { - if (!playerIdsInBed.Add(player.Id)) + if (!sessionIdsInBed.Add(player.SessionId)) { return; } @@ -33,7 +36,7 @@ public void PlayerEnteredBed(Player player) public void PlayerExitedBed(Player player) { - if (!playerIdsInBed.Remove(player.Id)) + if (!sessionIdsInBed.Remove(player.SessionId)) { return; } @@ -41,49 +44,16 @@ public void PlayerExitedBed(Player player) BroadcastStatus(); } - public void PlayerDisconnected(Player player) - { - playerIdsInBed.Remove(player.Id); - // If sleep is already in progress, let it complete - don't cancel just because someone disconnected - if (isSleepInProgress) - { - return; - } - if (playerIdsInBed.Count <= 0) - { - return; - } - - // Note: The disconnecting player is still in playerManager at this point, so we subtract 1 - int remainingPlayers = playerManager.GetConnectedPlayers().Count - 1; - - // Send to all players except the disconnecting one - SleepStatusUpdate packet = new(playerIdsInBed.Count, remainingPlayers); - foreach (Player connectedPlayer in playerManager.GetConnectedPlayers()) - { - if (connectedPlayer.Id != player.Id) - { - connectedPlayer.SendPacket(packet); - } - } - - // Check if remaining players are now all sleeping (disconnected player was the only one awake) - if (remainingPlayers > 0 && playerIdsInBed.Count >= remainingPlayers) - { - StartSleep(); - } - } - private bool AreAllPlayersInBed() { int totalPlayers = playerManager.GetConnectedPlayers().Count; - return totalPlayers > 0 && playerIdsInBed.Count >= totalPlayers; + return totalPlayers > 0 && sessionIdsInBed.Count >= totalPlayers; } private void BroadcastStatus() { int totalPlayers = playerManager.GetConnectedPlayers().Count; - playerManager.SendPacketToAllPlayers(new SleepStatusUpdate(playerIdsInBed.Count, totalPlayers)); + packetSender.SendPacketToAllAsync(new SleepStatusUpdate(sessionIdsInBed.Count, totalPlayers)); } private void StartSleep() @@ -97,13 +67,37 @@ private void StartSleep() sleepTimer.Elapsed += delegate { timeService.SkipTime(TimeSpan.FromSeconds(SLEEP_TIME_SKIP_SECONDS)); - playerManager.SendPacketToAllPlayers(new SleepComplete()); + packetSender.SendPacketToAllAsync(new SleepComplete()); isSleepInProgress = false; sleepTimer.Dispose(); sleepTimer = null; }; sleepTimer?.Start(); - playerIdsInBed.Clear(); + sessionIdsInBed.Clear(); + } + + public async Task OnEventAsync(ISessionCleaner.Args args) + { + sessionIdsInBed.Remove(args.Session.Id); + // If sleep is already in progress, let it complete - don't cancel just because someone disconnected + if (isSleepInProgress) + { + return; + } + if (sessionIdsInBed.Count <= 0) + { + return; + } + + // Send to all players except the disconnecting one + SleepStatusUpdate packet = new(sessionIdsInBed.Count, args.NewPlayerTotal); + await packetSender.SendPacketToOthersAsync(packet, args.Session.Id); + + // Check if remaining players are now all sleeping (disconnected player was the only one awake) + if (args.NewPlayerTotal > 0 && sessionIdsInBed.Count >= args.NewPlayerTotal) + { + StartSleep(); + } } } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/StoryManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/StoryManager.cs index bd6f831c00..c591a1ea4d 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/StoryManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/StoryManager.cs @@ -3,6 +3,7 @@ using Nitrox.Server.Subnautica.Models.AppEvents.Core; using Nitrox.Server.Subnautica.Models.GameLogic.Unlockables; using Nitrox.Server.Subnautica.Models.Helper; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic; @@ -14,7 +15,7 @@ internal sealed class StoryManager : ISummarize private readonly ILogger logger; private readonly IOptions options; private readonly PdaManager pdaManager; - private readonly PlayerManager playerManager; + private readonly IPacketSender packetSender; private readonly TimeService timeService; /// @@ -35,9 +36,9 @@ internal sealed class StoryManager : ISummarize public StoryGoalData StoryGoalData { get; set; } = new(); - public StoryManager(PlayerManager playerManager, PdaManager pdaManager, TimeService timeService, IOptions options, ILogger logger) + public StoryManager(IPacketSender packetSender, PdaManager pdaManager, TimeService timeService, IOptions options, ILogger logger) { - this.playerManager = playerManager; + this.packetSender = packetSender; this.pdaManager = pdaManager; this.timeService = timeService; this.options = options; @@ -87,7 +88,7 @@ public void BroadcastExplodeAurora(bool instantaneous) logger.ZLogInformation($"Aurora's explosion countdown will start in 3 seconds"); } - playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), false)); + packetSender.SendPacketToAllAsync(new AuroraAndTimeUpdate(GetTimeData(), false)); } public void BroadcastRestoreAurora() @@ -104,7 +105,7 @@ public void BroadcastRestoreAurora() StoryGoalData.CompletedGoals.Remove(eventKey); } - playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), true)); + packetSender.SendPacketToAllAsync(new AuroraAndTimeUpdate(GetTimeData(), true)); logger.ZLogInformation($"Restored Aurora, will explode again in {GetMinutesBeforeAuroraExplosion():@minutes} minutes"); } @@ -137,7 +138,7 @@ public void StartSunbeamEvent(string sunbeamEventKey) { StoryGoalData.CompletedGoals.Remove(PlaySunbeamEvent.SunbeamGoals[i]); } - playerManager.SendPacketToAllPlayers(new PlaySunbeamEvent(sunbeamEventKey)); + packetSender.SendPacketToAllAsync(new PlaySunbeamEvent(sunbeamEventKey)); } public AuroraEventData MakeAuroraData() diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/StoryScheduler.cs b/Nitrox.Server.Subnautica/Models/GameLogic/StoryScheduler.cs index b47600f654..188ed01b86 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/StoryScheduler.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/StoryScheduler.cs @@ -2,11 +2,13 @@ using System.Linq; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.GameLogic { - internal class StoryScheduler(PdaManager pdaManager, StoryManager storyManager, TimeService timeService, PlayerManager playerManager) + internal class StoryScheduler(IPacketSender packetSender, PdaManager pdaManager, StoryManager storyManager, TimeService timeService, PlayerManager playerManager) { + private readonly IPacketSender packetSender = packetSender; private readonly PdaManager pdaManager = pdaManager; private readonly PlayerManager playerManager = playerManager; private readonly ThreadSafeDictionary scheduledStories = []; @@ -71,7 +73,7 @@ public void UnscheduleStory(string storyGoalKey, bool becauseOfTime = false) if (becauseOfTime && !IsTrackedStory(storyGoalKey)) { scheduledGoal.TimeExecute = ElapsedSecondsFloat + 15; - playerManager.SendPacketToAllPlayers(new Schedule(scheduledGoal.TimeExecute, storyGoalKey, scheduledGoal.GoalType)); + packetSender.SendPacketToAllAsync(new Schedule(scheduledGoal.TimeExecute, storyGoalKey, scheduledGoal.GoalType)); return; } scheduledStories.Remove(storyGoalKey); diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/TimeService.cs b/Nitrox.Server.Subnautica/Models/GameLogic/TimeService.cs index cdefee322a..a51b158d16 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/TimeService.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/TimeService.cs @@ -2,12 +2,13 @@ using Nitrox.Model.Networking; using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.AppEvents.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Services; using Timer = System.Timers.Timer; namespace Nitrox.Server.Subnautica.Models.GameLogic; -internal sealed class TimeService(PlayerManager playerManager, NtpSyncer ntpSyncer, ILoggerFactory loggerFactory, ILogger logger) +internal sealed class TimeService(IPacketSender packetSender, NtpSyncer ntpSyncer, ILoggerFactory loggerFactory, ILogger logger) : BackgroundService, ISummarize, IHibernate { public delegate void TimeSkippedEventHandler(TimeSpan skippedTime); @@ -34,7 +35,7 @@ internal sealed class TimeService(PlayerManager playerManager, NtpSyncer ntpSync private readonly ILoggerFactory loggerFactory = loggerFactory; private readonly NtpSyncer ntpSyncer = ntpSyncer; - private readonly PlayerManager playerManager = playerManager; + private readonly IPacketSender packetSender = packetSender; private readonly PeriodicTimer resyncTimer = new(TimeSpan.FromSeconds(RESYNC_INTERVAL_SECONDS)); private readonly Stopwatch stopWatch = new(); @@ -112,7 +113,7 @@ public void ChangeTime(StoryManager.TimeModification type) GameTime += skippedTime; TimeSkipped?.Invoke(skippedTime); - playerManager.SendPacketToAllPlayers(MakeTimePacket()); + packetSender.SendPacketToAllAsync(MakeTimePacket()); } } @@ -133,7 +134,7 @@ public void SkipTime(TimeSpan skipAmount) GameTime += skipAmount; TimeSkipped?.Invoke(skipAmount); - playerManager.SendPacketToAllPlayers(MakeTimePacket()); + packetSender.SendPacketToAllAsync(MakeTimePacket()); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -152,7 +153,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (!stoppingToken.IsCancellationRequested) { await resyncTimer.WaitForNextTickAsync(stoppingToken); - playerManager.SendPacketToAllPlayers(MakeTimePacket()); + await packetSender.SendPacketToAllAsync(MakeTimePacket()); } } @@ -163,12 +164,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) return Task.CompletedTask; } - Task IEvent.OnEventAsync(IHibernate.WakeArgs args) + async Task IEvent.OnEventAsync(IHibernate.WakeArgs args) { stopWatch.Start(); resyncTimer.Period = TimeSpan.FromSeconds(RESYNC_INTERVAL_SECONDS); - playerManager.SendPacketToAllPlayers(MakeTimePacket()); - return Task.CompletedTask; + await packetSender.SendPacketToAllAsync(MakeTimePacket()); } Task IEvent.OnEventAsync(ISummarize.Args args) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Unlockables/PdaStateData.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Unlockables/PdaStateData.cs index 103f12f3bd..a19a2b09e8 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Unlockables/PdaStateData.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Unlockables/PdaStateData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Runtime.Serialization; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -7,8 +8,11 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic.Unlockables; /// /// Data container for everything Subnautica PDA related. Behavior should go in . /// +/// +/// Important: Keep in sync with any code changes! +/// [DataContract] -internal sealed class PdaStateData +internal sealed record PdaStateData { /// /// Gets or sets the KnownTech construct which powers the popup shown to the user when a new TechType is discovered @@ -17,22 +21,22 @@ internal sealed class PdaStateData /// KnownTech.analyzedTech /// [DataMember(Order = 1)] - public ThreadSafeList KnownTechTypes { get; } = []; + public List KnownTechTypes { get; } = []; [DataMember(Order = 2)] - public ThreadSafeList AnalyzedTechTypes { get; } = []; + public List AnalyzedTechTypes { get; } = []; /// /// Gets or sets the log of story events present in the PDA /// [DataMember(Order = 3)] - public ThreadSafeList PdaLog { get; } = []; + public List PdaLog { get; } = []; /// /// Gets or sets the entries that show up the the PDA's Encyclopedia /// [DataMember(Order = 4)] - public ThreadSafeList EncyclopediaEntries { get; } = []; + public List EncyclopediaEntries { get; } = []; /// /// The ids of the already scanned entities. @@ -43,17 +47,36 @@ internal sealed class PdaStateData /// We can therefore use it as a list. /// [DataMember(Order = 5)] - public ThreadSafeSet ScannerFragments { get; } = []; + public HashSet ScannerFragments { get; } = []; /// /// Partially unlocked PDA entries (e.g. fragments) /// [DataMember(Order = 6)] - public ThreadSafeList ScannerPartial { get; } = []; + public List ScannerPartial { get; } = []; /// /// Fully unlocked PDA entries /// [DataMember(Order = 7)] - public ThreadSafeList ScannerComplete { get; } = []; + public List ScannerComplete { get; } = []; + + /// + /// Gets a full copy of the entire which allows for thread safe access. + /// + public PdaStateData GetFullCopy() + { + PdaStateData copy = new(); + copy.KnownTechTypes.AddRange(KnownTechTypes); + copy.AnalyzedTechTypes.AddRange(AnalyzedTechTypes); + copy.PdaLog.AddRange(PdaLog); + copy.EncyclopediaEntries.AddRange(EncyclopediaEntries); + foreach (NitroxId fragment in ScannerFragments) + { + copy.ScannerFragments.Add(fragment); + } + copy.ScannerPartial.AddRange(ScannerPartial); + copy.ScannerComplete.AddRange(ScannerComplete); + return copy; + } } diff --git a/Nitrox.Server.Subnautica/Models/Helper/DeterministicGenerator.cs b/Nitrox.Server.Subnautica/Models/Helper/DeterministicGenerator.cs index a2742eb66b..c22d515a20 100644 --- a/Nitrox.Server.Subnautica/Models/Helper/DeterministicGenerator.cs +++ b/Nitrox.Server.Subnautica/Models/Helper/DeterministicGenerator.cs @@ -1,36 +1,30 @@ using Nitrox.Model.DataStructures; -namespace Nitrox.Server.Subnautica.Models.Helper -{ - public class DeterministicGenerator - { - private readonly Random random; +namespace Nitrox.Server.Subnautica.Models.Helper; - public DeterministicGenerator(string worldSeed, object reference) - { - random = new Random(worldSeed.GetHashCode() + reference.GetHashCode()); - } +internal sealed class DeterministicGenerator(string worldSeed, string secondSeedValue) +{ + private readonly Random random = new($"{worldSeed}{secondSeedValue}".ToMd5HashedInt32()); - public double NextDouble() - { - return random.NextDouble(); - } + public double NextDouble() + { + return random.NextDouble(); + } - public double NextDouble(double min, double max) - { - return random.NextDouble() * (max - min) + min; - } + public double NextDouble(double min, double max) + { + return random.NextDouble() * (max - min) + min; + } - public int NextInt(int min, int max) - { - return random.Next(min, max); - } + public int NextInt(int min, int max) + { + return random.Next(min, max); + } - public NitroxId NextId() - { - byte[] bytes = new byte[16]; - random.NextBytes(bytes); - return new NitroxId(bytes); - } + public NitroxId NextId() + { + byte[] bytes = new byte[16]; + random.NextBytes(bytes); + return new NitroxId(bytes); } } diff --git a/Nitrox.Server.Subnautica/Models/Helper/EasyPool.cs b/Nitrox.Server.Subnautica/Models/Helper/EasyPool.cs new file mode 100644 index 0000000000..1699267421 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Helper/EasyPool.cs @@ -0,0 +1,19 @@ +using System.Buffers; + +namespace Nitrox.Server.Subnautica.Models.Helper; + +internal static class EasyPool +{ + private static readonly ArrayPool pool = ArrayPool.Create(1, 1); + + public static Lease Rent() => new(pool.Rent(1)); + + internal readonly struct Lease(T[] rentedArray) : IDisposable + { + private readonly T[] rentedArray = rentedArray; + + public ref T GetRef() => ref rentedArray[0]; + + public void Dispose() => pool.Return(rentedArray); + } +} diff --git a/Nitrox.Server.Subnautica/Models/Helper/XorRandom.cs b/Nitrox.Server.Subnautica/Models/Helper/XorRandom.cs index 22c3aa6309..e511f7b736 100644 --- a/Nitrox.Server.Subnautica/Models/Helper/XorRandom.cs +++ b/Nitrox.Server.Subnautica/Models/Helper/XorRandom.cs @@ -3,138 +3,136 @@ namespace Nitrox.Server.Subnautica.Models.Helper; /// -/// Stolen from -/// Aims at replicating UnityEngine.Random's implementation which is really uncommon because its uniform. +/// Aims at replicating UnityEngine.Random's implementation to better match behavior of UWE games. /// -public static class XorRandom +/// +/// Inspired by +/// +internal sealed class XorRandom { - private static uint x; - private static uint y; - private static uint z; - private static uint w; - private const uint MT19937 = 1812433253; + private uint w; + private uint x; + private uint y; + private uint z; /// - /// Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from Mersenne Twister (MT19937) - /// https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization + /// Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from + /// Mersenne Twister (MT19937) + /// https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization /// - public static void InitSeed(int seed) + public XorRandom(int seed) { x = (uint)seed; - y = (MT19937 * x + 1); - z = (MT19937 * y + 1); - w = (MT19937 * z + 1); + y = MT19937 * x + 1; + z = MT19937 * y + 1; + w = MT19937 * z + 1; } - /// - /// Explicitly set the state parameters - /// - public static void InitState(uint initial_x, uint initial_y, uint initial_z, uint initial_w) - { - x = initial_x; - y = initial_y; - z = initial_x; - w = initial_w; - } - - public static uint XORShift() + public uint XorShift() { uint t = x ^ (x << 11); - x = y; y = z; z = w; + x = y; + y = z; + z = w; return w = w ^ (w >> 19) ^ t ^ (t >> 8); } - /// - /// Alias of base Next/XORShift. - /// UnityEngine.Random doesn't have any uint functions so these functions behave exactly like int Random.Range + /// Alias of base Next/XORShift. + /// UnityEngine.Random doesn't have any uint functions so these functions behave exactly like int Random.Range /// - public static uint NextUInt() + public uint NextUInt() { - return XORShift(); + return XorShift(); } /// - /// A random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive) + /// A random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive) /// - public static uint NextUIntMax(uint max) + public uint NextUIntMax(uint max) { - if (max == 0) return 0; - return XORShift() % max; + if (max == 0) + { + return 0; + } + return XorShift() % max; } /// - /// A random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive) + /// A random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive) /// - public static uint NextUIntRange(uint min, uint max) + public uint NextUIntRange(uint min, uint max) { - if (max - min == 0) return min; - + if (max - min == 0) + { + return min; + } if (max < min) - return min - XORShift() % (max + min); - else - return min + XORShift() % (max - min); + { + return min - XorShift() % (max + min); + } + return min + XorShift() % (max - min); } /// - /// A random signed 32-bit integer value in the range -2,147,483,648 (inclusive) to 2,147,483,647 (inclusive) + /// A random signed 32-bit integer value in the range -2,147,483,648 (inclusive) to 2,147,483,647 (inclusive) /// - public static int NextInt() + public int NextInt() { - return (int)(XORShift() % int.MaxValue); + return (int)(XorShift() % int.MaxValue); } - public static int NextIntMax(int max) + public int NextIntMax(int max) { return NextInt() % max; } /// - /// A random signed 32-bit integer value in the range min (inclusive) to max (exclusive) + /// A random signed 32-bit integer value in the range min (inclusive) to max (exclusive) /// /// - /// If you only need to generate positive integers, use NextUIntRange instead + /// If you only need to generate positive integers, use NextUIntRange instead /// - public static int NextIntRange(int min, int max) + public int NextIntRange(int min, int max) { // If max and min are the same, just return min since it will result in a DivideByZeroException - if (max - min == 0) return min; + if (max - min == 0) + { + return min; + } // Do operations in Int64 to prevent overflow that might be caused by any of the following operations // I'm sure there's a faster/better way to do this and avoid casting, but we prefer equivalence to Unity over performance long minLong = min; long maxLong = max; - long r = XORShift(); + long r = XorShift(); // Flip the first operator if the max is lower than the min, if (max < min) { return (int)(minLong - r % (maxLong - minLong)); } - else - { - return (int)(minLong + r % (maxLong - minLong)); - } + return (int)(minLong + r % (maxLong - minLong)); } /// - /// A random floating point between 0.0 and 1.0 (inclusive?) + /// A random floating point between 0.0 and 1.0 (inclusive?) /// - public static float NextFloat() + public float NextFloat() { return 1.0f - NextFloatRange(0.0f, 1.0f); } /// - /// A random floating point between min (inclusive) and max (exclusive) + /// A random floating point between min (inclusive) and max (exclusive) /// - public static float NextFloatRange(float min, float max) + public float NextFloatRange(float min, float max) { - return (min - max) * ((float)(XORShift() << 9) / 0xFFFFFFFF) + max; + return (min - max) * ((float)(XorShift() << 9) / 0xFFFFFFFF) + max; } - public static NitroxVector3 NextInsideSphere(float radius = 1f) + public NitroxVector3 NextInsideSphere(float radius = 1f) { NitroxVector3 pointInUnitSphere = new NitroxVector3(NextFloat(), NextFloat(), NextFloat()).Normalized; return pointInUnitSphere * NextFloatRange(0, radius); diff --git a/Nitrox.Server.Subnautica/Models/Logging/Middleware/WriteRedactedLogLoggerMiddleware.cs b/Nitrox.Server.Subnautica/Models/Logging/Middleware/WriteRedactedLogLoggerMiddleware.cs index 3a0da16f18..ddd5777d56 100644 --- a/Nitrox.Server.Subnautica/Models/Logging/Middleware/WriteRedactedLogLoggerMiddleware.cs +++ b/Nitrox.Server.Subnautica/Models/Logging/Middleware/WriteRedactedLogLoggerMiddleware.cs @@ -56,7 +56,7 @@ public void ExecuteLogMiddleware(ref ILoggerMiddleware.Context context, ILoggerM foreach (ValueMatch match in ParameterTagRegex.EnumerateMatches(originalFormat)) { // Write text before current match first. - Range beforeRange = lastMatch ?? new Range(0, match.Index); + Range beforeRange = lastMatch != null ? new Range(lastMatch.Value.End, match.Index) : new Range(0, match.Index); if (!beforeRange.IsEmpty()) { context.Writer.Write(originalFormat[beforeRange]); diff --git a/Nitrox.Server.Subnautica/Models/Logging/Redaction/Core/IRedactor.cs b/Nitrox.Server.Subnautica/Models/Logging/Redaction/Core/IRedactor.cs index 127b3baf5f..fcdcd3997b 100644 --- a/Nitrox.Server.Subnautica/Models/Logging/Redaction/Core/IRedactor.cs +++ b/Nitrox.Server.Subnautica/Models/Logging/Redaction/Core/IRedactor.cs @@ -3,7 +3,7 @@ namespace Nitrox.Server.Subnautica.Models.Logging.Redaction.Core; internal interface IRedactor { /// - /// The structured log names that this redactor will try to redact. + /// The structured log names that this redactor will try to redact. This is case-insensitive. /// string[] RedactableKeys { get; } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/AnonProcessorContext.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/AnonProcessorContext.cs new file mode 100644 index 0000000000..3f8fb1eeb9 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/AnonProcessorContext.cs @@ -0,0 +1,24 @@ +using System.Net; +using Nitrox.Model.Core; +using Nitrox.Model.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +/// +/// Context used by . +/// +internal record AnonProcessorContext : IPacketProcessContext<(SessionId SessionId, IPEndPoint EndPoint)> +{ + private readonly IPacketSender packetSender; + public (SessionId SessionId, IPEndPoint EndPoint) Sender { get; set; } + + public AnonProcessorContext((SessionId SessionId, IPEndPoint EndPoint) sender, IPacketSender packetSender) + { + this.packetSender = packetSender; + Sender = sender; + } + + public async Task ReplyAsync(T packet) where T : Packet => await packetSender.SendPacketAsync(packet, Sender.SessionId); + + public async Task SendToAllAsync(T packet) where T : Packet => await packetSender.SendPacketToAllAsync(packet); +} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/AuthProcessorContext.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/AuthProcessorContext.cs new file mode 100644 index 0000000000..0f058b39d7 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/AuthProcessorContext.cs @@ -0,0 +1,27 @@ +using Nitrox.Model.Core; +using Nitrox.Model.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +/// +/// Context used by . +/// +internal record AuthProcessorContext : IPacketProcessContext +{ + private readonly IPacketSender packetSender; + public Player Sender { get; set; } + + public AuthProcessorContext(Player sender, IPacketSender packetSender) + { + this.packetSender = packetSender; + Sender = sender; + } + + public async Task SendAsync(T packet, SessionId sessionId) where T : Packet => await packetSender.SendPacketAsync(packet, sessionId); + + public async Task ReplyAsync(T packet) where T : Packet => await packetSender.SendPacketAsync(packet, Sender.SessionId); + + public async Task SendToAllAsync(T packet) where T : Packet => await packetSender.SendPacketToAllAsync(packet); + + public async Task SendToOthersAsync(T packet) where T : Packet => await packetSender.SendPacketToOthersAsync(packet, Sender.SessionId); +} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/IAnonPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/IAnonPacketProcessor.cs new file mode 100644 index 0000000000..af47c2d4a9 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/IAnonPacketProcessor.cs @@ -0,0 +1,11 @@ +using Nitrox.Model.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +internal interface IAnonPacketProcessor : IPacketProcessor; + +/// +/// A server packet processor that accepts anonymous connections. +/// +/// The packet type this processor can handle. +internal interface IAnonPacketProcessor : IAnonPacketProcessor, IPacketProcessor where TPacket : Packet; diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/IAuthPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/IAuthPacketProcessor.cs new file mode 100644 index 0000000000..c684339519 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/IAuthPacketProcessor.cs @@ -0,0 +1,11 @@ +using Nitrox.Model.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +internal interface IAuthPacketProcessor : IPacketProcessor; + +/// +/// A server packet processor that handles a packet type for authenticated connections. +/// +/// The packet type this processor can handle. +internal interface IAuthPacketProcessor : IAuthPacketProcessor, IPacketProcessor where TPacket : Packet; diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/IPacketSender.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/IPacketSender.cs new file mode 100644 index 0000000000..71c47c0885 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/IPacketSender.cs @@ -0,0 +1,13 @@ +using Nitrox.Model.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +internal interface IPacketSender +{ + /// + /// Sends a packet to the given session id, if still connected. + /// + ValueTask SendPacketAsync(T packet, SessionId sessionId) where T : Packet; + ValueTask SendPacketToAllAsync(T packet) where T : Packet; + ValueTask SendPacketToOthersAsync(T packet, SessionId excludedSessionId) where T : Packet; +} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Core/NopPacketSender.cs b/Nitrox.Server.Subnautica/Models/Packets/Core/NopPacketSender.cs new file mode 100644 index 0000000000..e575241857 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Core/NopPacketSender.cs @@ -0,0 +1,12 @@ +using Nitrox.Model.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Core; + +internal sealed class NopPacketSender : IPacketSender +{ + public ValueTask SendPacketAsync(T packet, SessionId sessionId) where T : Packet => ValueTask.CompletedTask; + + public ValueTask SendPacketToAllAsync(T packet) where T : Packet => ValueTask.CompletedTask; + + public ValueTask SendPacketToOthersAsync(T packet, SessionId excludedSessionId) where T : Packet => ValueTask.CompletedTask; +} diff --git a/Nitrox.Server.Subnautica/Models/Packets/PacketHandler.cs b/Nitrox.Server.Subnautica/Models/Packets/PacketHandler.cs deleted file mode 100644 index dceda4110b..0000000000 --- a/Nitrox.Server.Subnautica/Models/Packets/PacketHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections.Generic; -using Nitrox.Model.Core; -using Nitrox.Model.Packets.Processors.Abstract; -using Nitrox.Server.Subnautica.Models.Communication; -using Nitrox.Server.Subnautica.Models.Packets.Processors; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; - -namespace Nitrox.Server.Subnautica.Models.Packets -{ - internal sealed class PacketHandler - { - private readonly PlayerManager playerManager; - private readonly DefaultServerPacketProcessor defaultServerPacketProcessor; - private readonly ILogger logger; - private readonly Dictionary packetProcessorAuthCache = new(); - private readonly Dictionary packetProcessorUnauthCache = new(); - - public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor packetProcessor, ILogger logger) - { - this.playerManager = playerManager; - defaultServerPacketProcessor = packetProcessor; - this.logger = logger; - } - - public void Process(Packet packet, INitroxConnection connection) - { - Player player = playerManager.GetPlayer(connection); - if (player == null) - { - ProcessUnauthenticated(packet, connection); - } - else - { - ProcessAuthenticated(packet, player); - } - } - - private void ProcessAuthenticated(Packet packet, Player player) - { - Type packetType = packet.GetType(); - if (!packetProcessorAuthCache.TryGetValue(packetType, out PacketProcessor processor)) - { - Type packetProcessorType = typeof(AuthenticatedPacketProcessor<>).MakeGenericType(packetType); - packetProcessorAuthCache[packetType] = processor = NitroxServiceLocator.LocateOptionalService(packetProcessorType).Value as PacketProcessor; - } - - if (processor != null) - { - try - { - processor.ProcessPacket(packet, player); - } - catch (Exception ex) - { - logger.ZLogError(ex, $"Error in packet processor {processor.GetType()}"); - } - } - else - { - defaultServerPacketProcessor.ProcessPacket(packet, player); - } - } - - private void ProcessUnauthenticated(Packet packet, INitroxConnection connection) - { - Type packetType = packet.GetType(); - if (!packetProcessorUnauthCache.TryGetValue(packetType, out PacketProcessor processor)) - { - Type packetProcessorType = typeof(UnauthenticatedPacketProcessor<>).MakeGenericType(packetType); - packetProcessorUnauthCache[packetType] = processor = NitroxServiceLocator.LocateOptionalService(packetProcessorType).Value as PacketProcessor; - } - if (processor == null) - { - logger.ZLogWarning($"Received invalid, unauthenticated packet: {packet}"); - return; - } - - try - { - processor.ProcessPacket(packet, connection); - } - catch (Exception ex) - { - logger.ZLogError(ex, $"Error in packet processor {processor.GetType()}"); - } - } - } -} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs index b6c30ca3e9..1deedf550a 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs @@ -1,13 +1,14 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; +using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class AggressiveWhenSeeTargetChangedProcessor( - PlayerManager playerManager, - EntityRegistry entityRegistry -) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) +internal sealed class AggressiveWhenSeeTargetChangedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(AggressiveWhenSeeTargetChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, AggressiveWhenSeeTargetChanged packet) + { + await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId, packet.TargetId]); + } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/AnimationChangeEventProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/AnimationChangeEventProcessor.cs index d084c8deff..0384832af0 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/AnimationChangeEventProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/AnimationChangeEventProcessor.cs @@ -1,24 +1,15 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; /// /// Broadcasts and stores the animation state of a player /// -internal sealed class AnimationChangeEventProcessor : AuthenticatedPacketProcessor +internal sealed class AnimationChangeEventProcessor : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - - public AnimationChangeEventProcessor(PlayerManager playerManager) + public async Task Process(AuthProcessorContext context, AnimationChangeEvent packet) { - this.playerManager = playerManager; - } - - public override void Process(AnimationChangeEvent packet, Player player) - { - player.PlayerContext.Animation = packet.Animation; - - playerManager.SendPacketToOtherPlayers(packet, player); + context.Sender.PlayerContext.Animation = packet.Animation; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs index 0633600a46..ab7005e52e 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs @@ -1,13 +1,14 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; +using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class AttackCyclopsTargetChangedProcessor( - PlayerManager playerManager, - EntityRegistry entityRegistry -) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) +internal sealed class AttackCyclopsTargetChangedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(AttackCyclopsTargetChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, AttackCyclopsTargetChanged packet) + { + await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId, packet.TargetId]); + } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/BaseDeconstructedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/BaseDeconstructedProcessor.cs index 306bdf3d42..6b023ee9e9 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/BaseDeconstructedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/BaseDeconstructedProcessor.cs @@ -1,17 +1,15 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class BaseDeconstructedProcessor : BuildingProcessor +internal sealed class BaseDeconstructedProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public BaseDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(BaseDeconstructed packet, Player player) + public override async Task Process(AuthProcessorContext context, BaseDeconstructed packet) { - if (buildingManager.ReplaceBaseByGhost(packet)) + if (BuildingManager.ReplaceBaseByGhost(packet)) { - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/BedEnterProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/BedEnterProcessor.cs index f3476025ca..0bc4788568 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/BedEnterProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/BedEnterProcessor.cs @@ -1,19 +1,15 @@ using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class BedEnterProcessor : AuthenticatedPacketProcessor +internal sealed class BedEnterProcessor(SleepManager sleepManager) : IAuthPacketProcessor { - private readonly SleepManager sleepManager; + private readonly SleepManager sleepManager = sleepManager; - public BedEnterProcessor(SleepManager sleepManager) + public Task Process(AuthProcessorContext context, BedEnter packet) { - this.sleepManager = sleepManager; - } - - public override void Process(BedEnter packet, Player player) - { - sleepManager.PlayerEnteredBed(player); + sleepManager.PlayerEnteredBed(context.Sender); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/BedExitProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/BedExitProcessor.cs index f23d399fd7..ff9d25212d 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/BedExitProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/BedExitProcessor.cs @@ -1,19 +1,15 @@ using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class BedExitProcessor : AuthenticatedPacketProcessor +internal sealed class BedExitProcessor(SleepManager sleepManager) : IAuthPacketProcessor { - private readonly SleepManager sleepManager; + private readonly SleepManager sleepManager = sleepManager; - public BedExitProcessor(SleepManager sleepManager) + public Task Process(AuthProcessorContext context, BedExit packet) { - this.sleepManager = sleepManager; - } - - public override void Process(BedExit packet, Player player) - { - sleepManager.PlayerExitedBed(player); + sleepManager.PlayerExitedBed(context.Sender); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingProcessor.cs index 35410f9f77..e4fcfadc29 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingProcessor.cs @@ -1,45 +1,40 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal abstract class BuildingProcessor : AuthenticatedPacketProcessor where T : Packet +internal abstract class BuildingProcessor(BuildingManager buildingManager, EntitySimulation? entitySimulation = null) : IAuthPacketProcessor + where T : Packet { - internal readonly BuildingManager buildingManager; - internal readonly PlayerManager playerManager; - internal readonly EntitySimulation entitySimulation; + protected readonly BuildingManager BuildingManager = buildingManager; + private readonly EntitySimulation? entitySimulation = entitySimulation; - public BuildingProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation = null) - { - this.buildingManager = buildingManager; - this.playerManager = playerManager; - this.entitySimulation = entitySimulation; - } + public abstract Task Process(AuthProcessorContext context, T packet); - public void SendToOtherPlayersWithOperationId(T packet, Player player, int operationId) + protected async Task SendToOtherPlayersWithOperationIdAsync(AuthProcessorContext context, T packet, int operationId) { if (packet is OrderedBuildPacket buildPacket) { buildPacket.OperationId = operationId; } - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } /// - /// Attempts to acquire simulation ownership on for . - /// If the attempt is successful, it will be notified to all players. - /// Otherwise, nothing will happen. + /// Attempts to acquire simulation ownership on for . + /// If the attempt is successful, it will be notified to all players. + /// Otherwise, nothing will happen. /// - public void TryClaimBuildPiece(Entity entity, Player player) + protected async Task TryClaimBuildPieceAsync(AuthProcessorContext context, Entity entity) { - if (entitySimulation.TryAssignEntityToPlayer(entity, player, false, out SimulatedEntity simulatedEntity)) + ArgumentNullException.ThrowIfNull(entitySimulation); + if (entitySimulation.TryAssignEntityToPlayer(entity, context.Sender, false, out SimulatedEntity simulatedEntity)) { SimulationOwnershipChange ownershipChangePacket = new(simulatedEntity); - playerManager.SendPacketToAllPlayers(ownershipChangePacket); + await context.SendToAllAsync(ownershipChangePacket); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingResyncRequestProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingResyncRequestProcessor.cs index 9edf7d30b2..557ade1901 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingResyncRequestProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/BuildingResyncRequestProcessor.cs @@ -2,23 +2,17 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class BuildingResyncRequestProcessor : AuthenticatedPacketProcessor +sealed class BuildingResyncRequestProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly EntityRegistry entityRegistry; - private readonly WorldEntityManager worldEntityManager; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public BuildingResyncRequestProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager) - { - this.entityRegistry = entityRegistry; - this.worldEntityManager = worldEntityManager; - } - - public override void Process(BuildingResyncRequest packet, Player player) + public async Task Process(AuthProcessorContext context, BuildingResyncRequest packet) { Dictionary buildEntities = new(); Dictionary moduleEntities = new(); @@ -47,6 +41,6 @@ void AddEntityToResync(Entity entity) AddEntityToResync(entity); } - player.SendPacket(new BuildingResync(buildEntities, moduleEntities)); + await context.ReplyAsync(new BuildingResync(buildEntities, moduleEntities)); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CellVisibilityChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CellVisibilityChangedProcessor.cs index afe7016c15..508d345913 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CellVisibilityChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CellVisibilityChangedProcessor.cs @@ -2,35 +2,29 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class CellVisibilityChangedProcessor : AuthenticatedPacketProcessor +sealed class CellVisibilityChangedProcessor(EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly EntitySimulation entitySimulation; - private readonly WorldEntityManager worldEntityManager; + private readonly EntitySimulation entitySimulation = entitySimulation; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public CellVisibilityChangedProcessor(EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) + public async Task Process(AuthProcessorContext context, CellVisibilityChanged packet) { - this.entitySimulation = entitySimulation; - this.worldEntityManager = worldEntityManager; - } - - public override void Process(CellVisibilityChanged packet, Player player) - { - player.AddCells(packet.Added); - player.RemoveCells(packet.Removed); + context.Sender.AddCells(packet.Added); + context.Sender.RemoveCells(packet.Removed); List totalEntities = []; List totalSimulationChanges = []; foreach (AbsoluteEntityCell addedCell in packet.Added) { - worldEntityManager.LoadUnspawnedEntities(addedCell.BatchId, false); + await worldEntityManager.LoadUnspawnedEntitiesAsync(addedCell.BatchId, false); - totalSimulationChanges.AddRange(entitySimulation.GetSimulationChangesForCell(player, addedCell)); + totalSimulationChanges.AddRange(entitySimulation.GetSimulationChangesForCell(context.Sender, addedCell)); List newEntities = worldEntityManager.GetEntities(addedCell); totalEntities.AddRange(newEntities); @@ -38,7 +32,7 @@ public override void Process(CellVisibilityChanged packet, Player player) foreach (AbsoluteEntityCell removedCell in packet.Removed) { - entitySimulation.FillWithRemovedCells(player, removedCell, totalSimulationChanges); + entitySimulation.FillWithRemovedCells(context.Sender, removedCell, totalSimulationChanges); } // Simulation update must be broadcasted before the entities are spawned @@ -47,7 +41,7 @@ public override void Process(CellVisibilityChanged packet, Player player) entitySimulation.BroadcastSimulationChanges(new(totalSimulationChanges)); } - // We send this data whether or not it's empty because the client needs to know about it (see Terrain) - player.SendPacket(new SpawnEntities(totalEntities, packet.Added, true)); + // We send this data whether it's empty because the client needs to know about it (see Terrain) + await context.ReplyAsync(new SpawnEntities(totalEntities, packet.Added, true)); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/ChatMessageProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/ChatMessageProcessor.cs index 105610e4ae..0d79ac4335 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/ChatMessageProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/ChatMessageProcessor.cs @@ -1,28 +1,20 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Model.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - sealed class ChatMessageProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public ChatMessageProcessor(PlayerManager playerManager, ILogger logger) - { - this.playerManager = playerManager; - this.logger = logger; - } +internal sealed class ChatMessageProcessor(ILogger logger) : IAuthPacketProcessor +{ + private readonly ILogger logger = logger; - public override void Process(ChatMessage packet, Player player) + public async Task Process(AuthProcessorContext context, ChatMessage packet) + { + if (context.Sender.PlayerContext.IsMuted) { - if (player.PlayerContext.IsMuted) - { - player.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, "You're currently muted")); - return; - } - logger.ZLogInformation($"<{player.Name}>: {packet.Text}"); - playerManager.SendPacketToAllPlayers(packet); + await context.ReplyAsync(new ChatMessage(SessionId.SERVER_ID, "You're currently muted")); + return; } + logger.ZLogInformation($"<{context.Sender.Name}>: {packet.Text}"); + await context.SendToAllAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CheatCommandProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CheatCommandProcessor.cs index fb681634f3..78c4012fd4 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CheatCommandProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CheatCommandProcessor.cs @@ -1,18 +1,18 @@ using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class CheatCommandProcessor(ILogger logger) : AuthenticatedPacketProcessor +internal sealed class CheatCommandProcessor(ILogger logger) : IAuthPacketProcessor { - public override void Process(CheatCommand packet, Player player) + public async Task Process(AuthProcessorContext context, CheatCommand packet) { - if (player.Permissions < Perms.MODERATOR) + if (context.Sender.Permissions < Perms.MODERATOR) { - logger.ZLogWarning($"{player.Name} used cheat command: '{packet.Command}' without sufficient permissions."); + logger.ZLogWarning($"{context.Sender.Name} used cheat command: '{packet.Command}' without sufficient permissions."); return; } - logger.ZLogInformation($"{player.Name} used cheat command: '{packet.Command}'"); + logger.ZLogInformation($"{context.Sender.Name} used cheat command: '{packet.Command}'"); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/ClearPlanterProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/ClearPlanterProcessor.cs index 6fde405637..46b01c8593 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/ClearPlanterProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/ClearPlanterProcessor.cs @@ -1,23 +1,24 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class ClearPlanterProcessor(EntityRegistry entityRegistry, ILogger logger) : AuthenticatedPacketProcessor +internal sealed class ClearPlanterProcessor(EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { private readonly EntityRegistry entityRegistry = entityRegistry; private readonly ILogger logger = logger; - public override void Process(ClearPlanter packet, Player player) + public Task Process(AuthProcessorContext context, ClearPlanter packet) { if (!entityRegistry.TryGetEntityById(packet.PlanterId, out PlanterEntity planterEntity)) { logger.ZLogErrorOnce($"could not find {nameof(PlanterEntity)} with id {packet.PlanterId}"); - return; + return Task.CompletedTask; } // No need to transmit this packet since the operation is automatically done on remote clients entityRegistry.CleanChildren(planterEntity); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/AuthenticatedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/AuthenticatedPacketProcessor.cs deleted file mode 100644 index bda375402e..0000000000 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/AuthenticatedPacketProcessor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Nitrox.Model.Packets.Processors.Abstract; - -namespace Nitrox.Server.Subnautica.Models.Packets.Processors.Core -{ - public abstract class AuthenticatedPacketProcessor : PacketProcessor where T : Packet - { - public override void ProcessPacket(Packet packet, IProcessorContext player) - { - Process((T)packet, (Player)player); - } - - public abstract void Process(T packet, Player player); - } -} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/TransmitIfCanSeePacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/TransmitIfCanSeePacketProcessor.cs index 9269a859bb..53aa6e2a7b 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/TransmitIfCanSeePacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/Core/TransmitIfCanSeePacketProcessor.cs @@ -4,25 +4,21 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -internal abstract class TransmitIfCanSeePacketProcessor : AuthenticatedPacketProcessor where T : Packet +internal abstract class TransmitIfCanSeePacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) : IAuthPacketProcessor + where T : Packet { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - - public TransmitIfCanSeePacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) - { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - } + private readonly PlayerManager playerManager = playerManager; + private readonly EntityRegistry entityRegistry = entityRegistry; /// /// Transmits the provided to all other players (excluding ) /// who can see () entities corresponding to the provided only if all those entities are registered. /// - public void TransmitIfCanSeeEntities(Packet packet, Player senderPlayer, List entityIds) + protected async Task TransmitIfCanSeeEntitiesAsync(AuthProcessorContext context, Packet packet, List entityIds) { List entities = []; foreach (NitroxId entityId in entityIds) @@ -37,12 +33,14 @@ public void TransmitIfCanSeeEntities(Packet packet, Player senderPlayer, List : PacketProcessor where T : Packet - { - public override void ProcessPacket(Packet packet, IProcessorContext connection) - { - Process((T)packet, (INitroxConnection)connection); - } - - public abstract void Process(T packet, INitroxConnection connection); - } -} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CreatureActionChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CreatureActionChangedProcessor.cs index 60ab64a94e..d85cfdfaee 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CreatureActionChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CreatureActionChangedProcessor.cs @@ -1,13 +1,11 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class CreatureActionChangedProcessor( - PlayerManager playerManager, - EntityRegistry entityRegistry -) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) +internal sealed class CreatureActionChangedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(CreatureActionChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]); + public override async Task Process(AuthProcessorContext context, CreatureActionChanged packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CreaturePoopPerformedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CreaturePoopPerformedProcessor.cs index 8e6e5e6010..803e5a7ed8 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CreaturePoopPerformedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CreaturePoopPerformedProcessor.cs @@ -1,13 +1,11 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class CreaturePoopPerformedProcessor( - PlayerManager playerManager, - EntityRegistry entityRegistry -) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) +internal sealed class CreaturePoopPerformedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(CreaturePoopPerformed packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]); + public override async Task Process(AuthProcessorContext context, CreaturePoopPerformed packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamagePointRepairedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamagePointRepairedProcessor.cs index c722d1e370..68c3a12702 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamagePointRepairedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamagePointRepairedProcessor.cs @@ -1,20 +1,11 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class CyclopsDamagePointRepairedProcessor : IAuthPacketProcessor { - class CyclopsDamagePointRepairedProcessor : AuthenticatedPacketProcessor + public async Task Process(AuthProcessorContext context, CyclopsDamagePointRepaired packet) { - private readonly PlayerManager playerManager; - - public CyclopsDamagePointRepairedProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } - - public override void Process(CyclopsDamagePointRepaired packet, Player simulatingPlayer) - { - playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamageProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamageProcessor.cs index 4d12235348..abbc73be1a 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamageProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsDamageProcessor.cs @@ -1,27 +1,18 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - /// - /// This is the absolute damage state. The current simulation owner is the only one who sends this packet to the server - /// - sealed class CyclopsDamageProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public CyclopsDamageProcessor(PlayerManager playerManager, ILogger logger) - { - this.playerManager = playerManager; - this.logger = logger; - } +/// +/// This is the absolute damage state. The current simulation owner is the only one who sends this packet to the server +/// +internal sealed class CyclopsDamageProcessor(ILogger logger) : IAuthPacketProcessor +{ + private readonly ILogger logger = logger; - public override void Process(CyclopsDamage packet, Player simulatingPlayer) - { - logger.ZLogDebug($"New cyclops damage from player #{simulatingPlayer.Id}: {packet}"); + public async Task Process(AuthProcessorContext context, CyclopsDamage packet) + { + logger.ZLogDebug($"New cyclops damage from player #{context.Sender.SessionId}: {packet}"); - playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsFireCreatedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsFireCreatedProcessor.cs index 2e5642b15b..aaf9835aa2 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsFireCreatedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/CyclopsFireCreatedProcessor.cs @@ -1,20 +1,11 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class CyclopsFireCreatedProcessor : IAuthPacketProcessor { - class CyclopsFireCreatedProcessor : AuthenticatedPacketProcessor + public async Task Process(AuthProcessorContext context, CyclopsFireCreated packet) { - private readonly PlayerManager playerManager; - - public CyclopsFireCreatedProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } - - public override void Process(CyclopsFireCreated packet, Player simulatingPlayer) - { - playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/DefaultServerPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/DefaultServerPacketProcessor.cs index 34c45064c0..3e8eb13dc8 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/DefaultServerPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/DefaultServerPacketProcessor.cs @@ -1,16 +1,23 @@ using System.Collections.Generic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class DefaultServerPacketProcessor : AuthenticatedPacketProcessor +internal sealed class DefaultServerPacketProcessor(ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly ILogger logger; + /// + /// Packet types which don't have a server packet processor but should not be transmitted + /// + private readonly HashSet defaultPacketProcessorBlacklist = + [ + typeof(GameModeChanged), + typeof(DropSimulationOwnership) + ]; - private readonly HashSet loggingPacketBlackList = new() - { + private readonly ILogger logger = logger; + + private readonly HashSet loggingPacketBlackList = + [ typeof(AnimationChangeEvent), typeof(PlayerMovement), typeof(ItemPosition), @@ -28,34 +35,21 @@ sealed class DefaultServerPacketProcessor : AuthenticatedPacketProcessor typeof(StasisSphereHit), typeof(SeaTreaderChunkPickedUp), typeof(ToggleLights) - }; + ]; - /// - /// Packet types which don't have a server packet processor but should not be transmitted - /// - private readonly HashSet defaultPacketProcessorBlacklist = new() + public async Task Process(AuthProcessorContext context, Packet packet) { - typeof(GameModeChanged), typeof(DropSimulationOwnership), - }; - - public DefaultServerPacketProcessor(PlayerManager playerManager, ILogger logger) - { - this.playerManager = playerManager; - this.logger = logger; - } - - public override void Process(Packet packet, Player player) - { - if (!loggingPacketBlackList.Contains(packet.GetType())) + Type packetType = packet.GetType(); + if (!loggingPacketBlackList.Contains(packetType)) { - logger.ZLogDebug($"Using default packet processor for: {packet} and player {player.Id}"); + logger.ZLogDebug($"Using default packet processor for: {packet} and player #{context.Sender.SessionId}"); } - - if (defaultPacketProcessorBlacklist.Contains(packet.GetType())) + if (defaultPacketProcessorBlacklist.Contains(packetType)) { - logger.ZLogErrorOnce($"Player {player.Name} [{player.Id}] sent a packet which is blacklisted by the server. It's likely that the said player is using a modified version of Nitrox and action could be taken accordingly."); + logger.ZLogErrorOnce($"Player {context.Sender.Name} #{context.Sender.SessionId} sent a packet which is blacklisted by the server. It's likely that the said player is using a modified version of Nitrox and action could be taken accordingly."); return; } - playerManager.SendPacketToOtherPlayers(packet, player); + + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/DiscordRequestIPProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/DiscordRequestIPProcessor.cs index f8c5392d9e..e6ff1ed67f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/DiscordRequestIPProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/DiscordRequestIPProcessor.cs @@ -1,35 +1,29 @@ using System.Collections.Generic; using System.Net; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class DiscordRequestIPProcessor : AuthenticatedPacketProcessor +internal sealed class DiscordRequestIPProcessor(IOptions options, ILogger logger) : IAuthPacketProcessor { - private readonly IOptions options; - private readonly ILogger logger; + private readonly IOptions options = options; + private readonly ILogger logger = logger; private string ipPort; - public DiscordRequestIPProcessor(IOptions options, ILogger logger) - { - this.options = options; - this.logger = logger; - } - - public override void Process(DiscordRequestIP packet, Player player) + public async Task Process(AuthProcessorContext context, DiscordRequestIP packet) { if (string.IsNullOrEmpty(ipPort)) { - Task.Run(() => ProcessPacketAsync(packet, player)); + await ProcessPacketAsync(context, packet); return; } packet.IpPort = ipPort; - player.SendPacket(packet); + await context.ReplyAsync(packet); } - private async Task ProcessPacketAsync(DiscordRequestIP packet, Player player) + private async Task ProcessPacketAsync(AuthProcessorContext context, DiscordRequestIP packet) { string result = await GetIpAsync(); if (result == "") @@ -39,7 +33,7 @@ private async Task ProcessPacketAsync(DiscordRequestIP packet, Player player) } packet.IpPort = ipPort = $"{result}:{options.Value.ServerPort}"; - player.SendPacket(packet); + await context.ReplyAsync(packet); } /// diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs index ebeb024e0f..9aea3e4c55 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs @@ -1,25 +1,18 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class EntityDestroyedPacketProcessor : AuthenticatedPacketProcessor +internal sealed class EntityDestroyedPacketProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly EntitySimulation entitySimulation; - private readonly WorldEntityManager worldEntityManager; + private readonly PlayerManager playerManager = playerManager; + private readonly EntitySimulation entitySimulation = entitySimulation; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public EntityDestroyedPacketProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.entitySimulation = entitySimulation; - } - - public override void Process(EntityDestroyed packet, Player destroyingPlayer) + public async Task Process(AuthProcessorContext context, EntityDestroyed packet) { entitySimulation.EntityDestroyed(packet.Id); @@ -32,10 +25,10 @@ public override void Process(EntityDestroyed packet, Player destroyingPlayer) foreach (Player player in playerManager.GetConnectedPlayers()) { - bool isOtherPlayer = player != destroyingPlayer; + bool isOtherPlayer = player != context.Sender; if (isOtherPlayer && player.CanSee(entity)) { - player.SendPacket(packet); + await context.ReplyAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityMetadataUpdateProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityMetadataUpdateProcessor.cs index a6cb6ec202..d8c23cc8be 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityMetadataUpdateProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityMetadataUpdateProcessor.cs @@ -1,25 +1,18 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class EntityMetadataUpdateProcessor : AuthenticatedPacketProcessor +internal sealed class EntityMetadataUpdateProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - private readonly ILogger logger; + private readonly PlayerManager playerManager = playerManager; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly ILogger logger = logger; - public EntityMetadataUpdateProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, ILogger logger) - { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - this.logger = logger; - } - - public override void Process(EntityMetadataUpdate packet, Player sendingPlayer) + public async Task Process(AuthProcessorContext context, EntityMetadataUpdate packet) { if (!entityRegistry.TryGetEntityById(packet.Id, out Entity entity)) { @@ -27,22 +20,21 @@ public override void Process(EntityMetadataUpdate packet, Player sendingPlayer) return; } - if (TryProcessMetadata(sendingPlayer, entity, packet.NewValue)) + if (TryProcessMetadata(context.Sender, entity, packet.NewValue)) { entity.Metadata = packet.NewValue; - SendUpdateToVisiblePlayers(packet, sendingPlayer, entity); + await SendUpdateToVisiblePlayersAsync(context, packet, entity); } } - private void SendUpdateToVisiblePlayers(EntityMetadataUpdate packet, Player sendingPlayer, Entity entity) + private async Task SendUpdateToVisiblePlayersAsync(AuthProcessorContext context, EntityMetadataUpdate packet, Entity entity) { foreach (Player player in playerManager.GetConnectedPlayers()) { bool updateVisibleToPlayer = player.CanSee(entity); - - if (player != sendingPlayer && updateVisibleToPlayer) + if (player != context.Sender && updateVisibleToPlayer) { - player.SendPacket(packet); + await context.SendAsync(packet, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityReparentedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityReparentedProcessor.cs index 59b8c6ea03..1c27d2d77f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityReparentedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityReparentedProcessor.cs @@ -1,17 +1,15 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class EntityReparentedProcessor(EntityRegistry entityRegistry, PlayerManager playerManager, ILogger logger) : AuthenticatedPacketProcessor +internal sealed class EntityReparentedProcessor(EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { private readonly EntityRegistry entityRegistry = entityRegistry; - private readonly PlayerManager playerManager = playerManager; private readonly ILogger logger = logger; - public override void Process(EntityReparented packet, Player player) + public async Task Process(AuthProcessorContext context, EntityReparented packet) { if (!entityRegistry.TryGetEntityById(packet.Id, out Entity entity)) { @@ -25,6 +23,6 @@ public override void Process(EntityReparented packet, Player player) } entityRegistry.ReparentEntity(packet.Id, packet.NewParentId); - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntitySpawnedByClientProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntitySpawnedByClientProcessor.cs index bc9e005034..c13fec0ccd 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntitySpawnedByClientProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntitySpawnedByClientProcessor.cs @@ -1,55 +1,47 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class EntitySpawnedByClientProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, EntitySimulation entitySimulation) + : IAuthPacketProcessor { - class EntitySpawnedByClientProcessor : AuthenticatedPacketProcessor + private readonly PlayerManager playerManager = playerManager; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; + private readonly EntitySimulation entitySimulation = entitySimulation; + + public async Task Process(AuthProcessorContext context, EntitySpawnedByClient packet) { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - private readonly WorldEntityManager worldEntityManager; - private readonly EntitySimulation entitySimulation; + Entity entity = packet.Entity; + + // If the entity already exists in the registry, it is fine to update. This is a normal case as the player + // may have an item in their inventory (that the registry knows about) then wants to spawn it into the world. + entityRegistry.AddOrUpdate(entity); - public EntitySpawnedByClientProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, EntitySimulation entitySimulation) + SimulatedEntity simulatedEntity = null; + if (entity is WorldEntity worldEntity) { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - this.worldEntityManager = worldEntityManager; - this.entitySimulation = entitySimulation; + worldEntityManager.TrackEntityInTheWorld(worldEntity); } - public override void Process(EntitySpawnedByClient packet, Player playerWhoSpawned) + if (packet.RequireSimulation && entitySimulation.TryAssignEntityToPlayer(entity, context.Sender, true, out simulatedEntity)) { - Entity entity = packet.Entity; - - // If the entity already exists in the registry, it is fine to update. This is a normal case as the player - // may have an item in their inventory (that the registry knows about) then wants to spawn it into the world. - entityRegistry.AddOrUpdate(entity); - - SimulatedEntity simulatedEntity = null; - if (entity is WorldEntity worldEntity) - { - worldEntityManager.TrackEntityInTheWorld(worldEntity); - } - - if (packet.RequireSimulation && entitySimulation.TryAssignEntityToPlayer(entity, playerWhoSpawned, true, out simulatedEntity)) - { - SimulationOwnershipChange ownershipChangePacket = new(simulatedEntity); - playerManager.SendPacketToAllPlayers(ownershipChangePacket); - } + SimulationOwnershipChange ownershipChangePacket = new(simulatedEntity); + await context.SendToAllAsync(ownershipChangePacket); + } - SpawnEntities spawnEntities = new(entity, simulatedEntity, packet.RequireRespawn); - foreach (Player player in playerManager.GetConnectedPlayers()) + SpawnEntities spawnEntities = new(entity, simulatedEntity, packet.RequireRespawn); + foreach (Player player in playerManager.GetConnectedPlayers()) + { + bool isOtherPlayer = player != context.Sender; + if (isOtherPlayer && player.CanSee(entity)) { - bool isOtherPlayer = player != playerWhoSpawned; - if (isOtherPlayer && player.CanSee(entity)) - { - player.SendPacket(spawnEntities); - } + await context.SendAsync(spawnEntities, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityTransformUpdatesProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityTransformUpdatesProcessor.cs index f26be6c36f..e479164fa3 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityTransformUpdatesProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityTransformUpdatesProcessor.cs @@ -1,86 +1,75 @@ using System.Collections.Generic; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - class EntityTransformUpdatesProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly WorldEntityManager worldEntityManager; - private readonly SimulationOwnershipData simulationOwnershipData; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public EntityTransformUpdatesProcessor(PlayerManager playerManager, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData) - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.simulationOwnershipData = simulationOwnershipData; - } +internal sealed class EntityTransformUpdatesProcessor(PlayerManager playerManager, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData) : IAuthPacketProcessor +{ + private readonly PlayerManager playerManager = playerManager; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public override void Process(EntityTransformUpdates packet, Player simulatingPlayer) - { - Dictionary> visibleUpdatesByPlayer = InitializeVisibleUpdateMapWithOtherPlayers(simulatingPlayer); - AssignVisibleUpdatesToPlayers(simulatingPlayer, packet.Updates, visibleUpdatesByPlayer); - SendUpdatesToPlayers(visibleUpdatesByPlayer); - } + public async Task Process(AuthProcessorContext context, EntityTransformUpdates packet) + { + Dictionary> visibleUpdatesByPlayer = InitializeVisibleUpdateMapWithOtherPlayers(context.Sender); + AssignVisibleUpdatesToPlayers(context.Sender, packet.Updates, visibleUpdatesByPlayer); + await SendUpdatesToPlayersAsync(context, visibleUpdatesByPlayer); + } - private Dictionary> InitializeVisibleUpdateMapWithOtherPlayers(Player simulatingPlayer) + private Dictionary> InitializeVisibleUpdateMapWithOtherPlayers(Player simulatingPlayer) + { + Dictionary> visibleUpdatesByPlayer = []; + foreach (Player player in playerManager.GetConnectedPlayers()) { - Dictionary> visibleUpdatesByPlayer = new Dictionary>(); - - foreach (Player player in playerManager.GetConnectedPlayers()) + if (!player.Equals(simulatingPlayer)) { - if (!player.Equals(simulatingPlayer)) - { - visibleUpdatesByPlayer[player] = new List(); - } + visibleUpdatesByPlayer[player] = []; } - - return visibleUpdatesByPlayer; } + return visibleUpdatesByPlayer; + } - private void AssignVisibleUpdatesToPlayers(Player sendingPlayer, List updates, Dictionary> visibleUpdatesByPlayer) + private void AssignVisibleUpdatesToPlayers(Player sendingPlayer, List updates, Dictionary> visibleUpdatesByPlayer) + { + foreach (EntityTransformUpdates.EntityTransformUpdate update in updates) { - foreach (EntityTransformUpdates.EntityTransformUpdate update in updates) + if (!simulationOwnershipData.TryGetLock(update.Id, out SimulationOwnershipData.PlayerLock playerLock) || playerLock.Player != sendingPlayer) { - if (!simulationOwnershipData.TryGetLock(update.Id, out SimulationOwnershipData.PlayerLock playerLock) || playerLock.Player != sendingPlayer) - { - // This will happen pretty frequently when a player moves very fast (swimfast or maybe some more edge cases) so we can just ignore this - continue; - } + // This will happen pretty frequently when a player moves very fast (swimfast or maybe some more edge cases) so we can just ignore this + continue; + } - if (!worldEntityManager.TryUpdateEntityPosition(update.Id, update.Position, update.Rotation, out AbsoluteEntityCell currentCell, out WorldEntity worldEntity)) - { - // Normal behaviour if the entity was removed at the same time as someone trying to simulate a postion update. - // we log an info inside entityManager.UpdateEntityPosition just in case. - continue; - } + if (!worldEntityManager.TryUpdateEntityPosition(update.Id, update.Position, update.Rotation, out AbsoluteEntityCell currentCell, out WorldEntity worldEntity)) + { + // Normal behaviour if the entity was removed at the same time as someone trying to simulate a postion update. + // we log an info inside entityManager.UpdateEntityPosition just in case. + continue; + } - foreach (KeyValuePair> playerUpdates in visibleUpdatesByPlayer) + foreach (KeyValuePair> playerUpdates in visibleUpdatesByPlayer) + { + if (playerUpdates.Key.CanSee(worldEntity)) { - if (playerUpdates.Key.CanSee(worldEntity)) - { - playerUpdates.Value.Add(update); - } + playerUpdates.Value.Add(update); } } } + } - private void SendUpdatesToPlayers(Dictionary> visibleUpdatesByPlayer) + private async Task SendUpdatesToPlayersAsync(AuthProcessorContext context, Dictionary> visibleUpdatesByPlayer) + { + foreach (KeyValuePair> playerUpdates in visibleUpdatesByPlayer) { - foreach (KeyValuePair> playerUpdates in visibleUpdatesByPlayer) - { - Player player = playerUpdates.Key; - List updates = playerUpdates.Value; + Player player = playerUpdates.Key; + List updates = playerUpdates.Value; - if (updates.Count > 0) - { - EntityTransformUpdates updatesPacket = new EntityTransformUpdates(updates); - player.SendPacket(updatesPacket); - } + if (updates.Count > 0) + { + await context.SendAsync(new EntityTransformUpdates(updates), player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EscapePodChangedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EscapePodChangedPacketProcessor.cs index e765da97b9..c25e362de8 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EscapePodChangedPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EscapePodChangedPacketProcessor.cs @@ -1,16 +1,12 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class EscapePodChangedPacketProcessor(PlayerManager playerManager, ILogger logger) : AuthenticatedPacketProcessor +internal sealed class EscapePodChangedPacketProcessor : IAuthPacketProcessor { - private readonly PlayerManager playerManager = playerManager; - private readonly ILogger logger = logger; - - public override void Process(EscapePodChanged packet, Player player) + public async Task Process(AuthProcessorContext context, EscapePodChanged packet) { - player.SubRootId = packet.EscapePodId; - playerManager.SendPacketToOtherPlayers(packet, player); + context.Sender.SubRootId = packet.EscapePodId; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODAssetProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODAssetProcessor.cs index 85abb5fab8..a1084e2d11 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODAssetProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODAssetProcessor.cs @@ -1,18 +1,18 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class FMODAssetProcessor(PlayerManager playerManager, FmodService fmodService, ILogger logger) : AuthenticatedPacketProcessor +internal sealed class FMODAssetProcessor(PlayerManager playerManager, FmodService fmodService, ILogger logger) : IAuthPacketProcessor { private readonly PlayerManager playerManager = playerManager; private readonly FmodService fmodService = fmodService; private readonly ILogger logger = logger; - public override void Process(FMODAssetPacket packet, Player sendingPlayer) + public async Task Process(AuthProcessorContext context, FMODAssetPacket packet) { if (!fmodService.TryGetSoundData(packet.AssetPath, out SoundData soundData)) { @@ -23,10 +23,10 @@ public override void Process(FMODAssetPacket packet, Player sendingPlayer) foreach (Player player in playerManager.GetConnectedPlayers()) { float distance = NitroxVector3.Distance(player.Position, packet.Position); - if (player != sendingPlayer && (soundData.IsGlobal || player.SubRootId.Equals(sendingPlayer.SubRootId)) && distance <= soundData.Radius) + if (player != context.Sender && (soundData.IsGlobal || player.SubRootId.Equals(context.Sender.SubRootId)) && distance <= soundData.Radius) { packet.Volume = SoundHelper.CalculateVolume(distance, soundData.Radius, packet.Volume); - player.SendPacket(packet); + await context.SendAsync(packet, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODEventInstanceProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODEventInstanceProcessor.cs index d8769efa73..0165e478a5 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODEventInstanceProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/FMODEventInstanceProcessor.cs @@ -1,25 +1,18 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class FMODEventInstanceProcessor : AuthenticatedPacketProcessor +internal sealed class FMODEventInstanceProcessor(PlayerManager playerManager, FmodService fmodService, ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly FmodService fmodService; - private readonly ILogger logger; + private readonly PlayerManager playerManager = playerManager; + private readonly FmodService fmodService = fmodService; + private readonly ILogger logger = logger; - public FMODEventInstanceProcessor(PlayerManager playerManager, FmodService fmodService, ILogger logger) - { - this.playerManager = playerManager; - this.fmodService = fmodService; - this.logger = logger; - } - - public override void Process(FMODEventInstancePacket packet, Player sendingPlayer) + public async Task Process(AuthProcessorContext context, FMODEventInstancePacket packet) { if (!fmodService.TryGetSoundData(packet.AssetPath, out SoundData soundData)) { @@ -30,12 +23,12 @@ public override void Process(FMODEventInstancePacket packet, Player sendingPlaye foreach (Player player in playerManager.GetConnectedPlayers()) { float distance = NitroxVector3.Distance(player.Position, packet.Position); - if (player != sendingPlayer && - (soundData.IsGlobal || player.SubRootId.Equals(sendingPlayer.SubRootId)) && + if (player != context.Sender && + (soundData.IsGlobal || player.SubRootId.Equals(context.Sender.SubRootId)) && distance < soundData.Radius) { packet.Volume = SoundHelper.CalculateVolume(distance, soundData.Radius, packet.Volume); - player.SendPacket(packet); + await context.SendAsync(packet, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/FireDousedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/FireDousedProcessor.cs index 780e0dc333..32ce976631 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/FireDousedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/FireDousedProcessor.cs @@ -1,20 +1,11 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class FireDousedProcessor : IAuthPacketProcessor { - class FireDousedProcessor : AuthenticatedPacketProcessor + public async Task Process(AuthProcessorContext context, FireDoused packet) { - private readonly PlayerManager playerManager; - - public FireDousedProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } - - public override void Process(FireDoused packet, Player simulatingPlayer) - { - playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/FootstepPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/FootstepPacketProcessor.cs index 35d0d5a221..554adb160f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/FootstepPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/FootstepPacketProcessor.cs @@ -1,24 +1,18 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class FootstepPacketProcessor : AuthenticatedPacketProcessor +internal sealed class FootstepPacketProcessor(PlayerManager playerManager, FmodService fmodService) : IAuthPacketProcessor { + private readonly FmodService fmodService = fmodService; + private readonly PlayerManager playerManager = playerManager; private float footstepAudioRange; // To modify this value, modify the last value of the event:/player/footstep_precursor_base sound in the SoundWhitelist_Subnautica.csv file - private readonly PlayerManager playerManager; - private readonly FmodService fmodService; - public FootstepPacketProcessor(PlayerManager playerManager, FmodService fmodService) - { - this.playerManager = playerManager; - this.fmodService = fmodService; - } - - public override void Process(FootstepPacket footstepPacket, Player sendingPlayer) + public async Task Process(AuthProcessorContext context, FootstepPacket footstepPacket) { if (footstepAudioRange == 0f && fmodService.TryGetSoundData("event:/player/footstep_precursor_base", out SoundData soundData)) { @@ -27,14 +21,14 @@ public override void Process(FootstepPacket footstepPacket, Player sendingPlayer foreach (Player player in playerManager.GetConnectedPlayers()) { - if (NitroxVector3.Distance(player.Position, sendingPlayer.Position) >= footstepAudioRange || - player == sendingPlayer) + if (NitroxVector3.Distance(player.Position, context.Sender.Position) >= footstepAudioRange || + player == context.Sender) { continue; } - if(player.SubRootId.Equals(sendingPlayer.SubRootId)) + if (player.SubRootId.Equals(context.Sender.SubRootId)) { - player.SendPacket(footstepPacket); + await context.SendAsync(footstepPacket, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/GoalCompletedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/GoalCompletedProcessor.cs index 0d6334117d..0265e6f398 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/GoalCompletedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/GoalCompletedProcessor.cs @@ -1,11 +1,11 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class GoalCompletedProcessor : AuthenticatedPacketProcessor +internal sealed class GoalCompletedProcessor : IAuthPacketProcessor { - public override void Process(GoalCompleted packet, Player player) + public async Task Process(AuthProcessorContext context, GoalCompleted packet) { - player.PersonalCompletedGoalsWithTimestamp.Add(packet.CompletedGoal, packet.CompletionTime); + context.Sender.PersonalCompletedGoalsWithTimestamp.Add(packet.CompletedGoal, packet.CompletionTime); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/KnownTechEntryAddProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/KnownTechEntryAddProcessor.cs index c115242fee..5d20398803 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/KnownTechEntryAddProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/KnownTechEntryAddProcessor.cs @@ -1,32 +1,24 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class KnownTechEntryAddProcessor(PdaManager pdaManager) : IAuthPacketProcessor { - internal sealed class KnownTechEntryAddProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly PdaManager pdaManager; + private readonly PdaManager pdaManager = pdaManager; - public KnownTechEntryAddProcessor(PlayerManager playerManager, PdaManager pdaManager) + public async Task Process(AuthProcessorContext context, KnownTechEntryAdd packet) + { + switch (packet.Category) { - this.playerManager = playerManager; - this.pdaManager = pdaManager; + case KnownTechEntryAdd.EntryCategory.KNOWN: + pdaManager.AddKnownTechType(packet.TechType, packet.PartialTechTypesToRemove); + break; + case KnownTechEntryAdd.EntryCategory.ANALYZED: + pdaManager.AddAnalyzedTechType(packet.TechType); + break; } - public override void Process(KnownTechEntryAdd packet, Player player) - { - switch (packet.Category) - { - case KnownTechEntryAdd.EntryCategory.KNOWN: - pdaManager.AddKnownTechType(packet.TechType, packet.PartialTechTypesToRemove); - break; - case KnownTechEntryAdd.EntryCategory.ANALYZED: - pdaManager.AddAnalyzedTechType(packet.TechType); - break; - } - - playerManager.SendPacketToOtherPlayers(packet, player); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/LargeWaterParkDeconstructedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/LargeWaterParkDeconstructedProcessor.cs index 814f99a131..db2ed53184 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/LargeWaterParkDeconstructedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/LargeWaterParkDeconstructedProcessor.cs @@ -1,21 +1,19 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class LargeWaterParkDeconstructedProcessor : BuildingProcessor +internal sealed class LargeWaterParkDeconstructedProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public LargeWaterParkDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(LargeWaterParkDeconstructed packet, Player player) + public override async Task Process(AuthProcessorContext context, LargeWaterParkDeconstructed packet) { // SeparateChildrenToWaterParks must happen before ReplacePieceByGhost // so the water park's children can be moved before it being removed - if (buildingManager.SeparateChildrenToWaterParks(packet) && - buildingManager.ReplacePieceByGhost(player, packet, out _, out int operationId)) + if (BuildingManager.SeparateChildrenToWaterParks(packet) && + BuildingManager.ReplacePieceByGhost(context.Sender, packet, out _, out int operationId)) { packet.BaseData = null; - SendToOtherPlayersWithOperationId(packet, player, operationId); + await SendToOtherPlayersWithOperationIdAsync(context, packet, operationId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/LeakRepairedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/LeakRepairedProcessor.cs index 58d29c262a..63d9476255 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/LeakRepairedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/LeakRepairedProcessor.cs @@ -1,25 +1,17 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class LeakRepairedProcessor : AuthenticatedPacketProcessor +sealed class LeakRepairedProcessor(WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly WorldEntityManager worldEntityManager; - private readonly PlayerManager playerManager; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public LeakRepairedProcessor(WorldEntityManager worldEntityManager, PlayerManager playerManager) - { - this.worldEntityManager = worldEntityManager; - this.playerManager = playerManager; - } - - public override void Process(LeakRepaired packet, Player player) + public async Task Process(AuthProcessorContext context, LeakRepaired packet) { if (worldEntityManager.TryDestroyEntity(packet.LeakId, out _)) { - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/ModifyConstructedAmountProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/ModifyConstructedAmountProcessor.cs index 79b429640a..bbb030c7d6 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/ModifyConstructedAmountProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/ModifyConstructedAmountProcessor.cs @@ -1,17 +1,15 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class ModifyConstructedAmountProcessor : BuildingProcessor +internal sealed class ModifyConstructedAmountProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public ModifyConstructedAmountProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(ModifyConstructedAmount packet, Player player) + public override async Task Process(AuthProcessorContext context, ModifyConstructedAmount packet) { - if (buildingManager.ModifyConstructedAmount(packet)) + if (BuildingManager.ModifyConstructedAmount(packet)) { - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionPolicyRequestProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionPolicyRequestProcessor.cs index 9ff5c9881e..81ac3db13a 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionPolicyRequestProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionPolicyRequestProcessor.cs @@ -1,24 +1,19 @@ -using Nitrox.Server.Subnautica.Models.Communication; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - public class MultiplayerSessionPolicyRequestProcessor : UnauthenticatedPacketProcessor - { - private readonly IOptions config; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public MultiplayerSessionPolicyRequestProcessor(IOptions config, ILogger logger) - { - this.config = config; - this.logger = logger; - } +internal sealed class MultiplayerSessionPolicyRequestProcessor(IOptions configProvider, ILogger logger) + : IAnonPacketProcessor +{ + private readonly IOptions configProvider = configProvider; + private readonly ILogger logger = logger; - // This will extend in the future when we look into different options for auth - public override void Process(MultiplayerSessionPolicyRequest packet, INitroxConnection connection) - { - logger.ZLogInformation($"Providing session policies..."); - connection.SendPacket(new MultiplayerSessionPolicy(packet.CorrelationId, config.Value.DisableConsole, config.Value.MaxConnections, config.Value.IsPasswordRequired())); - } + // This will extend in the future when we look into different options for auth + public async Task Process(AnonProcessorContext context, MultiplayerSessionPolicyRequest packet) + { + logger.ZLogInformation($"Providing session policies..."); + SubnauticaServerOptions options = configProvider.Value; + await context.ReplyAsync(new MultiplayerSessionPolicy(packet.CorrelationId, options.DisableConsole, options.MaxConnections, options.IsPasswordRequired())); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionReservationRequestProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionReservationRequestProcessor.cs index 7fbc6060ab..4aa8c40658 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionReservationRequestProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/MultiplayerSessionReservationRequestProcessor.cs @@ -1,38 +1,32 @@ using Nitrox.Model.MultiplayerSession; using Nitrox.Model.Subnautica.MultiplayerSession; -using Nitrox.Server.Subnautica.Models.Communication; using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - sealed class MultiplayerSessionReservationRequestProcessor : UnauthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly ILogger logger; - - public MultiplayerSessionReservationRequestProcessor(PlayerManager playerManager, ILogger logger) - { - this.playerManager = playerManager; - this.logger = logger; - } +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public override void Process(MultiplayerSessionReservationRequest packet, INitroxConnection connection) - { - logger.ZLogInformation($"Processing reservation request from {packet.AuthenticationContext.Username}"); +internal sealed class MultiplayerSessionReservationRequestProcessor(PlayerManager playerManager, ILogger logger) + : IAnonPacketProcessor +{ + private readonly PlayerManager playerManager = playerManager; + private readonly ILogger logger = logger; - string correlationId = packet.CorrelationId; - PlayerSettings playerSettings = packet.PlayerSettings; - AuthenticationContext authenticationContext = packet.AuthenticationContext; - MultiplayerSessionReservation reservation = playerManager.ReservePlayerContext( - connection, - playerSettings, - authenticationContext, - correlationId); + public async Task Process(AnonProcessorContext context, MultiplayerSessionReservationRequest packet) + { + logger.ZLogInformation($"Processing reservation request from {packet.AuthenticationContext.Username}"); - logger.ZLogInformation($"Reservation processed successfully: Username: {packet.AuthenticationContext.Username} - {reservation}"); + string correlationId = packet.CorrelationId; + PlayerSettings playerSettings = packet.PlayerSettings; + AuthenticationContext authenticationContext = packet.AuthenticationContext; + MultiplayerSessionReservation reservation = playerManager.ReservePlayerContext( + context.Sender.SessionId, + context.Sender.EndPoint, + playerSettings, + authenticationContext, + correlationId); - connection.SendPacket(reservation); - } + logger.ZLogInformation($"Reservation processed successfully: Username: {packet.AuthenticationContext.Username} - {reservation}"); + await context.ReplyAsync(reservation); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs index 9b440eee0e..7d6e833a13 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs @@ -1,23 +1,15 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - internal sealed class PDAEncyclopediaEntryAddProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly PdaManager pdaManager; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public PDAEncyclopediaEntryAddProcessor(PlayerManager playerManager, PdaManager pdaManager) - { - this.playerManager = playerManager; - this.pdaManager = pdaManager; - } +internal sealed class PDAEncyclopediaEntryAddProcessor(PdaManager pdaManager) : IAuthPacketProcessor +{ + private readonly PdaManager pdaManager = pdaManager; - public override void Process(PDAEncyclopediaEntryAdd packet, Player player) - { - pdaManager.AddEncyclopediaEntry(packet.Key); - playerManager.SendPacketToOtherPlayers(packet, player); - } + public async Task Process(AuthProcessorContext context, PDAEncyclopediaEntryAdd packet) + { + pdaManager.AddEncyclopediaEntry(packet.Key); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDALogEntryAddProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDALogEntryAddProcessor.cs index 672e020c98..f8777a429b 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDALogEntryAddProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDALogEntryAddProcessor.cs @@ -1,30 +1,21 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - internal sealed class PDALogEntryAddProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly PdaManager pdaManager; - private readonly StoryScheduler storyScheduler; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public PDALogEntryAddProcessor(PlayerManager playerManager, PdaManager pdaManager, StoryScheduler storyScheduler) - { - this.playerManager = playerManager; - this.pdaManager = pdaManager; - this.storyScheduler = storyScheduler; - } +internal sealed class PDALogEntryAddProcessor(PdaManager pdaManager, StoryScheduler storyScheduler) : IAuthPacketProcessor +{ + private readonly PdaManager pdaManager = pdaManager; + private readonly StoryScheduler storyScheduler = storyScheduler; - public override void Process(PDALogEntryAdd packet, Player player) + public async Task Process(AuthProcessorContext context, PDALogEntryAdd packet) + { + pdaManager.AddPDALogEntry(new PDALogEntry(packet.Key, packet.Timestamp)); + if (storyScheduler.ContainsScheduledStory(packet.Key)) { - pdaManager.AddPDALogEntry(new PDALogEntry(packet.Key, packet.Timestamp)); - if (storyScheduler.ContainsScheduledStory(packet.Key)) - { - storyScheduler.UnscheduleStory(packet.Key); - } - playerManager.SendPacketToOtherPlayers(packet, player); + storyScheduler.UnscheduleStory(packet.Key); } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAScanFinishedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAScanFinishedPacketProcessor.cs index 054d985af8..1554c0f8d6 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAScanFinishedPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PDAScanFinishedPacketProcessor.cs @@ -1,29 +1,21 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PDAScanFinishedPacketProcessor : AuthenticatedPacketProcessor +internal sealed class PDAScanFinishedPacketProcessor(PdaManager pdaManager, WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly PdaManager pdaManager; - private readonly WorldEntityManager worldEntityManager; + private readonly PdaManager pdaManager = pdaManager; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public PDAScanFinishedPacketProcessor(PlayerManager playerManager, PdaManager pdaManager, WorldEntityManager worldEntityManager) - { - this.playerManager = playerManager; - this.pdaManager = pdaManager; - this.worldEntityManager = worldEntityManager; - } - - public override void Process(PDAScanFinished packet, Player player) + public async Task Process(AuthProcessorContext context, PDAScanFinished packet) { if (!packet.WasAlreadyResearched) { pdaManager.UpdateEntryUnlockedProgress(packet.TechType, packet.UnlockedAmount, packet.FullyResearched); } - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); if (packet.Id != null) { diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PickupItemPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PickupItemPacketProcessor.cs index 0210d42caf..e89604a84a 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PickupItemPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PickupItemPacketProcessor.cs @@ -1,35 +1,27 @@ +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PickupItemPacketProcessor : AuthenticatedPacketProcessor +internal sealed class PickupItemPacketProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData) + : IAuthPacketProcessor { - private readonly EntityRegistry entityRegistry; - private readonly WorldEntityManager worldEntityManager; - private readonly PlayerManager playerManager; - private readonly SimulationOwnershipData simulationOwnershipData; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; - public PickupItemPacketProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, PlayerManager playerManager, SimulationOwnershipData simulationOwnershipData) - { - this.entityRegistry = entityRegistry; - this.worldEntityManager = worldEntityManager; - this.playerManager = playerManager; - this.simulationOwnershipData = simulationOwnershipData; - } - - public override void Process(PickupItem packet, Player player) + public async Task Process(AuthProcessorContext context, PickupItem packet) { NitroxId id = packet.Item.Id; if (simulationOwnershipData.RevokeOwnerOfId(id)) { - ushort serverId = ushort.MaxValue; - SimulationOwnershipChange simulationOwnershipChange = new(id, serverId, SimulationLockType.TRANSIENT); - playerManager.SendPacketToAllPlayers(simulationOwnershipChange); + SimulationOwnershipChange simulationOwnershipChange = new(id, SessionId.SERVER_ID, SimulationLockType.TRANSIENT); + await context.SendToAllAsync(simulationOwnershipChange); } StopTrackingExistingWorldEntity(id); @@ -37,14 +29,14 @@ public override void Process(PickupItem packet, Player player) entityRegistry.AddOrUpdate(packet.Item); // Have other players respawn the item inside the inventory. - playerManager.SendPacketToOtherPlayers(new SpawnEntities(packet.Item, forceRespawn: true), player); + await context.SendToOthersAsync(new SpawnEntities(packet.Item, forceRespawn: true)); } private void StopTrackingExistingWorldEntity(NitroxId id) { Optional entity = entityRegistry.GetEntityById(id); - if (entity.HasValue && entity.Value is WorldEntity worldEntity) + if (entity is { HasValue: true, Value: WorldEntity worldEntity }) { // Do not track this entity in the open world anymore. worldEntityManager.StopTrackingEntity(worldEntity); diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PieceDeconstructedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PieceDeconstructedProcessor.cs index ceccece8fc..286d94d500 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PieceDeconstructedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PieceDeconstructedProcessor.cs @@ -1,18 +1,16 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class PieceDeconstructedProcessor : BuildingProcessor +internal sealed class PieceDeconstructedProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public PieceDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(PieceDeconstructed packet, Player player) + public override async Task Process(AuthProcessorContext context, PieceDeconstructed packet) { - if (buildingManager.ReplacePieceByGhost(player, packet, out _, out int operationId)) + if (BuildingManager.ReplacePieceByGhost(context.Sender, packet, out _, out int operationId)) { packet.BaseData = null; - SendToOtherPlayersWithOperationId(packet, player, operationId); + await SendToOtherPlayersWithOperationIdAsync(context, packet, operationId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PinnedRecipeMovedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PinnedRecipeMovedProcessor.cs index d8409d7a78..f729b0aa95 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PinnedRecipeMovedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PinnedRecipeMovedProcessor.cs @@ -1,12 +1,13 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class PinnedRecipeMovedProcessor : AuthenticatedPacketProcessor +internal sealed class PinnedRecipeMovedProcessor : IAuthPacketProcessor { - public override void Process(PinnedRecipeMoved packet, Player player) + public Task Process(AuthProcessorContext context, PinnedRecipeMoved packet) { - player.PinnedRecipePreferences.Clear(); - player.PinnedRecipePreferences.AddRange(packet.RecipePins); + context.Sender.PinnedRecipePreferences.Clear(); + context.Sender.PinnedRecipePreferences.AddRange(packet.RecipePins); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceBaseProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceBaseProcessor.cs index b7936b53da..4df1c0ffdc 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceBaseProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceBaseProcessor.cs @@ -1,22 +1,20 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PlaceBaseProcessor : BuildingProcessor +internal sealed class PlaceBaseProcessor(BuildingManager buildingManager, EntitySimulation entitySimulation) : BuildingProcessor(buildingManager, entitySimulation) { - public PlaceBaseProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation){ } - - public override void Process(PlaceBase packet, Player player) + public override async Task Process(AuthProcessorContext context, PlaceBase packet) { - if (buildingManager.CreateBase(packet)) + if (BuildingManager.CreateBase(packet)) { - TryClaimBuildPiece(packet.BuildEntity, player); + await TryClaimBuildPieceAsync(context, packet.BuildEntity); // End-players can process elementary operations without this data (packet would be heavier for no reason) packet.Deflate(); - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceGhostProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceGhostProcessor.cs index 195e89a401..1e12a16b73 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceGhostProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceGhostProcessor.cs @@ -1,17 +1,15 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class PlaceGhostProcessor : BuildingProcessor +internal sealed class PlaceGhostProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public PlaceGhostProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(PlaceGhost packet, Player player) + public override async Task Process(AuthProcessorContext context, PlaceGhost packet) { - if (buildingManager.AddGhost(packet)) + if (BuildingManager.AddGhost(packet)) { - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceModuleProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceModuleProcessor.cs index ce875dc687..c7847a16c9 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceModuleProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlaceModuleProcessor.cs @@ -1,22 +1,20 @@ -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class PlaceModuleProcessor : BuildingProcessor +internal sealed class PlaceModuleProcessor(BuildingManager buildingManager, EntitySimulation entitySimulation) : BuildingProcessor(buildingManager, entitySimulation) { - public PlaceModuleProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation) { } - - public override void Process(PlaceModule packet, Player player) + public override async Task Process(AuthProcessorContext context, PlaceModule packet) { - if (buildingManager.AddModule(packet)) + if (BuildingManager.AddModule(packet)) { if (packet.ModuleEntity.ParentId == null || !packet.ModuleEntity.IsInside) { - TryClaimBuildPiece(packet.ModuleEntity, player); + await TryClaimBuildPieceAsync(context, packet.ModuleEntity); } - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerDeathEventProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerDeathEventProcessor.cs index 31bdda630d..7e01a49130 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerDeathEventProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerDeathEventProcessor.cs @@ -1,38 +1,26 @@ -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class PlayerDeathEventProcessor(IOptions config) : IAuthPacketProcessor { - class PlayerDeathEventProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly IOptions options; + private readonly IOptions options = config; - public PlayerDeathEventProcessor(PlayerManager playerManager, IOptions config) + public async Task Process(AuthProcessorContext context, PlayerDeathEvent packet) + { + if (options.Value.IsHardcore()) { - this.playerManager = playerManager; - options = config; + context.Sender.IsPermaDeath = true; + await context.ReplyAsync(new PlayerKicked("Permanent death from hardcore mode")); } - - public override void Process(PlayerDeathEvent packet, Player player) + context.Sender.LastStoredPosition = packet.DeathPosition; + context.Sender.LastStoredSubRootID = context.Sender.SubRootId; + if (context.Sender.Permissions > Perms.MODERATOR) { - if (options.Value.IsHardcore()) - { - player.IsPermaDeath = true; - PlayerKicked playerKicked = new PlayerKicked("Permanent death from hardcore mode"); - player.SendPacket(playerKicked); - } - - player.LastStoredPosition = packet.DeathPosition; - player.LastStoredSubRootID = player.SubRootId; - - if (player.Permissions > Perms.MODERATOR) - { - player.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, "You can use /back to go to your death location")); - } - - playerManager.SendPacketToOtherPlayers(packet, player); + await context.ReplyAsync(new ChatMessage(SessionId.SERVER_ID, "You can use /back to go to your death location")); } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerHeldItemChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerHeldItemChangedProcessor.cs index 447aaf51cf..9cb9946479 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerHeldItemChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerHeldItemChangedProcessor.cs @@ -1,25 +1,16 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class PlayerHeldItemChangedProcessor : IAuthPacketProcessor { - internal sealed class PlayerHeldItemChangedProcessor : AuthenticatedPacketProcessor + public async Task Process(AuthProcessorContext context, PlayerHeldItemChanged packet) { - private readonly PlayerManager playerManager; - - public PlayerHeldItemChangedProcessor(PlayerManager playerManager) + if (packet.IsFirstTime != null && !context.Sender.UsedItems.Contains(packet.IsFirstTime)) { - this.playerManager = playerManager; + context.Sender.UsedItems.Add(packet.IsFirstTime); } - public override void Process(PlayerHeldItemChanged packet, Player player) - { - if (packet.IsFirstTime != null && !player.UsedItems.Contains(packet.IsFirstTime)) - { - player.UsedItems.Add(packet.IsFirstTime); - } - - playerManager.SendPacketToOtherPlayers(packet, player); - } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs index ff79aaafc3..74c09ada6f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs @@ -1,37 +1,27 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PlayerInCyclopsMovementProcessor : AuthenticatedPacketProcessor +internal sealed class PlayerInCyclopsMovementProcessor(IPacketSender packetSender, EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - private readonly ILogger logger; + private readonly IPacketSender packetSender = packetSender; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly ILogger logger = logger; - public PlayerInCyclopsMovementProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, ILogger logger) + public async Task Process(AuthProcessorContext context, PlayerInCyclopsMovement packet) { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - this.logger = logger; - } - - public override void Process(PlayerInCyclopsMovement packet, Player player) - { - if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerEntity playerEntity)) + if (!entityRegistry.TryGetEntityById(context.Sender.PlayerContext.PlayerNitroxId, out PlayerEntity playerEntity)) { - playerEntity.Transform.LocalPosition = packet.LocalPosition; - playerEntity.Transform.LocalRotation = packet.LocalRotation; - - player.Position = playerEntity.Transform.Position; - player.Rotation = playerEntity.Transform.Rotation; - playerManager.SendPacketToOtherPlayers(packet, player); - } - else - { - logger.ZLogErrorOnce($"{nameof(PlayerEntity)} couldn't be found for player {player.Name}. It is advised the player reconnects before losing too much progression."); + logger.ZLogErrorOnce($"{nameof(PlayerEntity)} couldn't be found for player {context.Sender.Name}. It is advised the player reconnects before losing too much progression."); + return; } + + playerEntity.Transform.LocalPosition = packet.LocalPosition; + playerEntity.Transform.LocalRotation = packet.LocalRotation; + context.Sender.Position = playerEntity.Transform.Position; + context.Sender.Rotation = playerEntity.Transform.Rotation; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs index 747d57130c..d7a1787849 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerJoiningMultiplayerSessionProcessor.cs @@ -1,20 +1,15 @@ -using Nitrox.Server.Subnautica.Models.Communication; using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PlayerJoiningMultiplayerSessionProcessor : UnauthenticatedPacketProcessor +internal sealed class PlayerJoiningMultiplayerSessionProcessor(JoiningManager joiningManager) : IAnonPacketProcessor { - private readonly JoiningManager joiningManager; + private readonly JoiningManager joiningManager = joiningManager; - public PlayerJoiningMultiplayerSessionProcessor(JoiningManager joiningManager) + public Task Process(AnonProcessorContext context, PlayerJoiningMultiplayerSession packet) { - this.joiningManager = joiningManager; - } - - public override void Process(PlayerJoiningMultiplayerSession packet, INitroxConnection connection) - { - joiningManager.AddToJoinQueue(connection, packet.ReservationKey); + joiningManager.AddToJoinQueue(context.Sender.SessionId, packet.ReservationKey); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerMovementProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerMovementProcessor.cs index cad2b6525d..38ae78bf25 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerMovementProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerMovementProcessor.cs @@ -1,35 +1,26 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class PlayerMovementProcessor(EntityRegistry entityRegistry) : IAuthPacketProcessor { - class PlayerMovementProcessor : AuthenticatedPacketProcessor + private readonly EntityRegistry entityRegistry = entityRegistry; + + public async Task Process(AuthProcessorContext context, PlayerMovement packet) { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; + Optional playerEntity = entityRegistry.GetEntityById(context.Sender.PlayerContext.PlayerNitroxId); - public PlayerMovementProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) + if (playerEntity.HasValue) { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; + playerEntity.Value.Transform.Position = packet.Position; + playerEntity.Value.Transform.Rotation = packet.BodyRotation; } - public override void Process(PlayerMovement packet, Player player) - { - Optional playerEntity = entityRegistry.GetEntityById(player.PlayerContext.PlayerNitroxId); - - if (playerEntity.HasValue) - { - playerEntity.Value.Transform.Position = packet.Position; - playerEntity.Value.Transform.Rotation = packet.BodyRotation; - } - - player.Position = packet.Position; - player.Rotation = packet.BodyRotation; - playerManager.SendPacketToOtherPlayers(packet, player); - } + context.Sender.Position = packet.Position; + context.Sender.Rotation = packet.BodyRotation; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerQuickSlotsBindingChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerQuickSlotsBindingChangedProcessor.cs index 0c938ebaa6..c663dc31e3 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerQuickSlotsBindingChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerQuickSlotsBindingChangedProcessor.cs @@ -1,12 +1,12 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal sealed class PlayerQuickSlotsBindingChangedProcessor : IAuthPacketProcessor { - public class PlayerQuickSlotsBindingChangedProcessor : AuthenticatedPacketProcessor + public Task Process(AuthProcessorContext context, PlayerQuickSlotsBindingChanged packet) { - public override void Process(PlayerQuickSlotsBindingChanged packet, Player player) - { - player.QuickSlotsBindingIds = packet.SlotItemIds; - } + context.Sender.QuickSlotsBindingIds = packet.SlotItemIds; + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSeeOutOfCellEntityProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSeeOutOfCellEntityProcessor.cs index 378311139d..1e834229c7 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSeeOutOfCellEntityProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSeeOutOfCellEntityProcessor.cs @@ -1,22 +1,18 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class PlayerSeeOutOfCellEntityProcessor : AuthenticatedPacketProcessor +internal sealed class PlayerSeeOutOfCellEntityProcessor(EntityRegistry entityRegistry) : IAuthPacketProcessor { - private readonly EntityRegistry entityRegistry; + private readonly EntityRegistry entityRegistry = entityRegistry; - public PlayerSeeOutOfCellEntityProcessor(EntityRegistry entityRegistry) - { - this.entityRegistry = entityRegistry; - } - - public override void Process(PlayerSeeOutOfCellEntity packet, Player player) + public Task Process(AuthProcessorContext context, PlayerSeeOutOfCellEntity packet) { if (entityRegistry.GetEntityById(packet.EntityId).HasValue) { - player.OutOfCellVisibleEntities.Add(packet.EntityId); + context.Sender.OutOfCellVisibleEntities.Add(packet.EntityId); } + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerStatsProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerStatsProcessor.cs index a68ebf5fef..5e0fddafb0 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerStatsProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerStatsProcessor.cs @@ -1,28 +1,20 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -class PlayerStatsProcessor : AuthenticatedPacketProcessor +internal sealed class PlayerStatsProcessor(ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly ILogger logger; + private readonly ILogger logger = logger; - public PlayerStatsProcessor(PlayerManager playerManager, ILogger logger) + public async Task Process(AuthProcessorContext context, PlayerStats packet) { - this.playerManager = playerManager; - this.logger = logger; - } - - public override void Process(PlayerStats packet, Player player) - { - if (packet.PlayerId != player.Id) + if (packet.SessionId != context.Sender.SessionId) { - logger.ZLogWarningOnce($"Player ID mismatch (received: {packet.PlayerId}, real: {player.Id})"); - packet.PlayerId = player.Id; + logger.ZLogWarningOnce($"Player ID mismatch (received: {packet.SessionId}, real: {context.Sender.SessionId})"); + packet.SessionId = context.Sender.SessionId; } - player.Stats = new PlayerStatsData(packet.Oxygen, packet.MaxOxygen, packet.Health, packet.Food, packet.Water, packet.InfectionAmount); - playerManager.SendPacketToOtherPlayers(packet, player); + context.Sender.Stats = new PlayerStatsData(packet.Oxygen, packet.MaxOxygen, packet.Health, packet.Food, packet.Water, packet.InfectionAmount); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSyncFinishedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSyncFinishedProcessor.cs index c83f2027c2..e7a572294c 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSyncFinishedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerSyncFinishedProcessor.cs @@ -1,33 +1,24 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Communication; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Services; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +internal class PlayerSyncFinishedProcessor(SessionManager sessionManager, JoiningManager joiningManager, HibernateService hibernateService) + : IAuthPacketProcessor { - internal class PlayerSyncFinishedProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly JoiningManager joiningManager; - private readonly HibernateService hibernateService; - private readonly ILogger logger; + private readonly SessionManager sessionManager = sessionManager; + private readonly JoiningManager joiningManager = joiningManager; + private readonly HibernateService hibernateService = hibernateService; - public PlayerSyncFinishedProcessor(PlayerManager playerManager, JoiningManager joiningManager, HibernateService hibernateService, ILogger logger) + public async Task Process(AuthProcessorContext context, PlayerSyncFinished packet) + { + if (sessionManager.GetSessionCount() > 0) { - this.playerManager = playerManager; - this.joiningManager = joiningManager; - this.hibernateService = hibernateService; - this.logger = logger; + await hibernateService.WakeAsync(); } - public override void Process(PlayerSyncFinished packet, Player player) - { - // If this is the first player connecting we need to restart time at this exact moment - if (playerManager.GetConnectedPlayers().Count == 1) - { - hibernateService.WakeAsync().ContinueWithHandleError(ex => logger.ZLogError(ex, $"Error while trying to enter low power mode")); - } - - joiningManager.SyncFinishedCallback?.Invoke(); - } + joiningManager.SyncFinishedCallback?.Invoke(); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerUnseeOutOfCellEntityProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerUnseeOutOfCellEntityProcessor.cs index acb83d3b76..2b89054473 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerUnseeOutOfCellEntityProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerUnseeOutOfCellEntityProcessor.cs @@ -1,49 +1,40 @@ using System.Collections.Generic; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PlayerUnseeOutOfCellEntityProcessor : AuthenticatedPacketProcessor +internal sealed class PlayerUnseeOutOfCellEntityProcessor(SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, EntitySimulation entitySimulation, EntityRegistry entityRegistry) + : IAuthPacketProcessor { - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly PlayerManager playerManager; - private readonly EntitySimulation entitySimulation; - private readonly EntityRegistry entityRegistry; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly PlayerManager playerManager = playerManager; + private readonly EntitySimulation entitySimulation = entitySimulation; + private readonly EntityRegistry entityRegistry = entityRegistry; - public PlayerUnseeOutOfCellEntityProcessor(SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, EntitySimulation entitySimulation, EntityRegistry entityRegistry) - { - this.simulationOwnershipData = simulationOwnershipData; - this.playerManager = playerManager; - this.entitySimulation = entitySimulation; - this.entityRegistry = entityRegistry; - } - - public override void Process(PlayerUnseeOutOfCellEntity packet, Player player) + public async Task Process(AuthProcessorContext context, PlayerUnseeOutOfCellEntity packet) { // Most of this packet's utility is in the below Remove - if (!player.OutOfCellVisibleEntities.Remove(packet.EntityId) || + if (!context.Sender.OutOfCellVisibleEntities.Remove(packet.EntityId) || !entityRegistry.TryGetEntityById(packet.EntityId, out Entity entity)) { return; } - // If player can still see the entity even after removing it from the OutOfCellVisibleEntities, then we don't need to change anything - if (player.CanSee(entity)) + if (context.Sender.CanSee(entity)) { return; } - // If the player doesn't own the entity's simulation then we don't need to do anything - if (!simulationOwnershipData.RevokeIfOwner(packet.EntityId, player)) + if (!simulationOwnershipData.RevokeIfOwner(packet.EntityId, context.Sender)) { return; } - List otherPlayers = playerManager.GetConnectedPlayersExcept(player); + List otherPlayers = playerManager.GetConnectedPlayersExcept(context.Sender); if (entitySimulation.TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity)) { entitySimulation.BroadcastSimulationChanges([simulatedEntity]); @@ -51,7 +42,7 @@ public override void Process(PlayerUnseeOutOfCellEntity packet, Player player) else { // No player has taken simulation on the entity - playerManager.SendPacketToAllPlayers(new DropSimulationOwnership(packet.EntityId)); + await context.SendToAllAsync(new DropSimulationOwnership(packet.EntityId)); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PvPAttackProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PvPAttackProcessor.cs index 4817371e0c..a95e1ce8a1 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PvPAttackProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PvPAttackProcessor.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PvPAttackProcessor : AuthenticatedPacketProcessor +internal sealed class PvPAttackProcessor(IPacketSender packetSender, PlayerManager playerManager, IOptions options) : IAuthPacketProcessor { - private readonly IOptions options; - private readonly PlayerManager playerManager; + private readonly IPacketSender packetSender = packetSender; + private readonly IOptions options = options; + private readonly PlayerManager playerManager = playerManager; // TODO: In the future, do a whole config for damage sources private static readonly Dictionary damageMultiplierByType = new() @@ -16,19 +17,13 @@ internal sealed class PvPAttackProcessor : AuthenticatedPacketProcessor options, PlayerManager playerManager) - { - this.options = options; - this.playerManager = playerManager; - } - - public override void Process(PvPAttack packet, Player player) + public async Task Process(AuthProcessorContext context, PvPAttack packet) { if (!options.Value.PvpEnabled) { return; } - if (!playerManager.TryGetPlayerById(packet.TargetPlayerId, out Player targetPlayer)) + if (!playerManager.TryGetPlayerBySessionId(packet.TargetSessionId, out Player targetPlayer)) { return; } @@ -38,6 +33,6 @@ public override void Process(PvPAttack packet, Player player) } packet.Damage *= multiplier; - targetPlayer.SendPacket(packet); + await packetSender.SendPacketAsync(packet, targetPlayer.SessionId); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/RadioPlayPendingMessageProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/RadioPlayPendingMessageProcessor.cs index b8fbf0e8f5..1bc73f6256 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/RadioPlayPendingMessageProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/RadioPlayPendingMessageProcessor.cs @@ -1,29 +1,21 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - internal sealed class RadioPlayPendingMessageProcessor : AuthenticatedPacketProcessor - { - private readonly StoryManager storyManager; - private readonly PlayerManager playerManager; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public RadioPlayPendingMessageProcessor(StoryManager storyManager, PlayerManager playerManager, ILogger logger) - { - this.storyManager = storyManager; - this.playerManager = playerManager; - this.logger = logger; - } +internal sealed class RadioPlayPendingMessageProcessor(StoryManager storyManager, IPacketSender packetSender, ILogger logger) : IAuthPacketProcessor +{ + private readonly StoryManager storyManager = storyManager; + private readonly IPacketSender packetSender = packetSender; + private readonly ILogger logger = logger; - public override void Process(RadioPlayPendingMessage packet, Player player) + public async Task Process(AuthProcessorContext context, RadioPlayPendingMessage packet) + { + if (!storyManager.RemovedLatestRadioMessage()) { - if (!storyManager.RemovedLatestRadioMessage()) - { - logger.ZLogWarning($"Tried to remove the latest radio message but the radio queue is empty: {packet}"); - return; - } - playerManager.SendPacketToOtherPlayers(packet, player); + logger.ZLogWarning($"Tried to remove the latest radio message but the radio queue is empty: {packet}"); + return; } + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs index 1b64c3eb52..4ca1b1e3d9 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs @@ -1,13 +1,14 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; +using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class RangedAttackLastTargetUpdateProcessor( +internal sealed class RangedAttackLastTargetUpdateProcessor( PlayerManager playerManager, EntityRegistry entityRegistry ) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(RangedAttackLastTargetUpdate packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, RangedAttackLastTargetUpdate packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId, packet.TargetId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/RecipePinnedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/RecipePinnedProcessor.cs index b66d5d67d4..5e2cbbaa56 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/RecipePinnedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/RecipePinnedProcessor.cs @@ -1,18 +1,19 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class PinnedRecipeProcessor : AuthenticatedPacketProcessor +internal sealed class PinnedRecipeProcessor : IAuthPacketProcessor { - public override void Process(RecipePinned packet, Player player) + public Task Process(AuthProcessorContext context, RecipePinned packet) { if (packet.Pinned) { - player.PinnedRecipePreferences.Add(packet.TechType); + context.Sender.PinnedRecipePreferences.Add(packet.TechType); } else { - player.PinnedRecipePreferences.Remove(packet.TechType); - } + context.Sender.PinnedRecipePreferences.Remove(packet.TechType); + } + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/RemoveCreatureCorpseProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/RemoveCreatureCorpseProcessor.cs index 05f7e6ad4c..ff9359d2a6 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/RemoveCreatureCorpseProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/RemoveCreatureCorpseProcessor.cs @@ -1,24 +1,18 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class RemoveCreatureCorpseProcessor : AuthenticatedPacketProcessor +internal sealed class RemoveCreatureCorpseProcessor(IPacketSender packetSender, PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly EntitySimulation entitySimulation; - private readonly WorldEntityManager worldEntityManager; + private readonly IPacketSender packetSender = packetSender; + private readonly PlayerManager playerManager = playerManager; + private readonly EntitySimulation entitySimulation = entitySimulation; + private readonly WorldEntityManager worldEntityManager = worldEntityManager; - public RemoveCreatureCorpseProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager) - { - this.playerManager = playerManager; - this.worldEntityManager = worldEntityManager; - this.entitySimulation = entitySimulation; - } - - public override void Process(RemoveCreatureCorpse packet, Player destroyingPlayer) + public async Task Process(AuthProcessorContext context, RemoveCreatureCorpse packet) { entitySimulation.EntityDestroyed(packet.CreatureId); @@ -26,11 +20,11 @@ public override void Process(RemoveCreatureCorpse packet, Player destroyingPlaye { foreach (Player player in playerManager.GetConnectedPlayers()) { - bool isOtherPlayer = player != destroyingPlayer; + bool isOtherPlayer = player != context.Sender; if (isOtherPlayer && player.CanSee(entity)) { player.OutOfCellVisibleEntities.Remove(entity.Id); - player.SendPacket(packet); + await context.SendAsync(packet, player.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/ScheduleProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/ScheduleProcessor.cs index 4f1b91831f..ca51effd78 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/ScheduleProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/ScheduleProcessor.cs @@ -1,24 +1,17 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - internal sealed class ScheduleProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly StoryScheduler storyScheduler; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public ScheduleProcessor(PlayerManager playerManager, StoryScheduler storyScheduler) - { - this.playerManager = playerManager; - this.storyScheduler = storyScheduler; - } +internal sealed class ScheduleProcessor(IPacketSender packetSender, StoryScheduler storyScheduler) : IAuthPacketProcessor +{ + private readonly IPacketSender packetSender = packetSender; + private readonly StoryScheduler storyScheduler = storyScheduler; - public override void Process(Schedule packet, Player player) - { - storyScheduler.ScheduleStory(NitroxScheduledGoal.From(packet.TimeExecute, packet.Key, packet.Type)); - playerManager.SendPacketToOtherPlayers(packet, player); - } + public async Task Process(AuthProcessorContext context, Schedule packet) + { + storyScheduler.ScheduleStory(NitroxScheduledGoal.From(packet.TimeExecute, packet.Key, packet.Type)); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonAttackTargetProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonAttackTargetProcessor.cs index e7f23e4f48..b26389f81b 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonAttackTargetProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonAttackTargetProcessor.cs @@ -1,6 +1,7 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; @@ -9,5 +10,5 @@ internal sealed class SeaDragonAttackTargetProcessor( EntityRegistry entityRegistry ) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(SeaDragonAttackTarget packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, SeaDragonAttackTarget packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.SeaDragonId, packet.TargetId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonGrabExosuitProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonGrabExosuitProcessor.cs index ba6ec56880..9305e1cf6b 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonGrabExosuitProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonGrabExosuitProcessor.cs @@ -1,6 +1,7 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; @@ -9,5 +10,5 @@ internal sealed class SeaDragonGrabExosuitProcessor( EntityRegistry entityRegistry ) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(SeaDragonGrabExosuit packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, SeaDragonGrabExosuit packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.SeaDragonId, packet.TargetId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonSwatAttackProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonSwatAttackProcessor.cs index 3d75af61fc..4fcae6ea0e 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonSwatAttackProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaDragonSwatAttackProcessor.cs @@ -1,6 +1,7 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; @@ -9,5 +10,5 @@ internal sealed class SeaDragonSwatAttackProcessor( EntityRegistry entityRegistry ) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(SeaDragonSwatAttack packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]); + public override async Task Process(AuthProcessorContext context, SeaDragonSwatAttack packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.SeaDragonId, packet.TargetId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs index ff85efd6cb..3c77675f13 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs @@ -1,6 +1,7 @@ using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; @@ -9,5 +10,5 @@ sealed class SeaTreaderSpawnedChunkProcessor( EntityRegistry entityRegistry ) : TransmitIfCanSeePacketProcessor(playerManager, entityRegistry) { - public override void Process(SeaTreaderSpawnedChunk packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]); + public override async Task Process(AuthProcessorContext context, SeaTreaderSpawnedChunk packet) => await TransmitIfCanSeeEntitiesAsync(context, packet, [packet.CreatureId]); } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/ServerCommandProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/ServerCommandProcessor.cs index c46e19387c..13af7968fa 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/ServerCommandProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/ServerCommandProcessor.cs @@ -1,17 +1,42 @@ -using Nitrox.Model.DataStructures; -using Nitrox.Server.Subnautica.Models.Commands.Processor; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Model.Core; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Logging.Scopes; +using Nitrox.Server.Subnautica.Models.Packets.Core; +using Nitrox.Server.Subnautica.Services; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class ServerCommandProcessor(TextCommandProcessor cmdProcessor, ILogger logger) : AuthenticatedPacketProcessor +internal sealed class ServerCommandProcessor(CommandService cmdProcessor, IPacketSender packetSender, ILogger logger) : IAuthPacketProcessor { - private readonly TextCommandProcessor cmdProcessor = cmdProcessor; + private readonly CommandService cmdProcessor = cmdProcessor; private readonly ILogger logger = logger; + private readonly IPacketSender packetSender = packetSender; - public override void Process(ServerCommand packet, Player player) + public async Task Process(AuthProcessorContext context, ServerCommand packet) { - logger.ZLogInformation($"{player.Name} issued command '/{packet.Cmd}'"); - cmdProcessor.ProcessCommand(packet.Cmd, Optional.Of(player), player.Permissions); + logger.ZLogInformation($"{context.Sender.Name} issued command '/{packet.Cmd}'"); + string commandOutput = ""; + bool success; + using (logger.BeginPlainScope()) + using (CaptureScope scope = logger.BeginCaptureScope()) + { + success = cmdProcessor.ExecuteCommand(packet.Cmd, new PlayerToServerCommandContext(packetSender, context.Sender), out Task? task); + if (task != null) + { + success = success && await task; + } + + commandOutput = string.Join("", scope.Logs); + } + + // Only log back to user if there are errors. Otherwise, successful commands will log themselves. + if (!success) + { + await context.ReplyAsync(new ChatMessage(SessionId.SERVER_ID, commandOutput.Trim('\r', '\n'))); + } + else if (!string.IsNullOrWhiteSpace(commandOutput)) + { + logger.ZLogInformation($"{commandOutput.TrimEnd(Environment.NewLine.ToCharArray())}"); + } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SetIntroCinematicModeProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SetIntroCinematicModeProcessor.cs index e6cff9cd1d..79debcc9ae 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SetIntroCinematicModeProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SetIntroCinematicModeProcessor.cs @@ -1,33 +1,27 @@ using System.Linq; using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class SetIntroCinematicModeProcessor : AuthenticatedPacketProcessor +internal sealed class SetIntroCinematicModeProcessor(PlayerManager playerManager, ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly ILogger logger; + private readonly PlayerManager playerManager = playerManager; + private readonly ILogger logger = logger; - public SetIntroCinematicModeProcessor(PlayerManager playerManager, ILogger logger) + public async Task Process(AuthProcessorContext context, SetIntroCinematicMode packet) { - this.playerManager = playerManager; - this.logger = logger; - } - - public override void Process(SetIntroCinematicMode packet, Player player) - { - if (packet.PlayerId != player.Id) + if (packet.SessionId != context.Sender.SessionId) { - logger.ZLogWarning($"Received packet where {nameof(SetIntroCinematicMode.PlayerId)} was not equal to sending {nameof(SetIntroCinematicMode.PlayerId)}"); + logger.ZLogWarning($"Received packet where {nameof(SetIntroCinematicMode.SessionId)} #{packet.SessionId} was not equal to sending {nameof(SetIntroCinematicMode.SessionId)} #{context.Sender.SessionId}"); return; } packet.PartnerId = null; // Resetting incoming packets just to be safe we don't relay any PartnerId. Server has only authority. - player.PlayerContext.IntroCinematicMode = packet.Mode; - playerManager.SendPacketToOtherPlayers(packet, player); - logger.ZLogDebug($"IntroCinematicMode set to {packet.Mode} for {player.PlayerContext.PlayerName}"); + context.Sender.PlayerContext.IntroCinematicMode = packet.Mode; + await context.SendToOthersAsync(packet); + logger.ZLogDebug($"IntroCinematicMode set to {packet.Mode} for {context.Sender.PlayerContext.PlayerName}"); Player[] allWaitingPlayers = playerManager.ConnectedPlayers().Where(p => p.PlayerContext.IntroCinematicMode == IntroCinematicMode.WAITING).ToArray(); if (allWaitingPlayers.Length >= 2) @@ -36,8 +30,8 @@ public override void Process(SetIntroCinematicMode packet, Player player) allWaitingPlayers[0].PlayerContext.IntroCinematicMode = allWaitingPlayers[1].PlayerContext.IntroCinematicMode = IntroCinematicMode.START; - playerManager.SendPacketToAllPlayers(new SetIntroCinematicMode(allWaitingPlayers[0].Id, IntroCinematicMode.START, allWaitingPlayers[1].Id)); - playerManager.SendPacketToAllPlayers(new SetIntroCinematicMode(allWaitingPlayers[1].Id, IntroCinematicMode.START, allWaitingPlayers[0].Id)); + await context.SendToAllAsync(new SetIntroCinematicMode(allWaitingPlayers[0].SessionId, IntroCinematicMode.START, allWaitingPlayers[1].SessionId)); + await context.SendToAllAsync(new SetIntroCinematicMode(allWaitingPlayers[1].SessionId, IntroCinematicMode.START, allWaitingPlayers[0].SessionId)); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SignalPingPreferenceChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SignalPingPreferenceChangedProcessor.cs index 54f49ea934..55606e30bc 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SignalPingPreferenceChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SignalPingPreferenceChangedProcessor.cs @@ -1,11 +1,12 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -public class SignalPingPreferenceChangedProcessor : AuthenticatedPacketProcessor +internal sealed class SignalPingPreferenceChangedProcessor : IAuthPacketProcessor { - public override void Process(SignalPingPreferenceChanged packet, Player player) + public Task Process(AuthProcessorContext context, SignalPingPreferenceChanged packet) { - player.PingInstancePreferences[packet.PingKey] = new(packet.Color, packet.Visible); + context.Sender.PingInstancePreferences[packet.PingKey] = new(packet.Color, packet.Visible); + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SimulationOwnershipRequestProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SimulationOwnershipRequestProcessor.cs index e48da7a67a..cd78614980 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SimulationOwnershipRequestProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SimulationOwnershipRequestProcessor.cs @@ -1,34 +1,26 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class SimulationOwnershipRequestProcessor : AuthenticatedPacketProcessor +internal sealed class SimulationOwnershipRequestProcessor(SimulationOwnershipData simulationOwnershipData, EntitySimulation entitySimulation) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly EntitySimulation entitySimulation; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly EntitySimulation entitySimulation = entitySimulation; - public SimulationOwnershipRequestProcessor(PlayerManager playerManager, SimulationOwnershipData simulationOwnershipData, EntitySimulation entitySimulation) + public async Task Process(AuthProcessorContext context, SimulationOwnershipRequest ownershipRequest) { - this.playerManager = playerManager; - this.simulationOwnershipData = simulationOwnershipData; - this.entitySimulation = entitySimulation; - } - - public override void Process(SimulationOwnershipRequest ownershipRequest, Player player) - { - bool aquiredLock = simulationOwnershipData.TryToAcquire(ownershipRequest.Id, player, ownershipRequest.LockType); + bool aquiredLock = simulationOwnershipData.TryToAcquire(ownershipRequest.Id, context.Sender, ownershipRequest.LockType); if (aquiredLock) { bool shouldEntityMove = entitySimulation.ShouldSimulateEntityMovement(ownershipRequest.Id); - SimulationOwnershipChange simulationOwnershipChange = new(ownershipRequest.Id, player.Id, ownershipRequest.LockType, shouldEntityMove); - playerManager.SendPacketToOtherPlayers(simulationOwnershipChange, player); + SimulationOwnershipChange simulationOwnershipChange = new(ownershipRequest.Id, context.Sender.SessionId, ownershipRequest.LockType, shouldEntityMove); + await context.SendToOthersAsync(simulationOwnershipChange); } SimulationOwnershipResponse responseToPlayer = new(ownershipRequest.Id, aquiredLock, ownershipRequest.LockType); - player.SendPacket(responseToPlayer); + await context.ReplyAsync(responseToPlayer); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/StoryGoalExecutedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/StoryGoalExecutedProcessor.cs index 369d8626b2..eeb750f397 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/StoryGoalExecutedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/StoryGoalExecutedProcessor.cs @@ -1,26 +1,18 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class StoryGoalExecutedProcessor : AuthenticatedPacketProcessor +internal sealed class StoryGoalExecutedProcessor(IPacketSender packetSender, StoryManager storyManager, StoryScheduler storyScheduler, PdaManager pdaManager, ILogger logger) + : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly StoryManager storyManager; - private readonly StoryScheduler storyScheduler; - private readonly PdaManager pdaManager; - private readonly ILogger logger; + private readonly IPacketSender packetSender = packetSender; + private readonly StoryManager storyManager = storyManager; + private readonly StoryScheduler storyScheduler = storyScheduler; + private readonly PdaManager pdaManager = pdaManager; + private readonly ILogger logger = logger; - public StoryGoalExecutedProcessor(PlayerManager playerManager, StoryManager storyManager, StoryScheduler storyScheduler, PdaManager pdaManager, ILogger logger) - { - this.playerManager = playerManager; - this.storyManager = storyManager; - this.storyScheduler = storyScheduler; - this.pdaManager = pdaManager; - this.logger = logger; - } - - public override void Process(StoryGoalExecuted packet, Player player) + public async Task Process(AuthProcessorContext context, StoryGoalExecuted packet) { logger.ZLogDebug($"Processing packet: {packet}"); // The switch is structure is similar to StoryGoal.Execute() @@ -40,9 +32,7 @@ public override void Process(StoryGoalExecuted packet, Player player) } break; } - storyScheduler.UnscheduleStory(packet.Key); - - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/SubRootChangedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/SubRootChangedPacketProcessor.cs index 1c1b112211..6949c8e63d 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/SubRootChangedPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/SubRootChangedPacketProcessor.cs @@ -1,25 +1,16 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - class SubRootChangedPacketProcessor : AuthenticatedPacketProcessor - { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public SubRootChangedPacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry) - { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - } +internal sealed class SubRootChangedPacketProcessor(EntityRegistry entityRegistry) : IAuthPacketProcessor +{ + private readonly EntityRegistry entityRegistry = entityRegistry; - public override void Process(SubRootChanged packet, Player player) - { - entityRegistry.ReparentEntity(player.GameObjectId, packet.SubRootId.OrNull()); - player.SubRootId = packet.SubRootId; - playerManager.SendPacketToOtherPlayers(packet, player); - } + public async Task Process(AuthProcessorContext context, SubRootChanged packet) + { + entityRegistry.ReparentEntity(context.Sender.GameObjectId, packet.SubRootId.OrNull()); + context.Sender.SubRootId = packet.SubRootId; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/TextAutoCompleteProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/TextAutoCompleteProcessor.cs new file mode 100644 index 0000000000..bbddc0c527 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/TextAutoCompleteProcessor.cs @@ -0,0 +1,29 @@ +using System.Text.RegularExpressions; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; + +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; + +/// +/// Provides contextual auto completion to clients upon request. +/// +internal sealed class TextAutoCompleteProcessor(CommandRegistry commandRegistry) : IAuthPacketProcessor +{ + private readonly CommandRegistry commandRegistry = commandRegistry; + + public async Task Process(AuthProcessorContext context, TextAutoComplete packet) + { + switch (packet.Context) + { + case TextAutoComplete.AutoCompleteContext.COMMAND_NAME: + Regex matchCommandNameRegex = new($@"^{packet.Text}\w+$", RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); + string? commandName = commandRegistry.FindCommandName(matchCommandNameRegex, context.Sender.Permissions, true); + if (string.IsNullOrWhiteSpace(commandName)) + { + break; + } + await context.ReplyAsync(new TextAutoComplete(commandName, TextAutoComplete.AutoCompleteContext.COMMAND_NAME)); + break; + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateBaseProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateBaseProcessor.cs index 2f88640d5d..ddafb8e79f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateBaseProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateBaseProcessor.cs @@ -1,25 +1,23 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class UpdateBaseProcessor : BuildingProcessor +internal sealed class UpdateBaseProcessor(BuildingManager buildingManager, EntitySimulation entitySimulation) : BuildingProcessor(buildingManager, entitySimulation) { - public UpdateBaseProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation) { } - - public override void Process(UpdateBase packet, Player player) + public override async Task Process(AuthProcessorContext context, UpdateBase packet) { - if (buildingManager.UpdateBase(player, packet, out int operationId)) + if (BuildingManager.UpdateBase(context.Sender, packet, out int operationId)) { if (packet.BuiltPieceEntity is GlobalRootEntity entity) { - TryClaimBuildPiece(entity, player); + await TryClaimBuildPieceAsync(context, entity); } // End-players can process elementary operations without this data (packet would be heavier for no reason) packet.Deflate(); - SendToOtherPlayersWithOperationId(packet, player, operationId); + await SendToOtherPlayersWithOperationIdAsync(context, packet, operationId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateDisplaySurfaceWaterProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateDisplaySurfaceWaterProcessor.cs index e234b886a7..4a168d1255 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateDisplaySurfaceWaterProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateDisplaySurfaceWaterProcessor.cs @@ -1,14 +1,15 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; /// /// Stores the state of a player displaying surface water /// -public class UpdateDisplaySurfaceWaterProcessor : AuthenticatedPacketProcessor +internal sealed class UpdateDisplaySurfaceWaterProcessor : IAuthPacketProcessor { - public override void Process(UpdateDisplaySurfaceWater packet, Player player) + public Task Process(AuthProcessorContext context, UpdateDisplaySurfaceWater packet) { - player.DisplaySurfaceWater = packet.DisplaySurfaceWater; + context.Sender.DisplaySurfaceWater = packet.DisplaySurfaceWater; + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateInPrecursorProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateInPrecursorProcessor.cs index aa79b3af3c..2bcc5d169f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateInPrecursorProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/UpdateInPrecursorProcessor.cs @@ -1,14 +1,15 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; /// /// Stores the state of a player being in precursor /// -public class UpdateInPrecursorProcessor : AuthenticatedPacketProcessor +internal sealed class UpdateInPrecursorProcessor : IAuthPacketProcessor { - public override void Process(UpdateInPrecursor packet, Player player) + public Task Process(AuthProcessorContext context, UpdateInPrecursor packet) { - player.InPrecursor = packet.InPrecursor; + context.Sender.InPrecursor = packet.InPrecursor; + return Task.CompletedTask; } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleDockingProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleDockingProcessor.cs index 22f0f4c1d5..3b1cd0f963 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleDockingProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleDockingProcessor.cs @@ -1,24 +1,23 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class VehicleDockingProcessor : AuthenticatedPacketProcessor +sealed class VehicleDockingProcessor : IAuthPacketProcessor { - private readonly PlayerManager playerManager; + private readonly IPacketSender packetSender; private readonly EntityRegistry entityRegistry; private readonly ILogger logger; - public VehicleDockingProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, ILogger logger) + public VehicleDockingProcessor(IPacketSender packetSender, EntityRegistry entityRegistry, ILogger logger) { - this.playerManager = playerManager; + this.packetSender = packetSender; this.entityRegistry = entityRegistry; this.logger = logger; } - public override void Process(VehicleDocking packet, Player player) + public async Task Process(AuthProcessorContext context, VehicleDocking packet) { if (!entityRegistry.TryGetEntityById(packet.VehicleId, out Entity vehicleEntity)) { @@ -34,6 +33,6 @@ public override void Process(VehicleDocking packet, Player player) entityRegistry.ReparentEntity(vehicleEntity, dockEntity); - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleMovementsPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleMovementsPacketProcessor.cs index 8a71d54207..8979876b2d 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleMovementsPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleMovementsPacketProcessor.cs @@ -1,36 +1,28 @@ using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class VehicleMovementsPacketProcessor : AuthenticatedPacketProcessor +internal sealed class VehicleMovementsPacketProcessor(EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) + : IAuthPacketProcessor { private static readonly NitroxVector3 CyclopsSteeringWheelRelativePosition = new(-0.05f, 0.97f, -23.54f); - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly ILogger logger; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly ILogger logger = logger; - public VehicleMovementsPacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) - { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - this.simulationOwnershipData = simulationOwnershipData; - this.logger = logger; - } - - public override void Process(VehicleMovements packet, Player player) + public async Task Process(AuthProcessorContext context, VehicleMovements packet) { for (int i = packet.Data.Count - 1; i >= 0; i--) { MovementData movementData = packet.Data[i]; - if (simulationOwnershipData.GetPlayerForLock(movementData.Id) != player) + if (simulationOwnershipData.GetPlayerForLock(movementData.Id) != context.Sender) { - logger.ZLogErrorOnce($"Player {player.Name} tried updating {movementData.Id}'s position but they don't have the lock on it"); + logger.ZLogErrorOnce($"Player {context.Sender.Name} tried updating {movementData.Id}'s position but they don't have the lock on it"); // TODO: In the future, add "packet.Data.RemoveAt(i);" and "continue;" to prevent those abnormal situations } @@ -44,13 +36,13 @@ public override void Process(VehicleMovements packet, Player player) // Cyclops' driving wheel is at a known position so we need to adapt the position of the player accordingly if (worldEntity.TechType.Name.Equals("Cyclops")) { - player.Entity.Transform.LocalPosition = CyclopsSteeringWheelRelativePosition; - player.Position = player.Entity.Transform.Position; + context.Sender.Entity.Transform.LocalPosition = CyclopsSteeringWheelRelativePosition; + context.Sender.Position = context.Sender.Entity.Transform.Position; } else { - player.Position = movementData.Position; - player.Rotation = movementData.Rotation; + context.Sender.Position = movementData.Position; + context.Sender.Rotation = movementData.Rotation; } } } @@ -58,7 +50,7 @@ public override void Process(VehicleMovements packet, Player player) if (packet.Data.Count > 0) { - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs index e5c81ed97b..de804ee32f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs @@ -1,21 +1,12 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class VehicleOnPilotModeChangedProcessor : AuthenticatedPacketProcessor +internal sealed class VehicleOnPilotModeChangedProcessor : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - - public VehicleOnPilotModeChangedProcessor(PlayerManager playerManager) + public async Task Process(AuthProcessorContext context, VehicleOnPilotModeChanged packet) { - this.playerManager = playerManager; - } - - public override void Process(VehicleOnPilotModeChanged packet, Player player) - { - player.PlayerContext.DrivingVehicle = packet.IsPiloting ? packet.VehicleId : null; - - playerManager.SendPacketToOtherPlayers(packet, player); + context.Sender.PlayerContext.DrivingVehicle = packet.IsPiloting ? packet.VehicleId : null; + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleUndockingProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleUndockingProcessor.cs index 993d1fe190..fcc948d0b3 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleUndockingProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/VehicleUndockingProcessor.cs @@ -1,24 +1,15 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class VehicleUndockingProcessor : AuthenticatedPacketProcessor +internal sealed class VehicleUndockingProcessor(EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { - private readonly PlayerManager playerManager; - private readonly EntityRegistry entityRegistry; - private readonly ILogger logger; + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly ILogger logger = logger; - public VehicleUndockingProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, ILogger logger) - { - this.playerManager = playerManager; - this.entityRegistry = entityRegistry; - this.logger = logger; - } - - public override void Process(VehicleUndocking packet, Player player) + public async Task Process(AuthProcessorContext context, VehicleUndocking packet) { if (packet.UndockingStart) { @@ -37,6 +28,6 @@ public override void Process(VehicleUndocking packet, Player player) entityRegistry.RemoveFromParent(vehicleEntity); } - playerManager.SendPacketToOtherPlayers(packet, player); + await context.SendToOthersAsync(packet); } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/WaterParkDeconstructedProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/WaterParkDeconstructedProcessor.cs index fb8d0ccfa3..815653ca75 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/WaterParkDeconstructedProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/WaterParkDeconstructedProcessor.cs @@ -1,20 +1,18 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.GameLogic.Bases; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -sealed class WaterParkDeconstructedProcessor : BuildingProcessor +internal sealed class WaterParkDeconstructedProcessor(BuildingManager buildingManager) : BuildingProcessor(buildingManager) { - public WaterParkDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { } - - public override void Process(WaterParkDeconstructed packet, Player player) + public override async Task Process(AuthProcessorContext context, WaterParkDeconstructed packet) { - if (buildingManager.ReplacePieceByGhost(player, packet, out Entity removedEntity, out int operationId) && - buildingManager.CreateWaterParkPiece(packet, removedEntity)) + if (BuildingManager.ReplacePieceByGhost(context.Sender, packet, out Entity removedEntity, out int operationId) && + BuildingManager.CreateWaterParkPiece(packet, removedEntity)) { packet.BaseData = null; - SendToOtherPlayersWithOperationId(packet, player, operationId); + await SendToOtherPlayersWithOperationIdAsync(context, packet, operationId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/WeldActionProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/WeldActionProcessor.cs index dc4beff155..324cec78ea 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/WeldActionProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/WeldActionProcessor.cs @@ -1,28 +1,20 @@ -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Packets.Core; -namespace Nitrox.Server.Subnautica.Models.Packets.Processors -{ - class WeldActionProcessor : AuthenticatedPacketProcessor - { - private readonly SimulationOwnershipData simulationOwnershipData; - private readonly ILogger logger; +namespace Nitrox.Server.Subnautica.Models.Packets.Processors; - public WeldActionProcessor(SimulationOwnershipData simulationOwnershipData, ILogger logger) - { - this.simulationOwnershipData = simulationOwnershipData; - this.logger = logger; - } +internal sealed class WeldActionProcessor(SimulationOwnershipData simulationOwnershipData, ILogger logger) : IAuthPacketProcessor +{ + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly ILogger logger = logger; - public override void Process(WeldAction packet, Player player) + public async Task Process(AuthProcessorContext context, WeldAction packet) + { + Player? simulatingPlayer = simulationOwnershipData.GetPlayerForLock(packet.Id); + if (simulatingPlayer != null) { - Player simulatingPlayer = simulationOwnershipData.GetPlayerForLock(packet.Id); - - if (simulatingPlayer != null) - { - logger.ZLogDebug($"Send WeldAction to simulating player {simulatingPlayer.Name} for entity {packet.Id}"); - simulatingPlayer.SendPacket(packet); - } + logger.ZLogDebug($"Send WeldAction to simulating player {simulatingPlayer.Name} for entity {packet.Id}"); + await context.SendAsync(packet, simulatingPlayer.SessionId); } } } diff --git a/Nitrox.Server.Subnautica/Models/Player.cs b/Nitrox.Server.Subnautica/Models/Player.cs index 915a3749cb..9e63f41095 100644 --- a/Nitrox.Server.Subnautica/Models/Player.cs +++ b/Nitrox.Server.Subnautica/Models/Player.cs @@ -1,26 +1,26 @@ using System.Collections.Generic; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Packets.Processors.Abstract; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.MultiplayerSession; -using Nitrox.Server.Subnautica.Models.Communication; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Models { - public class Player : IProcessorContext + internal sealed class Player { private readonly ThreadSafeSet visibleCells; public ThreadSafeList UsedItems { get; } public Optional[] QuickSlotsBindingIds { get; set; } - public INitroxConnection Connection { get; set; } - public PlayerSettings PlayerSettings => PlayerContext.PlayerSettings; - public PlayerContext PlayerContext { get; set; } - public ushort Id { get; } + public PlayerSettings? PlayerSettings => PlayerContext?.PlayerSettings; + public PlayerContext? PlayerContext { get; set; } + public PeerId Id { get; init; } + public SessionId SessionId { get; set; } public string Name { get; } public bool IsPermaDeath { get; set; } public NitroxVector3 Position { get; set; } @@ -35,23 +35,24 @@ public class Player : IProcessorContext public ThreadSafeDictionary PersonalCompletedGoalsWithTimestamp { get; } public ThreadSafeDictionary PingInstancePreferences { get; set; } public ThreadSafeList PinnedRecipePreferences { get; set; } - public ThreadSafeDictionary EquippedItems { get; set ;} + public ThreadSafeDictionary EquippedItems { get; set; } public ThreadSafeSet OutOfCellVisibleEntities { get; set; } = []; public bool InPrecursor { get; set; } public bool DisplaySurfaceWater { get; set; } public PlayerEntity Entity { get; set; } - public Player(ushort id, string name, bool isPermaDeath, PlayerContext playerContext, INitroxConnection connection, + public Player(PeerId id, SessionId sessionId, string name, bool isPermaDeath, PlayerContext? playerContext, NitroxVector3 position, NitroxQuaternion rotation, NitroxId playerId, Optional subRootId, Perms perms, PlayerStatsData stats, SubnauticaGameMode gameMode, IEnumerable usedItems, Optional[] quickSlotsBindingIds, - IDictionary equippedItems, IDictionary personalCompletedGoalsWithTimestamp, IDictionary pingInstancePreferences, IList pinnedRecipePreferences, bool inPrecursor, bool displaySurfaceWater) + IDictionary equippedItems, IDictionary personalCompletedGoalsWithTimestamp, IDictionary pingInstancePreferences, IList pinnedRecipePreferences, bool inPrecursor, + bool displaySurfaceWater) { Id = id; + SessionId = sessionId; Name = name; IsPermaDeath = isPermaDeath; PlayerContext = playerContext; - Connection = connection; Position = position; Rotation = rotation; SubRootId = subRootId; @@ -72,12 +73,12 @@ public Player(ushort id, string name, bool isPermaDeath, PlayerContext playerCon DisplaySurfaceWater = displaySurfaceWater; } - public static bool operator ==(Player left, Player right) + public static bool operator ==(Player? left, Player? right) { return Equals(left, right); } - public static bool operator !=(Player left, Player right) + public static bool operator !=(Player? left, Player? right) { return !Equals(left, right); } @@ -104,14 +105,6 @@ public override int GetHashCode() return Id.GetHashCode(); } - /// - /// Returns a new list from the original set. To use the original set, use , and . - /// - internal List GetVisibleCells() - { - return [.. visibleCells]; - } - public void AddCells(IEnumerable cells) { foreach (AbsoluteEntityCell cell in cells) @@ -149,29 +142,33 @@ public bool CanSee(Entity entity) return true; } - public void SendPacket(Packet packet) + public void Teleport(NitroxVector3 destination, Optional subRootID, IPacketSender packetSender) { - Connection.SendPacket(packet); - } - - public void Teleport(NitroxVector3 destination, Optional subRootID) - { - PlayerTeleported playerTeleported = new PlayerTeleported(Name, Position, destination, subRootID); + PlayerTeleported playerTeleported = new(Name, Position, destination, subRootID); Position = playerTeleported.DestinationTo; LastStoredPosition = playerTeleported.DestinationFrom; LastStoredSubRootID = subRootID; - SendPacket(playerTeleported); + packetSender.SendPacketAsync(playerTeleported, SessionId); } public override string ToString() { - return $"[Player - Id: {Id}, Name: {Name}, Perms: {Permissions}, Position: {Position}]"; + return $"[Player - SessionId: {Id}, Name: {Name}, Perms: {Permissions}, Position: {Position}]"; } - protected bool Equals(Player other) + private bool Equals(Player other) { return Id == other.Id; } + + /// + /// Returns a new list from the original set. To use the original set, use , + /// and . + /// + internal List GetVisibleCells() + { + return [.. visibleCells]; + } } } diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs index ed37e5021a..c660b71b3b 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs @@ -7,17 +7,16 @@ namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; -internal class EntityDistributionsResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource +internal sealed class EntityDistributionsResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource { private readonly SubnauticaAssetsManager assetsManager = assetsManager; - private readonly IOptions options = options; private readonly TaskCompletionSource lootDistributionTcs = new(); - public LootDistributionData LootDistribution => lootDistributionTcs.Task.GetAwaiter().GetResult(); + private readonly IOptions options = options; public async Task LoadAsync(CancellationToken cancellationToken) { - lootDistributionTcs.TrySetResult(await GetLootDistributionDataAsync(cancellationToken)); + lootDistributionTcs.TrySetResult(await LoadLootDistributionDataAsync(cancellationToken)); } public Task CleanupAsync() @@ -26,7 +25,9 @@ public Task CleanupAsync() return Task.CompletedTask; } - private async Task GetLootDistributionDataAsync(CancellationToken cancellationToken = default) + public async Task GetLootDistributionDataAsync(CancellationToken cancellationToken = default) => await lootDistributionTcs.Task.WaitAsync(cancellationToken); + + private async Task LoadLootDistributionDataAsync(CancellationToken cancellationToken = default) { // TODO: Do not depend on game code; use custom types to map to game JSON files. LootDictionary result = JsonSerializer.Deserialize(await GetJsonAsync(cancellationToken), diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs index 7caf26f0be..f3c97eb8bc 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs @@ -6,6 +6,7 @@ using AssetsTools.NET.Extra; using Newtonsoft.Json; using Nitrox.Model.DataStructures.Unity; +using Nitrox.Server.Subnautica.Models.Factories; using Nitrox.Server.Subnautica.Models.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.Helper; using Nitrox.Server.Subnautica.Models.Resources.AddressablesTools.Catalog; @@ -15,7 +16,7 @@ namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; -internal sealed class PrefabPlaceholderGroupsResource(SubnauticaAssetsManager assetsManager, IOptions options, ILogger logger) : IGameResource +internal sealed class PrefabPlaceholderGroupsResource(SubnauticaAssetsManager assetsManager, RandomFactory randomFactory, IOptions options, ILogger logger) : IGameResource { /// /// The version of the cache supported by this parser @@ -30,6 +31,7 @@ internal sealed class PrefabPlaceholderGroupsResource(SubnauticaAssetsManager as private const string CACHE_FILENAME = "PrefabPlaceholdersGroupAssetsCache.json"; private readonly SubnauticaAssetsManager assetsManager = assetsManager; + private readonly XorRandom random = randomFactory.GetUnityLikeRandom(); private readonly ILogger logger = logger; private readonly IOptions options = options; private readonly TaskCompletionSource resourceLoadFinished = new(); @@ -81,7 +83,7 @@ public void PickRandomClassIdIfRequired(ref string classId) { if (RandomPossibilitiesByClassId.TryGetValue(classId, out string[] choices)) { - int randomIndex = XorRandom.NextIntRange(0, choices.Length); + int randomIndex = random.NextIntRange(0, choices.Length); classId = choices[randomIndex]; } } diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/RandomStartResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/RandomStartResource.cs index d6310dd987..edfe7874ea 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/RandomStartResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/RandomStartResource.cs @@ -14,7 +14,6 @@ internal sealed class RandomStartResource(SubnauticaAssetsManager assetsManager, private readonly SubnauticaAssetsManager assetsManager = assetsManager; private readonly IOptions options = options; private readonly TaskCompletionSource randomStartGeneratorTcs = new(); - public RandomStartGenerator RandomStartGenerator => randomStartGeneratorTcs.Task.GetAwaiter().GetResult(); public Task LoadAsync(CancellationToken cancellationToken) { @@ -28,6 +27,8 @@ public Task CleanupAsync() return Task.CompletedTask; } + public Task GetRandomStartGeneratorAsync() => randomStartGeneratorTcs.Task; + private RandomStartGenerator? LoadAndGetRandomStartGenerator(CancellationToken cancellationToken = default) { string bundlePath = Path.Combine(options.Value.GetSubnauticaStandaloneResourcePath(), "essentials.unity_0ee8dd89ed55f05bc38a09cc77137d4e.bundle"); @@ -51,7 +52,7 @@ public Task CleanupAsync() return new RandomStartGenerator(new PixelProvider(texture)); } - private class PixelProvider : RandomStartGenerator.IPixelProvider + private sealed class PixelProvider : RandomStartGenerator.IPixelProvider { private readonly Image texture; @@ -61,10 +62,12 @@ public PixelProvider(Image texture) this.texture = texture; } - public byte GetRed(int x, int y) => texture[x, y].R; + private Bgra32 GetPixel(int x, int y) => texture[Math.Clamp(x, 0, texture.Width - 1), Math.Clamp(y, 0, texture.Height - 1)]; + + public byte GetRed(int x, int y) => GetPixel(x, y).R; - public byte GetGreen(int x, int y) => texture[x, y].G; + public byte GetGreen(int x, int y) => GetPixel(x, y).G; - public byte GetBlue(int x, int y) => texture[x, y].B; + public byte GetBlue(int x, int y) => GetPixel(x, y).B; } } diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/WorldEntitiesResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/WorldEntitiesResource.cs index cee332038f..d632bca153 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/WorldEntitiesResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/WorldEntitiesResource.cs @@ -7,17 +7,15 @@ namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; -internal class WorldEntitiesResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource +internal sealed class WorldEntitiesResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource { private readonly SubnauticaAssetsManager assetsManager = assetsManager; private readonly IOptions startOptions = options; private readonly TaskCompletionSource> worldEntitiesByClassId = new(); - public Dictionary WorldEntitiesByClassId => worldEntitiesByClassId.Task.GetAwaiter().GetResult(); - public Task LoadAsync(CancellationToken cancellationToken) + public async Task LoadAsync(CancellationToken cancellationToken) { - worldEntitiesByClassId.TrySetResult(GetWorldEntitiesByClassId(cancellationToken)); - return Task.CompletedTask; + worldEntitiesByClassId.TrySetResult(await LoadWorldEntitiesByClassIdAsync(cancellationToken)); } public Task CleanupAsync() @@ -26,7 +24,9 @@ public Task CleanupAsync() return Task.CompletedTask; } - private Dictionary GetWorldEntitiesByClassId(CancellationToken cancellationToken = default) + public Task> GetWorldEntitiesByClassIdAsync() => worldEntitiesByClassId.Task; + + private Task> LoadWorldEntitiesByClassIdAsync(CancellationToken cancellationToken = default) { Dictionary result = []; @@ -52,6 +52,6 @@ private Dictionary GetWorldEntitiesByClassId(Cancellati } Validate.IsTrue(result.Count > 0); - return result; + return Task.FromResult(result); } } diff --git a/Nitrox.Server.Subnautica/Models/Serialization/Json/PeerIdConverter.cs b/Nitrox.Server.Subnautica/Models/Serialization/Json/PeerIdConverter.cs new file mode 100644 index 0000000000..d762baabb4 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Serialization/Json/PeerIdConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Nitrox.Model.Core; + +namespace Nitrox.Server.Subnautica.Models.Serialization.Json; + +internal sealed class PeerIdConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, PeerId value, JsonSerializer serializer) + { + writer.WriteValue(value); + } + + public override PeerId ReadJson(JsonReader reader, Type objectType, PeerId existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.Value is { } num) + { + return Convert.ToUInt32(reader.Value); + } + if (hasExistingValue) + { + return existingValue; + } + return 0; + } +} diff --git a/Nitrox.Server.Subnautica/Models/Serialization/ServerJsonSerializer.cs b/Nitrox.Server.Subnautica/Models/Serialization/ServerJsonSerializer.cs index 43aa251637..7a4ee1ad78 100644 --- a/Nitrox.Server.Subnautica/Models/Serialization/ServerJsonSerializer.cs +++ b/Nitrox.Server.Subnautica/Models/Serialization/ServerJsonSerializer.cs @@ -6,7 +6,7 @@ namespace Nitrox.Server.Subnautica.Models.Serialization; -public class ServerJsonSerializer : IServerSerializer +public sealed class ServerJsonSerializer : IServerSerializer { public const string FILE_ENDING = ".json"; @@ -24,6 +24,7 @@ public ServerJsonSerializer(ILogger logger) serializer.TypeNameHandling = TypeNameHandling.Auto; serializer.ContractResolver = new AttributeContractResolver(); serializer.Converters.Add(new NitroxIdConverter()); + serializer.Converters.Add(new PeerIdConverter()); serializer.Converters.Add(new TechTypeConverter()); serializer.Converters.Add(new VersionConverter()); serializer.Converters.Add(new KeyValuePairConverter()); diff --git a/Nitrox.Server.Subnautica/Models/Serialization/ServerProtoBufSerializer.cs b/Nitrox.Server.Subnautica/Models/Serialization/ServerProtoBufSerializer.cs index d389ed5ddd..2842c5e51f 100644 --- a/Nitrox.Server.Subnautica/Models/Serialization/ServerProtoBufSerializer.cs +++ b/Nitrox.Server.Subnautica/Models/Serialization/ServerProtoBufSerializer.cs @@ -82,7 +82,7 @@ private void RegisterAssemblyClasses(string assemblyName) } catch (Exception ex) { - logger?.ZLogError(ex, $"ServerProtoBufSerializer has thrown an error registering the type: {type:@Type} from {assemblyName:@AssemblyName}"); + logger?.ZLogError(ex, $"error registering the type: {type:@Type} from {assemblyName:@AssemblyName}"); } } } diff --git a/Nitrox.Server.Subnautica/Models/Serialization/World/WorldData.cs b/Nitrox.Server.Subnautica/Models/Serialization/World/WorldData.cs index 4833bc6a6d..1508789886 100644 --- a/Nitrox.Server.Subnautica/Models/Serialization/World/WorldData.cs +++ b/Nitrox.Server.Subnautica/Models/Serialization/World/WorldData.cs @@ -14,14 +14,14 @@ internal class WorldData [DataMember(Order = 2)] public GameData? GameData { get; set; } + [Obsolete("Use server.cfg seed instead - TODO: delete this but keep backward compat via save upgrade")] [DataMember(Order = 3)] public string? Seed { get; set; } public bool IsValid() { return ParsedBatchCells != null && - GameData != null && - Seed != null; + GameData != null; } } } diff --git a/Nitrox.Server.Subnautica/Models/Serialization/World/WorldService.cs b/Nitrox.Server.Subnautica/Models/Serialization/World/WorldService.cs index 9d63caaf4f..f03ece9abf 100644 --- a/Nitrox.Server.Subnautica/Models/Serialization/World/WorldService.cs +++ b/Nitrox.Server.Subnautica/Models/Serialization/World/WorldService.cs @@ -86,8 +86,7 @@ public bool Save(string saveDir) WorldData = new() { ParsedBatchCells = batchEntitySpawner.SerializableParsedBatches, - GameData = GameData.From(pdaManager, storyManager.StoryGoalData, storyScheduler, storyManager, timeService), - Seed = options.Value.Seed, + GameData = GameData.From(pdaManager, storyManager.StoryGoalData, storyScheduler, storyManager, timeService) }, PlayerData = PlayerData.From(playerManager.GetAllPlayers()), GlobalRootData = GlobalRootData.From(worldEntityManager.GetPersistentGlobalRootEntities()), @@ -98,14 +97,14 @@ public bool Save(string saveDir) public async Task StartAsync(CancellationToken cancellationToken) { - if (!LoadWorldFromSavePath(startOptions.Value.GetServerSavePath())) + if (!await LoadWorldFromSavePathAsync(startOptions.Value.GetServerSavePath())) { - CreateAndLoadWorld(); + await CreateAndLoadWorldAsync(); } - CreateFullEntityCacheIfRequested(); + await CreateFullEntityCacheIfRequested(); return; - void CreateFullEntityCacheIfRequested() + async Task CreateFullEntityCacheIfRequested() { if (!options.Value.CreateFullEntityCache) { @@ -119,10 +118,10 @@ void CreateFullEntityCacheIfRequested() logger.ZLogInformation($"{entityRegistry.GetAllEntities().Count} entities already cached"); if (entityRegistry.GetAllEntities().Count < 504732) { - worldEntityManager.LoadAllUnspawnedEntities(cancellationToken); + await worldEntityManager.LoadAllUnspawnedEntitiesAsync(cancellationToken); logger.ZLogInformation($"Saving newly cached entities."); - saveService.QueueSave(); + await saveService.QueueActionAsync(SaveService.ServiceAction.SAVE, cancellationToken); } logger.ZLogInformation($"All batches have now been loaded."); } @@ -161,8 +160,6 @@ internal bool Save(PersistedWorldData persistedData, string saveDir) Serializer.Serialize(Path.Combine(saveDir, $"GlobalRootData{FileEnding}"), persistedData.GlobalRootData); Serializer.Serialize(Path.Combine(saveDir, $"EntityData{FileEnding}"), persistedData.EntityData); - options.Value.Seed = persistedData.WorldData.Seed; - logger.ZLogInformation($"World state saved"); return true; } @@ -314,12 +311,9 @@ void EnsureChildrenTransformAreParented(WorldEntity entity) } // TODO: This method should be removed. Each service should load its own data instead of centralizing it here. - private void LoadPersistedWorldIntoServices(PersistedWorldData pWorldData) + private async Task LoadPersistedWorldIntoServicesAsync(PersistedWorldData pWorldData) { - string seed = pWorldData.WorldData.Seed ?? options.Value.Seed ?? throw new InvalidOperationException("World seed must not be null"); - // Initialized only once, just like UnityEngine.Random - XorRandom.InitSeed(seed.GetHashCode()); - + string seed = options.Value.Seed; logger.ZLogInformation($"Loading world with seed {seed}"); // Time @@ -327,7 +321,7 @@ private void LoadPersistedWorldIntoServices(PersistedWorldData pWorldData) // Entities entityRegistry.AddEntities(pWorldData.EntityData.Entities); entityRegistry.AddEntitiesIgnoringDuplicate(pWorldData.GlobalRootData.Entities.OfType().ToList()); - escapePodManager.AddKnownPods(entityRegistry.GetEntities()); + await escapePodManager.AddKnownPodsAsync(entityRegistry.GetEntities()); // TODO: hacky code - see WorldEntityManager for more information. List worldEntities = entityRegistry.GetEntities(); @@ -338,7 +332,7 @@ private void LoadPersistedWorldIntoServices(PersistedWorldData pWorldData) foreach (Player player in pWorldData.PlayerData.GetPlayers()) { - playerManager.AddPlayer(player); + playerManager.AddSavedPlayer(player); } batchEntitySpawner.SerializableParsedBatches = pWorldData.WorldData.ParsedBatchCells; // Pda @@ -355,7 +349,7 @@ private void LoadPersistedWorldIntoServices(PersistedWorldData pWorldData) logger.ZLogInformation($"World finished loading"); } - private bool LoadWorldFromSavePath(string saveDir) + private async Task LoadWorldFromSavePathAsync(string saveDir) { if (!Directory.Exists(saveDir) || !File.Exists(Path.Combine(saveDir, $"Version{FileEnding}"))) { @@ -370,11 +364,11 @@ private bool LoadWorldFromSavePath(string saveDir) { return false; } - LoadPersistedWorldIntoServices(persistedData); + await LoadPersistedWorldIntoServicesAsync(persistedData); return true; } - private void CreateAndLoadWorld() + private async Task CreateAndLoadWorldAsync() { PersistedWorldData pWorldData = new() { @@ -388,12 +382,11 @@ private void CreateAndLoadWorld() StoryGoals = new StoryGoalData(), StoryTiming = new StoryTimingData() }, - ParsedBatchCells = [], - Seed = options.Value.Seed + ParsedBatchCells = [] }, GlobalRootData = new GlobalRootData() }; - LoadPersistedWorldIntoServices(pWorldData); + await LoadPersistedWorldIntoServicesAsync(pWorldData); InitNewWorld(); void InitNewWorld() diff --git a/Nitrox.Server.Subnautica/Program.cs b/Nitrox.Server.Subnautica/Program.cs index 25770ca6d7..10011993b7 100644 --- a/Nitrox.Server.Subnautica/Program.cs +++ b/Nitrox.Server.Subnautica/Program.cs @@ -7,7 +7,7 @@ using Nitrox.Model.Networking; using Nitrox.Model.Platforms.Discovery; using Nitrox.Server.Subnautica.Models; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities.Spawning; +using Nitrox.Server.Subnautica.Models.Factories; using Nitrox.Server.Subnautica.Models.Serialization; using Nitrox.Server.Subnautica.Services; @@ -45,11 +45,15 @@ private static async Task StartupHostAsync(string[] args) .Build(); startOptions = new ServerStartOptions(); configuration.Bind(startOptions); - if (!GameInstallationFinder.FindGameCached(GameInfo.Subnautica)) + + if (startOptions.GamePath == null) { - throw new DirectoryNotFoundException("Could not find Subnautica installation."); + if (!GameInstallationFinder.FindGameCached(GameInfo.Subnautica)) + { + throw new DirectoryNotFoundException("Could not find Subnautica installation."); + } + startOptions.GamePath = NitroxUser.GamePath; } - startOptions.GamePath ??= NitroxUser.GamePath; startOptions.NitroxAppDataPath ??= NitroxUser.AppDataPath; startOptions.NitroxAssetsPath ??= NitroxUser.AssetsPath; @@ -101,27 +105,19 @@ private static async Task StartServerAsync(string[] args) .AddWorld() .AddSaving() .AddAppEvents() + .AddAdminFeatures() .AddKeyedSingleton("startup", serverStartStopWatch) .AddHostedSingletonService() .AddHostedSingletonService() .AddHostedSingletonService() .AddHostedSingletonService() - .AddHostedSingletonService() .AddHostedSingletonService() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() ; - IHost host = builder.Build(); - - // TODO: Remove the need for NitroxServiceLocator in server. -#pragma warning disable DIMA001 - NitroxServiceLocator.Locator = host.Services.GetRequiredService; - NitroxServiceLocator.OptionalLocator = host.Services.GetService; -#pragma warning restore DIMA001 - - await host.RunAsync(); + await builder.Build().RunAsync(); } } diff --git a/Nitrox.Server.Subnautica/Services/AutoSaveService.cs b/Nitrox.Server.Subnautica/Services/AutoSaveService.cs index 032927a492..732564ab37 100644 --- a/Nitrox.Server.Subnautica/Services/AutoSaveService.cs +++ b/Nitrox.Server.Subnautica/Services/AutoSaveService.cs @@ -42,7 +42,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } logger.ZLogTrace($"Requesting to save..."); - saveService.QueueSave(); + await saveService.QueueActionAsync(SaveService.ServiceAction.SAVE, stoppingToken); } } } diff --git a/Nitrox.Server.Subnautica/Services/CommandService.cs b/Nitrox.Server.Subnautica/Services/CommandService.cs index 53055fc18e..87664739a2 100644 --- a/Nitrox.Server.Subnautica/Services/CommandService.cs +++ b/Nitrox.Server.Subnautica/Services/CommandService.cs @@ -1,55 +1,309 @@ -extern alias JB; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using System.Threading.Channels; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Server.Subnautica.Models.Commands.Processor; +using Microsoft.Extensions.Logging.Abstractions; +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Server.Subnautica.Services; /// /// Enables processing of commands from a string. /// -internal sealed class CommandService(TextCommandProcessor textCommandProcessor, ILogger logger) : IHostedService +internal sealed partial class CommandService(CommandRegistry registry, ILogger logger, ILoggerFactory loggerFactory) : IHostedLifecycleService, ICommandSubmit { - private readonly TextCommandProcessor textCommandProcessor = textCommandProcessor; + private const int MAX_ARGS = 8; + private readonly ILogger logger = logger; + private readonly ILoggerFactory loggerFactory = loggerFactory; + + private readonly CommandRegistry registry = registry; private readonly Channel runningCommands = Channel.CreateUnbounded(); - private Task? commandWaiterTask; + private Task commandWaiterTask; + + [GeneratedRegex(@"""(?:[^""\\]|\\.)*""|\S+", RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + private static partial Regex ArgumentsRegex { get; } - public void ExecuteCommand(string command) + public bool ExecuteCommand(ReadOnlySpan inputText, ICommandContext context, out Task? commandTask) { - // TODO: Make ProcessCommand async - runningCommands.Writer.TryWrite(Task.Run(() => textCommandProcessor.ProcessCommand(command, Optional.Empty, Perms.HOST))); + commandTask = null; + inputText = inputText.Trim(); + if (inputText.IsEmpty) + { + return false; + } + + // Extract command name from command input. + int endOfNameIndex = inputText.IndexOf(' '); + if (endOfNameIndex == -1) + { + endOfNameIndex = inputText.Length; + } + ReadOnlySpan commandName = inputText[.. endOfNameIndex]; + + if (!registry.TryGetHandlersByCommandName(context, commandName, out List handlers)) + { + logger.ZLogInformation($"Unknown command {commandName.ToString():@CommandName}"); + return false; + } + + ReadOnlySpan commandArgs = inputText[endOfNameIndex ..].Trim(); + + // Get text ranges within the input that are potentially valid arguments. + Span ranges = stackalloc Range[MAX_ARGS]; + Regex.ValueMatchEnumerator argsEnumerator = ArgumentsRegex.EnumerateMatches(commandArgs); + int rangeIndex = 0; + while (argsEnumerator.MoveNext()) + { + ValueMatch match = argsEnumerator.Current; + if (rangeIndex >= MAX_ARGS) + { + logger.ZLogError($"Too many arguments passed to command {commandName.ToString():@CommandName}"); + return false; + } + Range newRange = new(match.Index, match.Index + match.Length); + + // Trim new range if it's a quoted string. + newRange = commandArgs[newRange] switch + { + ['"', .., '"'] => new Range(newRange.Start.Value + 1, newRange.End.Value - 1), + ['\'', .., '\''] => new Range(newRange.Start.Value + 1, newRange.End.Value - 1), + _ => newRange + }; + + ranges[rangeIndex++] = newRange; + } + + // Convert text ranges into object args. First arg is always the context (and should be accounted for in algorithm below). + Span args = new(new object[MAX_ARGS]) { [0] = context }; + CommandHandlerEntry? handler = null; + bool inputHasCorrectParameterCount = false; + List? almostMatchingHandlers = null; + foreach (CommandHandlerEntry currentHandler in handlers) + { + if (currentHandler.Parameters.Length != rangeIndex) + { + continue; + } + inputHasCorrectParameterCount = true; + for (int i = 0; i < currentHandler.ParameterTypes.Length; i++) + { + ReadOnlySpan part = commandArgs[ranges[i]]; + object? parsedValue = registry.TryParseToType(part, currentHandler.ParameterTypes[i]); + if (parsedValue == null) + { + almostMatchingHandlers ??= []; + almostMatchingHandlers.Add(currentHandler); + + // Unset args array except for first arg which is the context. + for (int j = i + 1; j >= 0; j--) + { + args[j] = null; + } + goto nextHandler; + } + args[i + 1] = parsedValue; + } + handler = currentHandler; + break; + + nextHandler: ; + } + if (!inputHasCorrectParameterCount) + { + // Try pass incorrect arguments to first handler with ONLY a string argument as a "catch all". + handler = handlers.FirstOrDefault(h => h.ParameterTypes.SequenceEqual([typeof(string)])); + if (handler != null) + { + args[1] = commandArgs.ToString(); + } + + // No catch-all handler, return help page... + if (handler == null) + { + logger.ZLogInformation($"Command {commandName.ToString():@CommandName} does not support the provided arguments. See below for more information."); + ExecuteCommand($"help {commandName}", context, out commandTask); + return false; + } + } + if (handler == null) + { + if (almostMatchingHandlers.Count == 0) + { + logger.ZLogInformation($"Command {commandName.ToString():@CommandName} failed"); + return false; + } + commandTask = QueueTryRunFirstArgConvertedHandler(context, almostMatchingHandlers, commandArgs.ToString(), ranges); + return true; + } + + RunHandler(handler, args[.. (handler.Parameters.Length + 1)], inputText.ToString()); + return true; } public Task StartAsync(CancellationToken cancellationToken) { - commandWaiterTask = Task.Factory.StartNew(async () => + commandWaiterTask = Task.Factory.StartNew(() => EnsureCommandsAreProcessedAsync(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + logger.ZLogTrace($"Waiting for commands to finish processing..."); + await commandWaiterTask; + logger.ZLogTrace($"Done waiting for commands"); + } + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) + { + logger.ZLogInformation($"To get help for commands, run help in console or /help in chatbox"); + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private async Task EnsureCommandsAreProcessedAsync(CancellationToken cancellationToken = default) + { + await foreach (Task task in runningCommands.Reader.ReadAllAsync(cancellationToken)) + { + if (task == null) + { + continue; + } + await task; + } + } + + private Task QueueTryRunFirstArgConvertedHandler(ICommandContext context, List looselyCompatibleHandlers, string argsInput, ReadOnlySpan argRanges) + { + List args = []; + foreach (Range range in argRanges) + { + if (range is { Start.Value: 0, End.Value: 0 }) + { + continue; + } + args.Add(argsInput[range]); + } + + Task tryRunHandlerTask = Task.Run(async () => { - await foreach (Task task in runningCommands.Reader.ReadAllAsync(cancellationToken)) + List<(CommandHandlerEntry handler, ConvertResult[][] conversions)> failedHandlers = []; + foreach (CommandHandlerEntry handler in looselyCompatibleHandlers) { - if (task == null) + ConvertResult[][] values = await TryConvertStringParamsToObjectParams(registry, args, handler.ParameterTypes); + if (values.Any(v => !v.LastOrDefault().Success)) { + failedHandlers.Add((handler, values)); continue; } - await task; + + RunHandler(handler, [context, ..values.Select(v => v.LastOrDefault().Value).ToArray()], argsInput); + return true; } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); - return Task.CompletedTask; + + logger.ZLogInformation($"Command '{$"{looselyCompatibleHandlers[0].Name} {argsInput}":@Command}' failed to match to any command handlers.{Environment.NewLine}{GetErrorMessagesFromFailedHandlers(failedHandlers)}"); + return false; + }); + runningCommands.Writer.TryWrite(tryRunHandlerTask); + return tryRunHandlerTask; + + static async Task TryConvertStringParamsToObjectParams(CommandRegistry registry, List args, Type[] parameterTypes) + { + List result = null; + for (int i = 0; i < parameterTypes.Length; i++) + { + ConvertResult[] conversions; + if (registry.TryParseToType(args[i], parameterTypes[i]) is { } parsedValue) + { + conversions = [ConvertResult.Ok(parsedValue)]; + } + else + { + conversions = [ConvertResult.Fail($"Failed to parse {args[i]} to a {parameterTypes[i].Name}")]; + } + if (conversions is [] || !conversions[0].Success) + { + conversions = [..conversions, ..await registry.TryConvertToType(args[i], parameterTypes[i])]; + } + + result ??= []; + result.Add(conversions); + } + return result?.ToArray() ?? []; + } + + static string GetErrorMessagesFromFailedHandlers(List<(CommandHandlerEntry handler, ConvertResult[][] conversions)> failedHandlers) + { + StringBuilder sb = new(); + int indent = 0; + foreach ((CommandHandlerEntry handler, ConvertResult[][] conversions) handlerResult in failedHandlers) + { + for (int argIndex = 0; argIndex < handlerResult.conversions.Length; argIndex++) + { + string[] messages = handlerResult.conversions[argIndex].Where(c => c is { Success: false, Value: string }).Select(c => c.Value).OfType().ToArray(); + if (messages is []) + { + continue; + } + + sb.Append("Arg ") + .Append(argIndex.ToString()) + .AppendLine(": "); + indent++; + for (int i = 0; i < messages.Length; i++) + { + sb.Append(GetIndentText()).Append("- ").Append(messages[i]); + if (i != messages.Length - 1) + { + sb.AppendLine(); + } + } + indent--; + sb.AppendLine(); + } + } + return sb.ToString(); + + string GetIndentText() => new(' ', indent * 4); + } } - public async Task StopAsync(CancellationToken cancellationToken) + private void RunHandler(CommandHandlerEntry handler, Span args, string inputText) { - if (commandWaiterTask == null) + ILogger? commandLogger = NullLogger.Instance; + if (args.Length > 0 && args[0] is ICommandContext context) { - return; + commandLogger = context.Logger = loggerFactory.CreateLogger(handler.Owner.GetType()); } - if (commandWaiterTask.IsCompletedSuccessfully) + + try { - return; + if (!runningCommands.Writer.TryWrite(RunHandlerWithExceptionLoggingAsync(commandLogger, handler, args.ToArray(), inputText))) + { + logger.ZLogError($"Failed to track command task"); + } + } + catch (Exception ex) + { + logger.ZLogError(ex, $"Error occurred while executing command {inputText:@Command}"); } - logger.ZLogTrace($"Waiting for commands to finish processing..."); - await commandWaiterTask; - logger.ZLogTrace($"Done waiting for commands"); + static async Task RunHandlerWithExceptionLoggingAsync(ILogger logger, CommandHandlerEntry handler, object[] args, string inputText) + { + try + { + await handler.InvokeAsync(args); + } + catch (Exception ex) + { + logger.ZLogError(ex, $"Error occurred while executing command '{inputText:@Command}'"); + } + } } } diff --git a/Nitrox.Server.Subnautica/Services/ConsoleInputService.cs b/Nitrox.Server.Subnautica/Services/ConsoleInputService.cs index 03f839f621..efc50b4843 100644 --- a/Nitrox.Server.Subnautica/Services/ConsoleInputService.cs +++ b/Nitrox.Server.Subnautica/Services/ConsoleInputService.cs @@ -1,17 +1,21 @@ using System.Runtime.InteropServices; using System.Text; using Nitrox.Model.DataStructures; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Services; /// /// Reads console input and handles input history. /// -internal sealed class ConsoleInputService(CommandService commandService, IHostApplicationLifetime appLifetime, ILogger logger) : BackgroundService +internal sealed class ConsoleInputService(CommandService commandService, IPacketSender packetSender, IHostApplicationLifetime appLifetime, ILogger logger) : BackgroundService { + private readonly IHostApplicationLifetime appLifetime = appLifetime; private readonly CommandService commandService = commandService; private readonly CircularBuffer inputHistory = new(1000); private readonly ILogger logger = logger; + private readonly IPacketSender packetSender = packetSender; private int currentHistoryIndex; protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -36,7 +40,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - private void SubmitInput(string input) => commandService.ExecuteCommand(input); + private void SubmitInput(string input) => commandService.ExecuteCommand(input, new HostToServerCommandContext(packetSender), out _); private async Task HandleInputAsync(CancellationToken cancellationToken) { @@ -174,7 +178,7 @@ private async Task HandleInputAsync(CancellationToken cancellationToken) { inputLineBuilder.Insert(Console.CursorLeft - 1, keyInfo.KeyChar); } - catch (IndexOutOfRangeException) + catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentOutOfRangeException) { // ignored } diff --git a/Nitrox.Server.Subnautica/Services/Core/QueingBackgroundService.cs b/Nitrox.Server.Subnautica/Services/Core/QueingBackgroundService.cs new file mode 100644 index 0000000000..1a1b5c9993 --- /dev/null +++ b/Nitrox.Server.Subnautica/Services/Core/QueingBackgroundService.cs @@ -0,0 +1,23 @@ +using System.Threading.Channels; + +namespace Nitrox.Server.Subnautica.Services.Core; + +/// +/// A background service that queues actions to be processed one after another, with the goal to eliminate race conditions. +/// +internal abstract class QueuingBackgroundService : BackgroundService +{ + private readonly Channel actionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); + + protected abstract Task ExecuteQueuedActionAsync(TActionEntry action, CancellationToken stoppingToken); + + public ValueTask QueueActionAsync(TActionEntry item, CancellationToken cancellationToken = default) => actionQueue.Writer.WriteAsync(item, cancellationToken); + + protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (TActionEntry action in actionQueue.Reader.ReadAllAsync(stoppingToken)) + { + await ExecuteQueuedActionAsync(action, stoppingToken); + } + } +} diff --git a/Nitrox.Server.Subnautica/Services/FmodService.cs b/Nitrox.Server.Subnautica/Services/FmodService.cs index bb6539b4c6..11107fcdff 100644 --- a/Nitrox.Server.Subnautica/Services/FmodService.cs +++ b/Nitrox.Server.Subnautica/Services/FmodService.cs @@ -2,7 +2,7 @@ namespace Nitrox.Server.Subnautica.Services; -sealed class FmodService(GameInfo gameInfo) : IHostedService +internal sealed class FmodService(GameInfo gameInfo) : IHostedService { private FMODWhitelist? whitelist; private readonly GameInfo gameInfo = gameInfo; diff --git a/Nitrox.Server.Subnautica/Services/HibernateService.cs b/Nitrox.Server.Subnautica/Services/HibernateService.cs index b060072492..8eaa5fc4a5 100644 --- a/Nitrox.Server.Subnautica/Services/HibernateService.cs +++ b/Nitrox.Server.Subnautica/Services/HibernateService.cs @@ -23,13 +23,13 @@ public async Task SleepAsync() { return; } - logger.ZLogTrace($"No players connected, entering power saving mode..."); + logger.ZLogTrace($"Server has paused, waiting for players to connect"); IsSleeping = true; await sleepTrigger.InvokeAsync(); } /// - /// Wakes up the server which will enable and simulate all features. + /// Wakes up the server which will enable and simulate all features. If not sleeping, this call won't do anything. /// public async Task WakeAsync() { @@ -37,7 +37,7 @@ public async Task WakeAsync() { return; } - logger.ZLogInformation($"Server has paused, waiting for players to connect"); + logger.ZLogTrace($"Server is entering normal operation due to at least one player playing"); IsSleeping = false; await wakeTrigger.InvokeAsync(); } diff --git a/Nitrox.Server.Subnautica/Services/LanBroadcastService.cs b/Nitrox.Server.Subnautica/Services/LanBroadcastService.cs index fee6f7c3ea..c262ec1585 100644 --- a/Nitrox.Server.Subnautica/Services/LanBroadcastService.cs +++ b/Nitrox.Server.Subnautica/Services/LanBroadcastService.cs @@ -9,7 +9,7 @@ namespace Nitrox.Server.Subnautica.Services; /// Broadcasts the server port over LAN. Clients listening for this broadcast automatically add this server to the /// server list. /// -internal class LanBroadcastService(IOptionsMonitor optionsProvider, ILogger logger) : BackgroundService +internal sealed class LanBroadcastService(IOptionsMonitor optionsProvider, ILogger logger) : BackgroundService { private const int ACTIVE_POLL_INTERVAL_MS = 100; private const int INACTIVE_POLL_INTERVAL_MS = (int)(5 * TimeSpan.MillisecondsPerSecond); diff --git a/Nitrox.Server.Subnautica/Services/MemoryService.cs b/Nitrox.Server.Subnautica/Services/MemoryService.cs index 406f962347..94245ec0cd 100644 --- a/Nitrox.Server.Subnautica/Services/MemoryService.cs +++ b/Nitrox.Server.Subnautica/Services/MemoryService.cs @@ -1,3 +1,5 @@ +using Nitrox.Server.Subnautica.Services.Core; + namespace Nitrox.Server.Subnautica.Services; /// @@ -6,28 +8,17 @@ namespace Nitrox.Server.Subnautica.Services; /// That's where this comes in, to force the GC to check if any file handler (pointers) /// are reachable and if not, deallocate them. /// -internal sealed class MemoryService(ILogger logger) : BackgroundService +internal sealed class MemoryService(ILogger logger) : QueuingBackgroundService { private readonly ILogger logger = logger; - private readonly AsyncBarrier memCompactBarrier = new(); - - /// - /// Queues a memory compaction loop to be executed as soon as possible. - /// - /// - /// This forces the GC to check on dangling memory in case of bad memory management. Ideally, this method should not exist. - /// - public void QueueCompact() - { - memCompactBarrier.Signal(); - } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteQueuedActionAsync(ServiceAction action, CancellationToken stoppingToken) { - while (!stoppingToken.IsCancellationRequested) + switch (action) { - await memCompactBarrier.WaitForSignalAsync(stoppingToken); - await CompactMemoryAsync(stoppingToken); + case ServiceAction.COMPACT_MEMORY: + await CompactMemoryAsync(stoppingToken); + break; } } @@ -63,4 +54,9 @@ private async Task CompactMemoryAsync(CancellationToken cancellationToken) } logger.ZLogTrace($"Stopped compacting memory"); } + + internal enum ServiceAction + { + COMPACT_MEMORY + } } diff --git a/Nitrox.Server.Subnautica/Services/PacketRegistryService.cs b/Nitrox.Server.Subnautica/Services/PacketRegistryService.cs new file mode 100644 index 0000000000..a2174c5c81 --- /dev/null +++ b/Nitrox.Server.Subnautica/Services/PacketRegistryService.cs @@ -0,0 +1,25 @@ +extern alias JB; +using Nitrox.Model.Packets.Core; + +namespace Nitrox.Server.Subnautica.Services; + +/// +/// Collects packet processors into a fast lookup, based on the packet type they can handle. +/// +internal sealed class PacketRegistryService(Func packetProcessorsProvider) : IHostedService +{ + private PacketProcessorsInvoker packetProcessorsInvoker; + private readonly Func packetProcessorsProvider = packetProcessorsProvider; + private PacketProcessorsInvoker.Entry defaultProcessor; + + public Task StartAsync(CancellationToken cancellationToken) + { + packetProcessorsInvoker = new PacketProcessorsInvoker(packetProcessorsProvider()); + defaultProcessor = packetProcessorsInvoker.GetProcessor(typeof(Packet)) ?? throw new InvalidOperationException("A default packet processor must be set"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public PacketProcessorsInvoker.Entry GetProcessor(Type packetType) => packetProcessorsInvoker.GetProcessor(packetType) ?? defaultProcessor; +} diff --git a/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs b/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs new file mode 100644 index 0000000000..96d00cfaab --- /dev/null +++ b/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs @@ -0,0 +1,86 @@ +using System.IO; + +namespace Nitrox.Server.Subnautica.Services; + +internal sealed class PacketSerializationService : BackgroundService +{ + private readonly TaskCompletionSource init; + private readonly ILogger logger; + private IPacketSerializer inner; + private readonly Lock innerLock = new(); + + public PacketSerializationService(ILogger logger) + { + this.logger = logger; + + init = new TaskCompletionSource(); + inner = new UnloadedSerializer(init, serializer => + { + lock (innerLock) + { + inner = serializer; + } + }); + } + + public void SerializeInto(Packet packet, Stream stream) + { + IPacketSerializer actual; + lock (innerLock) + { + actual = inner; + } + actual.SerializeInto(packet, stream); + } + + public override void Dispose() + { + if (init.Task.IsCompleted) + { + init.Task.Dispose(); + } + base.Dispose(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Run(() => + { + try + { + Packet.InitSerializer(); + } + catch (Exception ex) + { + init.TrySetException(ex); + } + if (!init.TrySetResult()) + { + throw new Exception("Failed to set init result"); + } + }, stoppingToken).ContinueWithHandleError(exception => logger.ZLogCritical(exception, $"Failed to initialize packet serializer")); + } + + private sealed class UnloadedSerializer(TaskCompletionSource init, Action implementationSwapper) : IPacketSerializer + { + public void SerializeInto(Packet packet, Stream stream) + { + if (!init.Task.IsCompletedSuccessfully) + { + init.Task.Wait(TimeSpan.FromSeconds(10)); + } + packet.SerializeInto(stream); + implementationSwapper(new LoadedSerializer()); + } + } + + private sealed class LoadedSerializer : IPacketSerializer + { + public void SerializeInto(Packet packet, Stream stream) => packet.SerializeInto(stream); + } + + private interface IPacketSerializer + { + void SerializeInto(Packet packet, Stream stream); + } +} diff --git a/Nitrox.Server.Subnautica/Services/PersistNitroxSerializableConfigService.cs b/Nitrox.Server.Subnautica/Services/PersistNitroxSerializableConfigService.cs new file mode 100644 index 0000000000..5f2e11cd47 --- /dev/null +++ b/Nitrox.Server.Subnautica/Services/PersistNitroxSerializableConfigService.cs @@ -0,0 +1,25 @@ +using System.IO; +using Nitrox.Model.Serialization; +using Nitrox.Server.Subnautica.Models.AppEvents; + +namespace Nitrox.Server.Subnautica.Services; + +/// +/// Saves to their file in the same save +/// directory of the active server. +/// +internal sealed class PersistNitroxSerializableConfigService(IOptions options) : IHostedService, ISaveState +{ + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task OnEventAsync(ISaveState.Args args) + { + await Task.Run(() => + { + string targetFilePath = Path.Combine(args.SavePath, SerializableFileNameAttribute.GetFileName()); + NitroxConfig.CreateFile(targetFilePath, options.Value); + }); + } +} diff --git a/Nitrox.Server.Subnautica/Services/SaveService.cs b/Nitrox.Server.Subnautica/Services/SaveService.cs index ac5968f4ba..dc6c563022 100644 --- a/Nitrox.Server.Subnautica/Services/SaveService.cs +++ b/Nitrox.Server.Subnautica/Services/SaveService.cs @@ -3,39 +3,37 @@ using System.Linq; using Nitrox.Model.Constants; using Nitrox.Model.Platforms.OS.Shared; +using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.Serialization.World; +using Nitrox.Server.Subnautica.Services.Core; namespace Nitrox.Server.Subnautica.Services; -internal sealed class SaveService(Func worldServiceProvider, IOptions options, IOptions startOptions, ILogger logger) : BackgroundService, IHostedLifecycleService +internal sealed class SaveService(Func worldServiceProvider, ISaveState.Trigger saveStateTrigger, IOptions options, IOptions startOptions, ILogger logger) : QueuingBackgroundService, IHostedLifecycleService { private readonly Func worldServiceProvider = worldServiceProvider; + private readonly ISaveState.Trigger saveStateTrigger = saveStateTrigger; private readonly IOptions startOptions = startOptions; private readonly IOptions options = options; private readonly ILogger logger = logger; - private readonly AsyncBarrier saveBarrier = new(); - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteQueuedActionAsync(ServiceAction action, CancellationToken stoppingToken) { - while (!stoppingToken.IsCancellationRequested) + switch (action) { - await saveBarrier.WaitForSignalAsync(stoppingToken); - - string savePath = startOptions.Value.GetServerSavePath(); - if (!worldServiceProvider().Save(savePath)) - { - continue; - } - ExecutePostSaveCommand(); - BackUp(savePath); + case ServiceAction.SAVE: + string savePath = startOptions.Value.GetServerSavePath(); + if (!worldServiceProvider().Save(savePath)) + { + return; + } + await saveStateTrigger.InvokeAsync(new ISaveState.Args(savePath)); + ExecutePostSaveCommand(); + BackUp(savePath); + break; } } - public void QueueSave() - { - saveBarrier.Signal(); - } - private void ExecutePostSaveCommand() { string postSaveCommandPath = options.Value.PostSaveCommandPath; @@ -121,17 +119,14 @@ private void BackUp(string saveDir) public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task StartedAsync(CancellationToken cancellationToken) - { - QueueSave(); - return Task.CompletedTask; - } + public async Task StartedAsync(CancellationToken cancellationToken) => await QueueActionAsync(ServiceAction.SAVE, cancellationToken); - public Task StoppingAsync(CancellationToken cancellationToken) - { - QueueSave(); - return Task.CompletedTask; - } + public async Task StoppingAsync(CancellationToken cancellationToken) => await QueueActionAsync(ServiceAction.SAVE, cancellationToken); public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + internal enum ServiceAction + { + SAVE + } } diff --git a/Nitrox.Server.Subnautica/Services/ServersManagementService.cs b/Nitrox.Server.Subnautica/Services/ServersManagementService.cs index 5014852f8e..b75a07a609 100644 --- a/Nitrox.Server.Subnautica/Services/ServersManagementService.cs +++ b/Nitrox.Server.Subnautica/Services/ServersManagementService.cs @@ -5,23 +5,22 @@ using Grpc.Net.Client; using MagicOnion.Client; using Nitrox.Model.Constants; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.MagicOnion; -using Nitrox.Server.Subnautica.Models.Commands.Processor; +using Nitrox.Server.Subnautica.Models.Commands.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.Logging.Scopes; using Nitrox.Server.Subnautica.Models.Logging.ZLogger; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Services; /// /// Connects to a locally running app that might want to track this server. Nitrox.Launcher is expected. /// -internal sealed class ServersManagementService(PlayerManager playerManager, TextCommandProcessor commandProcessor, IOptions options, ILogger logger) : BackgroundService +internal sealed class ServersManagementService(PlayerManager playerManager, IPacketSender packetSender, CommandService commandProcessor, IOptions options, ILogger logger) : BackgroundService { public static readonly Channel LogQueue = Channel.CreateBounded(new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.DropOldest }); - private readonly TextCommandProcessor commandProcessor = commandProcessor; + private readonly CommandService commandProcessor = commandProcessor; private readonly ILogger logger = logger; private readonly IOptions options = options; private readonly PlayerManager playerManager = playerManager; @@ -32,7 +31,7 @@ internal sealed class ServersManagementService(PlayerManager playerManager, Text protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - ServerManagementReceiver? receiver = new(commandProcessor); + ServerManagementReceiver? receiver = new(commandProcessor, packetSender); IServersManagement api = null; using PeriodicTimer refreshTimer = new(TimeSpan.FromSeconds(5)); @@ -46,25 +45,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await WaitNextAsync(); continue; } - // Init gRPC channel or update - if (channel == null || api == null || !channel.Target.EndsWith(launcherGrpcPortAsync.ToString(), StringComparison.Ordinal)) - { - channel?.Dispose(); - channel = GrpcChannel.ForAddress($"http://localhost:{launcherGrpcPortAsync}"); - if (api == null) - { - StreamingHubClientOptions grpcOptions = StreamingHubClientOptions.CreateWithDefault() - .WithCallOptions(new CallOptions(new Metadata - { - { "ProcessId", Environment.ProcessId.ToString() }, - { "SaveName", options.Value.SaveName } - })); - api = await StreamingHubClient.ConnectAsync(channel, - receiver, - cancellationToken: stoppingToken, - options: grpcOptions); - } - } + await RefreshConnectionAsync(launcherGrpcPortAsync); // Push data await PushPollDataAsync(api); @@ -85,6 +66,35 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) ValueTask WaitNextAsync() => refreshTimer.WaitForNextTickAsync(stoppingToken); + async Task RefreshConnectionAsync(int? grpcPort) + { + if (channel?.Target.EndsWith(grpcPort.ToString(), StringComparison.Ordinal) == false) + { + channel?.Dispose(); + channel = null; + if (api != null) + { + await api.DisposeAsync(); + } + api = null; + } + + channel ??= GrpcChannel.ForAddress($"http://localhost:{grpcPort}"); + if (api == null) + { + StreamingHubClientOptions grpcOptions = StreamingHubClientOptions.CreateWithDefault() + .WithCallOptions(new CallOptions(new Metadata + { + { "ProcessId", Environment.ProcessId.ToString() }, + { "SaveName", options.Value.SaveName } + })); + api = await StreamingHubClient.ConnectAsync(channel, + receiver, + cancellationToken: stoppingToken, + options: grpcOptions); + } + } + Task CreateLoopingTask(Func action, IServersManagement service, CancellationToken cancellationToken) => Task.Run(async () => { @@ -165,9 +175,9 @@ private bool ShouldIgnoreException(Exception ex) return null; } - private class ServerManagementReceiver(TextCommandProcessor commandProcessor) : IServerManagementReceiver + private class ServerManagementReceiver(CommandService commandProcessor, IPacketSender packetSender) : IServerManagementReceiver { - public void OnCommand(string command) => commandProcessor.ProcessCommand(command, Optional.Empty, Perms.HOST); + public void OnCommand(string command) => commandProcessor.ExecuteCommand(command, new HostToServerCommandContext(packetSender), out _); } internal record LogEntry(IZLoggerEntry Entry, IZLoggerFormatter Formatter, ZLoggerPlainOptions.LogGeneratorCall Generator, ArrayBufferWriter Writer); diff --git a/Nitrox.Server.Subnautica/Services/StatusService.cs b/Nitrox.Server.Subnautica/Services/StatusService.cs index 96fc53c919..f01238c6aa 100644 --- a/Nitrox.Server.Subnautica/Services/StatusService.cs +++ b/Nitrox.Server.Subnautica/Services/StatusService.cs @@ -5,17 +5,18 @@ using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Server.Subnautica.Models.AppEvents; using Nitrox.Server.Subnautica.Models.AppEvents.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.Logging.Scopes; +using Nitrox.Server.Subnautica.Models.Packets.Core; namespace Nitrox.Server.Subnautica.Services; /// -/// Service which prints out information at appropriate time in the app life cycle. +/// Service which prints out information at an appropriate time in the app life cycle. /// internal sealed class StatusService( [FromKeyedServices("startup")] Stopwatch appStartStopWatch, GameInfo gameInfo, - PlayerManager playerManager, + IPacketSender packetSender, ISummarize.Trigger summarizeTrigger, IOptions options, IOptions startOptions, @@ -24,7 +25,7 @@ internal sealed class StatusService( private readonly GameInfo gameInfo = gameInfo; private readonly ILogger logger = logger; private readonly IOptions options = options; - private readonly PlayerManager playerManager = playerManager; + private readonly IPacketSender packetSender = packetSender; private readonly IOptions startOptions = startOptions; private readonly ISummarize.Trigger summarizeTrigger = summarizeTrigger; @@ -55,30 +56,36 @@ public async Task StartedAsync(CancellationToken cancellationToken) async Task LogIps() { - logger.ZLogInformation($"Use IP to connect:"); - using (logger.BeginPlainScope()) + // Capture and log so that logs are written in one go. This prevents different log lines being inserted in-between. + string logMessage; + using (CaptureScope captureScope = logger.BeginCaptureScope()) { - using (logger.BeginPrefixScope("\t")) + using (logger.BeginPlainScope()) { - logger.ZLogInformation($"{IPAddress.Loopback} - You (Local)"); - foreach ((IPAddress address, NetHelper.MachineIpOrigin origin, string? networkName) in await NetHelper.GetAllKnownIpsAsync()) + logger.ZLogInformation($"Use IP to connect:"); + using (logger.BeginPrefixScope("\t")) { - switch (origin) + logger.ZLogInformation($"{IPAddress.Loopback} - You (Local)"); + foreach ((IPAddress address, NetHelper.MachineIpOrigin origin, string? networkName) in await NetHelper.GetAllKnownIpsAsync()) { - case NetHelper.MachineIpOrigin.LAN: - logger.LogLanIp(address); - break; - case NetHelper.MachineIpOrigin.VPN: - logger.LogVpnIp(networkName!, address); - break; - case NetHelper.MachineIpOrigin.WAN: - logger.LogWanIp(address); - break; + switch (origin) + { + case NetHelper.MachineIpOrigin.LAN: + logger.LogLanIp(address); + break; + case NetHelper.MachineIpOrigin.VPN: + logger.LogVpnIp(networkName!, address); + break; + case NetHelper.MachineIpOrigin.WAN: + logger.LogWanIp(address); + break; + } } } } - logger.ZLogInformation($""); + logMessage = $"{string.Join("", captureScope.Logs).Trim(Environment.NewLine)}{Environment.NewLine}"; } + logger.ZLogInformation($"{logMessage}"); } } @@ -86,7 +93,7 @@ public Task StoppingAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); logger.ZLogInformation($"Server is stopping..."); - playerManager.SendPacketToAllPlayers(new ChatMessage(ChatMessage.SERVER_ID, "[BROADCAST] Server is shutting down...")); + packetSender.SendPacketToAllAsync(new ChatMessage(SessionId.SERVER_ID, "[BROADCAST] Server is shutting down...")); return Task.CompletedTask; } diff --git a/Nitrox.Server.Subnautica/Services/SubnauticaResourceLoaderService.cs b/Nitrox.Server.Subnautica/Services/SubnauticaResourceLoaderService.cs index d15bc7c04e..4c839136d7 100644 --- a/Nitrox.Server.Subnautica/Services/SubnauticaResourceLoaderService.cs +++ b/Nitrox.Server.Subnautica/Services/SubnauticaResourceLoaderService.cs @@ -27,7 +27,7 @@ await Parallel.ForEachAsync(resources, cancellationToken, async (resource, token }); logger.ZLogDebug($"All resources loaded in {Math.Round(totalStopWatch.Elapsed.TotalSeconds, 3):@Seconds} seconds"); - memoryService.QueueCompact(); // AssetsTools.NET holds open file handlers without a way to close them. We ask the GC to check and dealloc this memory for us. + await memoryService.QueueActionAsync(MemoryService.ServiceAction.COMPACT_MEMORY, cancellationToken); // AssetsTools.NET holds open file handlers without a way to close them. We ask the GC to check and dealloc this memory for us. } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs b/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs index 91ce722bcc..9aceab9eda 100644 --- a/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs +++ b/Nitrox.Test/Client/Communication/DeferredPacketReceiverTest.cs @@ -22,6 +22,6 @@ public void NonActionPacket() storedPacket.Should().NotBeNull(); packetReceiver.GetNextPacket().Should().BeNull(); storedPacket.Should().Be(packet); - packet.PlayerId.Should().Be(PLAYER_ID); + packet.SessionId.Should().Be(PLAYER_ID); } } diff --git a/Nitrox.Test/Client/Communication/TestNonActionPacket.cs b/Nitrox.Test/Client/Communication/TestNonActionPacket.cs index 6abe3efaf8..c1b8b5aad9 100644 --- a/Nitrox.Test/Client/Communication/TestNonActionPacket.cs +++ b/Nitrox.Test/Client/Communication/TestNonActionPacket.cs @@ -1,15 +1,10 @@ -using Nitrox.Model.Packets; +using Nitrox.Model.Core; +using Nitrox.Model.Packets; -namespace Nitrox.Test.Client.Communication -{ - [Serializable] - public class TestNonActionPacket : Packet - { - public ushort PlayerId { get; } +namespace Nitrox.Test.Client.Communication; - public TestNonActionPacket(ushort playerId) - { - PlayerId = playerId; - } - } +[Serializable] +public class TestNonActionPacket(SessionId sessionId) : Packet +{ + public SessionId SessionId { get; } = sessionId; } diff --git a/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs b/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs index bb7b1b7f67..b4fef98a28 100644 --- a/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs +++ b/Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs @@ -1,9 +1,8 @@ using Nitrox.Model.Subnautica.Logger; using Nitrox.Model.Packets; -using Nitrox.Model.Packets.Processors.Abstract; -using Nitrox.Server.Subnautica; +using Nitrox.Model.Packets.Core; using Nitrox.Server.Subnautica.Models; -using Nitrox.Server.Subnautica.Models.Commands.Abstract; +using Nitrox.Server.Subnautica.Models.Commands.Core; namespace Nitrox.Test.Helper.Faker; @@ -14,7 +13,7 @@ public class NitroxAbstractFaker : NitroxFaker, INitroxFaker static NitroxAbstractFaker() { Assembly[] assemblies = [typeof(Packet).Assembly, typeof(SubnauticaInGameLogger).Assembly, typeof(ConsoleUnhandledErrorHandler).Assembly]; - HashSet blacklistedTypes = [typeof(Packet), typeof(CorrelatedPacket), typeof(Command), typeof(PacketProcessor)]; + HashSet blacklistedTypes = [typeof(Packet), typeof(CorrelatedPacket), typeof(ICommandHandler), typeof(IPacketProcessor)]; List types = new(); foreach (Assembly assembly in assemblies) @@ -40,7 +39,7 @@ public NitroxAbstractFaker(Type type) if (!subtypesByBaseType.TryGetValue(type, out Type[] subTypes)) { - throw new ArgumentException($"Argument is not contained in {nameof(subtypesByBaseType)}", nameof(type)); + throw new ArgumentException($"Argument '{type}' is not contained in {nameof(subtypesByBaseType)}", nameof(type)); } OutputType = type; diff --git a/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs b/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs index 4e58f9ab99..684d137d60 100644 --- a/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs +++ b/Nitrox.Test/Helper/Faker/NitroxCollectionFaker.cs @@ -4,86 +4,13 @@ namespace Nitrox.Test.Helper.Faker; public class NitroxCollectionFaker : NitroxFaker, INitroxFaker { - public enum CollectionType - { - NONE, - ARRAY, - LIST, - DICTIONARY, - SET, - QUEUE - } - private const int DEFAULT_SIZE = 2; + private readonly Func, object> generateAction; - public static bool IsCollection(Type t, out CollectionType collectionType) - { - if (t.IsArray && t.GetArrayRank() == 1) - { - collectionType = CollectionType.ARRAY; - return true; - } - - if (t.IsGenericType) - { - Type[] genericInterfacesDefinition = t.GetInterfaces() - .Where(i => i.IsGenericType) - .Select(i => i.GetGenericTypeDefinition()) - .ToArray(); - - if (genericInterfacesDefinition.Any(i => i == typeof(IList<>))) - { - collectionType = CollectionType.LIST; - return true; - } - - if (genericInterfacesDefinition.Any(i => i == typeof(IDictionary<,>))) - { - collectionType = CollectionType.DICTIONARY; - return true; - } - - if (genericInterfacesDefinition.Any(i => i == typeof(ISet<>))) - { - collectionType = CollectionType.SET; - return true; - } - - Type genericTypeDefinition = t.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(Queue<>) || genericTypeDefinition == typeof(ThreadSafeQueue<>)) // Queue has no defining interface - { - collectionType = CollectionType.QUEUE; - return true; - } - } - - collectionType = CollectionType.NONE; - return false; - } - - public static bool TryGetCollectionTypes(Type type, out Type[] types) - { - if (!IsCollection(type, out CollectionType collectionType)) - { - types = []; - return false; - } - - if (collectionType == CollectionType.ARRAY) - { - types = [type.GetElementType()]; - return true; - } - - types = type.GenericTypeArguments; - return true; - } + private readonly INitroxFaker[] subFakers; public int GenerateSize = DEFAULT_SIZE; - public Type OutputCollectionType; - - private readonly INitroxFaker[] subFakers; - private readonly Func, object> generateAction; + public readonly Type OutputCollectionType; public NitroxCollectionFaker(Type type, CollectionType collectionType) { @@ -93,7 +20,7 @@ public NitroxCollectionFaker(Type type, CollectionType collectionType) switch (collectionType) { case CollectionType.ARRAY: - Type arrayType = OutputType = type.GetElementType(); + Type arrayType = OutputType = type.GetElementType() ?? type.GetGenericArguments()[0]; elementFaker = GetOrCreateFaker(arrayType); subFakers = [elementFaker]; @@ -196,7 +123,86 @@ public NitroxCollectionFaker(Type type, CollectionType collectionType) } } + public static bool IsCollection(Type t, out CollectionType collectionType) + { + if (t.IsArray && t.GetArrayRank() == 1) + { + collectionType = CollectionType.ARRAY; + return true; + } + + if (t.IsGenericType) + { + if (t.GetGenericTypeDefinition() == typeof(IList<>)) + { + collectionType = CollectionType.ARRAY; + return true; + } + + Type[] genericInterfacesDefinition = t.GetInterfaces() + .Where(i => i.IsGenericType) + .Select(i => i.GetGenericTypeDefinition()) + .ToArray(); + + if (genericInterfacesDefinition.Any(i => i == typeof(IList<>))) + { + collectionType = CollectionType.LIST; + return true; + } + + if (genericInterfacesDefinition.Any(i => i == typeof(IDictionary<,>))) + { + collectionType = CollectionType.DICTIONARY; + return true; + } + + if (genericInterfacesDefinition.Any(i => i == typeof(ISet<>))) + { + collectionType = CollectionType.SET; + return true; + } + + Type genericTypeDefinition = t.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(Queue<>) || genericTypeDefinition == typeof(ThreadSafeQueue<>)) // Queue has no defining interface + { + collectionType = CollectionType.QUEUE; + return true; + } + } + + collectionType = CollectionType.NONE; + return false; + } + + public static bool TryGetCollectionTypes(Type type, out Type[] types) + { + if (!IsCollection(type, out CollectionType collectionType)) + { + types = []; + return false; + } + + if (collectionType == CollectionType.ARRAY) + { + types = [type.GetElementType()]; + return true; + } + + types = type.GenericTypeArguments; + return true; + } + public INitroxFaker[] GetSubFakers() => subFakers; public object GenerateUnsafe(HashSet typeTree) => generateAction.Invoke(typeTree); + + public enum CollectionType + { + NONE, + ARRAY, + LIST, + DICTIONARY, + SET, + QUEUE + } } diff --git a/Nitrox.Test/Helper/Faker/NitroxFaker.cs b/Nitrox.Test/Helper/Faker/NitroxFaker.cs index 8bdb450903..a35ca04a38 100644 --- a/Nitrox.Test/Helper/Faker/NitroxFaker.cs +++ b/Nitrox.Test/Helper/Faker/NitroxFaker.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -40,6 +41,8 @@ static NitroxFaker() { typeof(string), new NitroxActionFaker(typeof(string), f => f.Random.Word()) }, // Nitrox types + { typeof(PeerId), new NitroxActionFaker(typeof(PeerId), f => (PeerId)f.Random.UInt() )}, + { typeof(SessionId), new NitroxActionFaker(typeof(SessionId), f => (SessionId)f.Random.UShort() )}, { typeof(NitroxTechType), new NitroxActionFaker(typeof(NitroxTechType), f => new NitroxTechType(f.PickRandom().ToString())) }, { typeof(NitroxId), new NitroxActionFaker(typeof(NitroxId), f => new NitroxId(f.Random.Guid())) }, }; @@ -51,49 +54,42 @@ public static INitroxFaker GetOrCreateFaker(Type t) protected static INitroxFaker CreateFaker(Type type) { - if (type.IsAbstract) + switch (type) { - return new NitroxAbstractFaker(type); - } - - if (type.IsEnum) - { - return new NitroxActionFaker(type, f => - { - string[] selection = Enum.GetNames(type); - if (selection.Length == 0) + case { IsEnum: true }: + return new NitroxActionFaker(type, f => { - throw new ArgumentException("There are no enum values after exclusion to choose from."); + string[] selection = Enum.GetNames(type); + if (selection.Length == 0) + { + throw new ArgumentException("There are no enum values after exclusion to choose from."); + } + + string val = f.Random.ArrayElement(selection); + return Enum.Parse(type, val); + }); + case not null when NitroxCollectionFaker.IsCollection(type, out NitroxCollectionFaker.CollectionType collectionType): + return new NitroxCollectionFaker(type, collectionType); + case { IsAbstract: true }: + return new NitroxAbstractFaker(type); + case { IsGenericType: true } when type.GetGenericTypeDefinition() is { } genericTypeDefinition: + if (genericTypeDefinition == typeof(Optional<>)) + { + return new NitroxOptionalFaker(type); } - - string val = f.Random.ArrayElement(selection); - return Enum.Parse(type, val); - }); - } - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) - { - return new NitroxOptionalFaker(type); - } - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return new NitroxNullableFaker(type); - } - - if (NitroxCollectionFaker.IsCollection(type, out NitroxCollectionFaker.CollectionType collectionType)) - { - return new NitroxCollectionFaker(type, collectionType); + if (genericTypeDefinition == typeof(Nullable<>)) + { + return new NitroxNullableFaker(type); + } + break; } - ConstructorInfo constructor = typeof(NitroxAutoFaker<>).MakeGenericType(type).GetConstructor(Array.Empty()); - + ConstructorInfo constructor = typeof(NitroxAutoFaker<>).MakeGenericType(type).GetConstructor([]); if (constructor == null) { - throw new NullReferenceException($"Could not get generic constructor for {type}"); + throw new NotSupportedException($"Type {type} does not have a compatible {nameof(INitroxFaker)} yet"); } - - return (INitroxFaker)constructor.Invoke(Array.Empty()); + return (INitroxFaker)constructor.Invoke([]); } protected static bool IsValidType(Type type) @@ -105,7 +101,7 @@ protected static bool IsValidType(Type type) type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } - protected static readonly MethodInfo CastMethodBase = typeof(NitroxFaker).GetMethod(nameof(Cast), BindingFlags.NonPublic | BindingFlags.Static); + protected static readonly MethodInfo CastMethodBase = typeof(NitroxFaker).GetMethod(nameof(Cast), BindingFlags.NonPublic | BindingFlags.Static) ?? throw new Exception($"{nameof(NitroxFaker)} has no {nameof(Cast)} method!"); protected static T Cast(object o) { diff --git a/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs b/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs index edc6870706..4f0677ba78 100644 --- a/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs +++ b/Nitrox.Test/Model/Packets/PacketsSerializableTest.cs @@ -61,7 +61,7 @@ public void PacketSerializationTest() foreach (ValueTuple packet in generatedPackets) { - Packet deserialized = Packet.Deserialize(packet.Item1.Serialize()); + Packet? deserialized = Packet.Deserialize(packet.Item1.Serialize()); packet.Item1.ShouldCompare(deserialized, $"with {packet.Item1.GetType()}", config); diff --git a/Nitrox.Test/Model/Packets/Processors/PacketProcessorTest.cs b/Nitrox.Test/Model/Packets/Processors/PacketProcessorTest.cs index a844abaab9..6380de39c7 100644 --- a/Nitrox.Test/Model/Packets/Processors/PacketProcessorTest.cs +++ b/Nitrox.Test/Model/Packets/Processors/PacketProcessorTest.cs @@ -1,13 +1,12 @@ using Nitrox.Model.Core; -using Nitrox.Model.Packets.Processors.Abstract; +using Nitrox.Model.Packets.Core; using Nitrox.Model.Platforms.Discovery; -using Nitrox.Test; -using NitroxClient; -using NitroxClient.Communication.Packets.Processors.Abstract; using Nitrox.Server.Subnautica; -using Nitrox.Server.Subnautica.Models.Packets; +using Nitrox.Server.Subnautica.Models.Packets.Core; using Nitrox.Server.Subnautica.Models.Packets.Processors; -using Nitrox.Server.Subnautica.Models.Packets.Processors.Core; +using Nitrox.Test; +using NitroxClient; +using NitroxClient.Communication.Packets.Processors.Core; namespace Nitrox.Model.Packets.Processors { @@ -17,77 +16,55 @@ public class PacketProcessorTest [TestMethod] public void ClientPacketProcessorSanity() { - typeof(ClientPacketProcessor<>).Assembly.GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) - .ToList() - .ForEach(processor => - { - // Make sure that each packet-processor is derived from the ClientPacketProcessor class, - // so that it's packet-type can be determined. - Assert.IsNotNull(processor.BaseType, $"{processor} does not derive from any type!"); - Assert.IsTrue(processor.BaseType.IsGenericType, $"{processor} does not derive from a generic type!"); - Assert.IsTrue(processor.BaseType.IsAssignableToGenericType(typeof(ClientPacketProcessor<>)), $"{processor} does not derive from ClientPacketProcessor!"); + typeof(ClientAutoFacRegistrar).Assembly.GetTypes() + .Where(p => typeof(IPacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) + .ToList() + .ForEach(processor => + { + Assert.IsTrue(processor.IsAssignableTo(typeof(IClientPacketProcessor)), $"{processor} does not implement {nameof(IClientPacketProcessor<>)}!"); - // Check constructor availability: - int numCtors = processor.GetConstructors().Length; - Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})"); - }); + // Check constructor availability: + int numCtors = processor.GetConstructors().Length; + Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})"); + }); } [TestMethod] public void ServerPacketProcessorSanity() { - typeof(PacketHandler).Assembly.GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) - .ToList() - .ForEach(processor => - { - // Make sure that each packet-processor is derived from the ClientPacketProcessor class, - // so that it's packet-type can be determined. - Assert.IsNotNull(processor.BaseType, $"{processor} does not derive from any type!"); - Assert.IsTrue(processor.BaseType.IsGenericType, $"{processor} does not derive from a generic type!"); - Assert.IsTrue(processor.BaseType.IsAssignableToGenericType(typeof(AuthenticatedPacketProcessor<>)) || - processor.BaseType.IsAssignableToGenericType(typeof(UnauthenticatedPacketProcessor<>)), $"{processor} does not derive from (Un)AuthenticatedPacketProcessor!"); - - // Check constructor availability: - int numCtors = processor.GetConstructors().Length; - Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})"); - - // Unable to check parameters, these are defined in PacketHandler.ctor - }); - } - - [TestMethod] - public void SameAmountOfServerPacketProcessors() - { - IEnumerable processors = typeof(PacketHandler).Assembly.GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract); + typeof(Program).Assembly.GetTypes() + .Where(p => typeof(IPacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) + .ToList() + .ForEach(processor => + { + Assert.IsTrue(processor.IsAssignableTo(typeof(IAnonPacketProcessor)) || processor.IsAssignableTo(typeof(IAuthPacketProcessor)), + $"{processor} does not implement from any server-sided packet processor interface!"); - List packetTypes = typeof(DefaultServerPacketProcessor).Assembly.GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) - .ToList(); + // Check constructor availability: + int numCtors = processor.GetConstructors().Length; + Assert.IsTrue(numCtors == 1, $"{processor} should have exactly 1 constructor! (has {numCtors})"); - int both = packetTypes.Count; - Assert.AreEqual(processors.Count(), both, - $"Not all(Un) AuthenticatedPacketProcessors have been discovered by the runtime code (auth + unauth: {both} out of {processors.Count()}). Perhaps the runtime matching code is too strict, or a processor does not derive from ClientPacketProcessor (and will hence not be detected)."); + // Unable to check parameters, these are defined in PacketHandler.ctor + }); } [TestMethod] public void AllPacketsAreHandled() { - List packetTypes = typeof(DefaultServerPacketProcessor).Assembly.GetTypes() - .Where(p => typeof(PacketProcessor).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) - .ToList(); + List packetTypes = typeof(GameInfo).Assembly.GetTypes().ToList(); + packetTypes.AddRange(typeof(DefaultServerPacketProcessor).Assembly.GetTypes()); + packetTypes = packetTypes.Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) + .ToList(); List abstractProcessorTypes = new(); - abstractProcessorTypes.AddRange(typeof(ClientPacketProcessor<>) + abstractProcessorTypes.AddRange(typeof(IClientPacketProcessor<>) .Assembly.GetTypes() - .Where(p => p.IsClass && p.IsAbstract && p.IsAssignableToGenericType(typeof(ClientPacketProcessor<>)))); + .Where(p => p.IsClass && p.IsAbstract && p.IsAssignableToGenericType(typeof(IClientPacketProcessor<>)))); - abstractProcessorTypes.AddRange(typeof(AuthenticatedPacketProcessor<>) + abstractProcessorTypes.AddRange(typeof(IAuthPacketProcessor<>) .Assembly.GetTypes() - .Where(p => p.IsClass && p.IsAbstract && (p.IsAssignableToGenericType(typeof(AuthenticatedPacketProcessor<>)) || p.IsAssignableToGenericType(typeof(UnauthenticatedPacketProcessor<>))))); + .Where(p => p.IsClass && p.IsAbstract && (p.IsAssignableToGenericType(typeof(IAuthPacketProcessor<>)) || p.IsAssignableToGenericType(typeof(IAnonPacketProcessor<>))))); if (!GameInstallationFinder.FindGameCached(GameInfo.Subnautica)) { @@ -99,10 +76,10 @@ public void AllPacketsAreHandled() foreach (Type packet in typeof(Packet).Assembly.GetTypes().Where(p => typeof(Packet).IsAssignableFrom(p) && p.IsClass && !p.IsAbstract).ToList()) { Assert.IsTrue(packetTypes.Contains(packet) || abstractProcessorTypes.Any(genericProcessor => - { - Type processorType = genericProcessor.MakeGenericType(packet); - return NitroxServiceLocator.LocateOptionalService(processorType).HasValue; - }), $"Packet of type '{packet}' should have at least one processor."); + { + Type processorType = genericProcessor.MakeGenericType(packet); + return NitroxServiceLocator.LocateOptionalService(processorType).HasValue; + }), $"Packet of type '{packet}' should have at least one processor."); } } diff --git a/Nitrox.Test/Server/Helper/XORRandomTest.cs b/Nitrox.Test/Server/Helper/XorRandomTest.cs similarity index 81% rename from Nitrox.Test/Server/Helper/XORRandomTest.cs rename to Nitrox.Test/Server/Helper/XorRandomTest.cs index 2127e494d3..20047b2627 100644 --- a/Nitrox.Test/Server/Helper/XORRandomTest.cs +++ b/Nitrox.Test/Server/Helper/XorRandomTest.cs @@ -3,18 +3,18 @@ namespace Nitrox.Test.Server.Helper; [TestClass] -public class XORRandomTest +public class XorRandomTest { [TestMethod] public void TestMeanGeneration() { // arbitrary values under there but we can't compare the generated values with UnityEngine.Random because it's unaccessible - XorRandom.InitSeed("cheescake".GetHashCode()); + XorRandom r = new("cheescake".GetHashCode()); float mean = 0; int count = 1000000; for (int i = 0; i < count; i++) { - mean += XorRandom.NextFloat(); + mean += r.NextFloat(); } mean /= count; Assert.IsTrue(Math.Abs(0.5f - mean) < 0.001f, $"Float number generation isn't uniform enough: {mean}"); diff --git a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs index 5de23c6606..06c1499f12 100644 --- a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs @@ -57,7 +57,6 @@ public static void ClassInitialize(TestContext testContext) public void WorldDataTest(PersistedWorldData worldDataAfter, string serializerName) { Assert.IsTrue(worldData.WorldData.ParsedBatchCells.SequenceEqual(worldDataAfter.WorldData.ParsedBatchCells)); - Assert.AreEqual(worldData.WorldData.Seed, worldDataAfter.WorldData.Seed); PDAStateTest(worldData.WorldData.GameData.PDAState, worldDataAfter.WorldData.GameData.PDAState); StoryGoalTest(worldData.WorldData.GameData.StoryGoals, worldDataAfter.WorldData.GameData.StoryGoals); diff --git a/NitroxClient/ClientAutoFacRegistrar.cs b/NitroxClient/ClientAutoFacRegistrar.cs index 75027c3b72..aad4b11bd0 100644 --- a/NitroxClient/ClientAutoFacRegistrar.cs +++ b/NitroxClient/ClientAutoFacRegistrar.cs @@ -5,7 +5,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.Communication.MultiplayerSession; using NitroxClient.Communication.NetworkingLayer.LiteNetLib; -using NitroxClient.Communication.Packets.Processors.Abstract; using NitroxClient.Debuggers; using NitroxClient.GameLogic; using NitroxClient.GameLogic.FMOD; @@ -22,9 +21,9 @@ using Nitrox.Model; using Nitrox.Model.Core; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Model.Helper; using Nitrox.Model.Networking; -using Nitrox.Model.Subnautica.Helper; +using Nitrox.Model.Packets.Core; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient { @@ -137,8 +136,11 @@ private void RegisterPacketProcessors(ContainerBuilder containerBuilder) { containerBuilder .RegisterAssemblyTypes(currentAssembly) - .AsClosedTypesOf(typeof(ClientPacketProcessor<>)) + .AsClosedTypesOf(typeof(IClientPacketProcessor<>)) + .As() .InstancePerLifetimeScope(); + + containerBuilder.RegisterType().InstancePerLifetimeScope(); } private void RegisterColorSwapManagers(ContainerBuilder containerBuilder) diff --git a/NitroxClient/Communication/PacketReceiver.cs b/NitroxClient/Communication/PacketReceiver.cs index 8b5f4341fe..0adff0f08c 100644 --- a/NitroxClient/Communication/PacketReceiver.cs +++ b/NitroxClient/Communication/PacketReceiver.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication; @@ -18,7 +17,7 @@ public void Add(Packet packet) } } - public Packet GetNextPacket() + public Packet? GetNextPacket() { lock (receivedPacketsLock) { @@ -31,7 +30,7 @@ public Packet GetNextPacket() /// public void ConsumePackets(Action consumer, TExtra extraParameter) { - Packet packet = GetNextPacket(); + Packet? packet = GetNextPacket(); while (packet != null) { consumer(packet, extraParameter); diff --git a/NitroxClient/Communication/Packets/Processors/Abstract/ClientPacketProcessor.cs b/NitroxClient/Communication/Packets/Processors/Abstract/ClientPacketProcessor.cs deleted file mode 100644 index b37fbd0a9b..0000000000 --- a/NitroxClient/Communication/Packets/Processors/Abstract/ClientPacketProcessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Nitrox.Model.Packets; -using Nitrox.Model.Packets.Processors.Abstract; -using Nitrox.Model.Subnautica.Packets; - -namespace NitroxClient.Communication.Packets.Processors.Abstract -{ - public abstract class ClientPacketProcessor : PacketProcessor where T : Packet - { - public override void ProcessPacket(Packet packet, IProcessorContext context) - { - Process((T)packet); - } - - public abstract void Process(T packet); - } -} diff --git a/NitroxClient/Communication/Packets/Processors/Abstract/KeepInventoryChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/Abstract/KeepInventoryChangedProcessor.cs deleted file mode 100644 index 1a14972a51..0000000000 --- a/NitroxClient/Communication/Packets/Processors/Abstract/KeepInventoryChangedProcessor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; - -namespace NitroxClient.Communication.Packets.Processors.Abstract; - -public class KeepInventoryChangedProcessor : ClientPacketProcessor -{ - private readonly LocalPlayer localPlayer; - - public KeepInventoryChangedProcessor(LocalPlayer localPlayer) - { - this.localPlayer = localPlayer; - } - - public override void Process(KeepInventoryChanged packet) - { - localPlayer.KeepInventoryOnDeath = packet.KeepInventoryOnDeath; - } -} diff --git a/NitroxClient/Communication/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs index 2d4ddbe768..ededf79c2b 100644 --- a/NitroxClient/Communication/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/AggressiveWhenSeeTargetChangedProcessor.cs @@ -1,14 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class AggressiveWhenSeeTargetChangedProcessor : ClientPacketProcessor +internal sealed class AggressiveWhenSeeTargetChangedProcessor : IClientPacketProcessor { - public override void Process(AggressiveWhenSeeTargetChanged packet) + public Task Process(ClientProcessorContext context, AggressiveWhenSeeTargetChanged packet) { AI.AggressiveWhenSeeTargetChanged(packet.CreatureId, packet.TargetId, packet.Locked, packet.AggressionAmount); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/AnimationChangeEventProcessor.cs b/NitroxClient/Communication/Packets/Processors/AnimationChangeEventProcessor.cs index 6282fdd517..67eae68222 100644 --- a/NitroxClient/Communication/Packets/Processors/AnimationChangeEventProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/AnimationChangeEventProcessor.cs @@ -2,7 +2,7 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using UnityEngine; @@ -10,36 +10,33 @@ namespace NitroxClient.Communication.Packets.Processors; -public class AnimationChangeEventProcessor : ClientPacketProcessor +internal sealed class AnimationChangeEventProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - private readonly PlayerManager remotePlayerManager; + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public AnimationChangeEventProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } - - public override void Process(AnimationChangeEvent animEvent) + public Task Process(ClientProcessorContext context, AnimationChangeEvent packet) { // Possible for this to be sent during initial sync when the RemotePlayer doesn't exist yet if (Multiplayer.Main.InitialSyncCompleted) { - UpdateAnimation(animEvent); + UpdateAnimation(packet); } else { CoroutineHost.StartCoroutine(Coroutine()); + IEnumerator Coroutine() { yield return new WaitUntil(() => Multiplayer.Main.InitialSyncCompleted); - UpdateAnimation(animEvent); + UpdateAnimation(packet); } } + return Task.CompletedTask; } private void UpdateAnimation(AnimationChangeEvent animEvent) { - Optional opPlayer = remotePlayerManager.Find(animEvent.PlayerId); + Optional opPlayer = remotePlayerManager.Find(animEvent.SessionId); if (opPlayer.HasValue) { PlayerAnimation playerAnimation = animEvent.Animation; diff --git a/NitroxClient/Communication/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs index 44024bf719..e192177ace 100644 --- a/NitroxClient/Communication/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/AttackCyclopsTargetChangedProcessor.cs @@ -1,14 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class AttackCyclopsTargetChangedProcessor : ClientPacketProcessor +internal sealed class AttackCyclopsTargetChangedProcessor : IClientPacketProcessor { - public override void Process(AttackCyclopsTargetChanged packet) + public Task Process(ClientProcessorContext context, AttackCyclopsTargetChanged packet) { AI.AttackCyclopsTargetChanged(packet.CreatureId, packet.TargetId, packet.AggressiveToNoiseAmount); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/AuroraAndTimeUpdateProcessor.cs b/NitroxClient/Communication/Packets/Processors/AuroraAndTimeUpdateProcessor.cs index f24bc121e4..ae61104b4e 100644 --- a/NitroxClient/Communication/Packets/Processors/AuroraAndTimeUpdateProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/AuroraAndTimeUpdateProcessor.cs @@ -1,20 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class AuroraAndTimeUpdateProcessor : ClientPacketProcessor +internal sealed class AuroraAndTimeUpdateProcessor(TimeManager timeManager) : IClientPacketProcessor { - private readonly TimeManager timeManager; - - public AuroraAndTimeUpdateProcessor(TimeManager timeManager) - { - this.timeManager = timeManager; - } + private readonly TimeManager timeManager = timeManager; - public override void Process(AuroraAndTimeUpdate packet) + public Task Process(ClientProcessorContext context, AuroraAndTimeUpdate packet) { timeManager.ProcessUpdate(packet.TimeData.TimePacket); StoryManager.UpdateAuroraData(packet.TimeData.AuroraEventData); @@ -23,5 +17,6 @@ public override void Process(AuroraAndTimeUpdate packet) { StoryManager.RestoreAurora(); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/BenchChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/BenchChangedProcessor.cs index 34554543ba..1312c3c736 100644 --- a/NitroxClient/Communication/Packets/Processors/BenchChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/BenchChangedProcessor.cs @@ -1,32 +1,26 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class BenchChangedProcessor : ClientPacketProcessor +internal sealed class BenchChangedProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - private readonly PlayerManager remotePlayerManager; - - public BenchChangedProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public override void Process(BenchChanged benchChanged) + public Task Process(ClientProcessorContext context, BenchChanged benchChanged) { - if (!remotePlayerManager.TryFind(benchChanged.PlayerId, out RemotePlayer remotePlayer)) + if (!remotePlayerManager.TryFind(benchChanged.SessionId, out RemotePlayer remotePlayer)) { - Log.Error($"Couldn't find {nameof(RemotePlayer)} for {benchChanged.PlayerId}"); - return; + Log.Error($"Couldn't find {nameof(RemotePlayer)} for {benchChanged.SessionId}"); + return Task.CompletedTask; } if (!NitroxEntity.TryGetObjectFrom(benchChanged.BenchId, out GameObject bench)) { Log.Error($"Couldn't find GameObject for {benchChanged.BenchId}"); - return; + return Task.CompletedTask; } remotePlayer.AnimationController["cinematics_enabled"] = benchChanged.ChangeState != BenchChanged.BenchChangeState.UNSET; @@ -45,7 +39,7 @@ public override void Process(BenchChanged benchChanged) else { Log.Error($"Couldn't find Constructable component on {benchChanged.BenchId} or its parent"); - return; + return Task.CompletedTask; } switch (benchChanged.ChangeState) @@ -57,5 +51,6 @@ public override void Process(BenchChanged benchChanged) benchBlocker.RemovePlayerFromBench(remotePlayer); break; } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/BuildProcessor.cs b/NitroxClient/Communication/Packets/Processors/BuildProcessor.cs index 9867bed23e..e2323fa6fc 100644 --- a/NitroxClient/Communication/Packets/Processors/BuildProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/BuildProcessor.cs @@ -1,32 +1,33 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic.Bases; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic.Bases; namespace NitroxClient.Communication.Packets.Processors; -public abstract class BuildProcessor : ClientPacketProcessor where T : Packet +internal abstract class BuildProcessor : IClientPacketProcessor where T : Packet { - public override void Process(T packet) + public Task Process(ClientProcessorContext context, T packet) { BuildingHandler.Main.BuildQueue.Enqueue(packet); + return Task.CompletedTask; } } -public class PlaceGhostProcessor : BuildProcessor { } +internal class PlaceGhostProcessor : BuildProcessor; -public class PlaceModuleProcessor : BuildProcessor { } +internal class PlaceModuleProcessor : BuildProcessor; -public class ModifyConstructedAmountProcessor : BuildProcessor { } +internal class ModifyConstructedAmountProcessor : BuildProcessor; -public class PlaceBaseProcessor : BuildProcessor { } +internal class PlaceBaseProcessor : BuildProcessor; -public class UpdateBaseProcessor : BuildProcessor { } +internal class UpdateBaseProcessor : BuildProcessor; -public class BaseDeconstructedProcessor : BuildProcessor { } +internal class BaseDeconstructedProcessor : BuildProcessor; -public class PieceDeconstructedProcessor : BuildProcessor { } +internal class PieceDeconstructedProcessor : BuildProcessor; -public class WaterParkDeconstructedProcessor : BuildProcessor { } +internal class WaterParkDeconstructedProcessor : BuildProcessor; -public class LargeWaterParkDeconstructedProcessor : BuildProcessor { } +internal class LargeWaterParkDeconstructedProcessor : BuildProcessor; diff --git a/NitroxClient/Communication/Packets/Processors/BuildingDesyncWarningProcessor.cs b/NitroxClient/Communication/Packets/Processors/BuildingDesyncWarningProcessor.cs index 698cfd561c..13742ef412 100644 --- a/NitroxClient/Communication/Packets/Processors/BuildingDesyncWarningProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/BuildingDesyncWarningProcessor.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic.Bases; -using NitroxClient.GameLogic.Settings; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic.Bases; +using NitroxClient.GameLogic.Settings; namespace NitroxClient.Communication.Packets.Processors; -public class BuildingDesyncWarningProcessor : ClientPacketProcessor +internal sealed class BuildingDesyncWarningProcessor : IClientPacketProcessor { - public override void Process(BuildingDesyncWarning packet) - { + public Task Process(ClientProcessorContext context, BuildingDesyncWarning packet) + { if (!BuildingHandler.Main) { - return; + return Task.CompletedTask; } foreach (KeyValuePair operation in packet.Operations) @@ -28,5 +27,6 @@ public override void Process(BuildingDesyncWarning packet) { Log.InGame(Language.main.Get("Nitrox_BuildingDesyncDetected")); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/BuildingResyncProcessor.cs b/NitroxClient/Communication/Packets/Processors/BuildingResyncProcessor.cs index bfccef1234..ecbbbadb72 100644 --- a/NitroxClient/Communication/Packets/Processors/BuildingResyncProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/BuildingResyncProcessor.cs @@ -8,7 +8,7 @@ using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; using Nitrox.Model.Subnautica.Packets; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.Bases; using NitroxClient.GameLogic.Spawning.Bases; @@ -19,35 +19,58 @@ namespace NitroxClient.Communication.Packets.Processors; -public class BuildingResyncProcessor : ClientPacketProcessor +internal sealed class BuildingResyncProcessor(Entities entities, EntityMetadataManager entityMetadataManager) : IClientPacketProcessor { - private readonly Entities entities; - private readonly EntityMetadataManager entityMetadataManager; + private readonly Entities entities = entities; + private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager; - public BuildingResyncProcessor(Entities entities, EntityMetadataManager entityMetadataManager) - { - this.entities = entities; - this.entityMetadataManager = entityMetadataManager; - } - - public override void Process(BuildingResync packet) + public Task Process(ClientProcessorContext context, BuildingResync packet) { if (!BuildingHandler.Main) { - return; + return Task.CompletedTask; } BuildingHandler.Main.StartCoroutine(ResyncBuildingEntities(packet.BuildEntities, packet.ModuleEntities)); + return Task.CompletedTask; } - public IEnumerator ResyncBuildingEntities(Dictionary buildEntities, Dictionary moduleEntities) + /// + /// Destroys manually ghosts, modules, interior pieces and vehicles of a base + /// + /// + /// This is the destructive way of clearing the base, if the base isn't modified consequently, IBaseModuleGeometry + /// under the base cells may start spamming errors. + /// + private static void ClearBaseChildren(Base @base) + { + for (int i = @base.transform.childCount - 1; i >= 0; i--) + { + Transform child = @base.transform.GetChild(i); + if (child.GetComponent().AliveOrNull() || child.GetComponent() || + child.GetComponent()) + { + UnityEngine.Object.Destroy(child.gameObject); + } + } + foreach (VehicleDockingBay vehicleDockingBay in @base.GetComponentsInChildren(true)) + { + if (vehicleDockingBay.dockedVehicle) + { + UnityEngine.Object.Destroy(vehicleDockingBay.dockedVehicle.gameObject); + vehicleDockingBay.SetVehicleUndocked(); + } + } + } + + private IEnumerator ResyncBuildingEntities(Dictionary buildEntities, Dictionary moduleEntities) { Stopwatch stopwatch = Stopwatch.StartNew(); BuildingHandler.Main.StartResync(buildEntities); - yield return UpdateEntities(buildEntities.Keys.ToList(), OverwriteBase, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, $"Encountered an exception while resyncing BuildEntities")); + yield return UpdateEntities(buildEntities.Keys.ToList(), OverwriteBase, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, "Encountered an exception while resyncing BuildEntities")); BuildingHandler.Main.StartResync(moduleEntities); - yield return UpdateEntities(moduleEntities.Keys.ToList(), OverwriteModule, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, $"Encountered an exception while resyncing ModuleEntities")); + yield return UpdateEntities(moduleEntities.Keys.ToList(), OverwriteModule, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, "Encountered an exception while resyncing ModuleEntities")); BuildingHandler.Main.StopResync(); stopwatch.Stop(); @@ -62,7 +85,8 @@ private bool IsInCloseProximity(WorldEntity entity, C componentInWorld) where } /// - /// Tries to overwrite components of the provided type found in GlobalRoot's hierarchy by the provided list of entities to update. + /// Tries to overwrite components of the provided type found in GlobalRoot's hierarchy by the provided list of entities + /// to update. /// If no component is found to be corresponding to a provided entity, the entity will be spawned independently. /// Other components of the provided type which weren't updated shall be destroyed. /// @@ -73,9 +97,9 @@ private bool IsInCloseProximity(WorldEntity entity, C componentInWorld) where /// The GlobalRootEntity type which will be updated /// A function to overwrite a given component by a given entity /// - /// Predicate to determine if an entity can overwrite the GameObject of the provided component. + /// Predicate to determine if an entity can overwrite the GameObject of the provided component. /// - public IEnumerator UpdateEntities(List entitiesToUpdate, Func overwrite, Func correspondingPredicate) where C : Component where E : GlobalRootEntity + private IEnumerator UpdateEntities(List entitiesToUpdate, Func overwrite, Func correspondingPredicate) where C : Component where E : GlobalRootEntity { List unmarkedComponents = new(); Dictionary entitiesToUpdateById = entitiesToUpdate.ToDictionary(e => e.Id); @@ -98,7 +122,7 @@ public IEnumerator UpdateEntities(List entitiesToUpdate, Func - correspondingPredicate(entity, c)); + correspondingPredicate(entity, c)); yield return overwrite(associatedComponent, entity).OnYieldError(Log.Error); unmarkedComponents.Remove(associatedComponent); @@ -117,7 +141,7 @@ public IEnumerator UpdateEntities(List entitiesToUpdate, Func - /// Destroys manually ghosts, modules, interior pieces and vehicles of a base - /// - /// - /// This is the destructive way of clearing the base, if the base isn't modified consequently, IBaseModuleGeometry under the base cells may start spamming errors. - /// - public static void ClearBaseChildren(Base @base) - { - for (int i = @base.transform.childCount - 1; i >= 0; i--) - { - Transform child = @base.transform.GetChild(i); - if (child.GetComponent().AliveOrNull() || child.GetComponent() || - child.GetComponent()) - { - UnityEngine.Object.Destroy(child.gameObject); - } - } - foreach (VehicleDockingBay vehicleDockingBay in @base.GetComponentsInChildren(true)) - { - if (vehicleDockingBay.dockedVehicle) - { - UnityEngine.Object.Destroy(vehicleDockingBay.dockedVehicle.gameObject); - vehicleDockingBay.SetVehicleUndocked(); - } - } - } } diff --git a/NitroxClient/Communication/Packets/Processors/ChatMessageProcessor.cs b/NitroxClient/Communication/Packets/Processors/ChatMessageProcessor.cs index 8cf1c88895..a1d286fbca 100644 --- a/NitroxClient/Communication/Packets/Processors/ChatMessageProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ChatMessageProcessor.cs @@ -1,82 +1,75 @@ using System; using System.Linq; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.DataStructures.Unity; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.ChatUI; using NitroxClient.GameLogic.Settings; -using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class ChatMessageProcessor(PlayerManager remotePlayerManager, LocalPlayer localPlayer) : IClientPacketProcessor { - class ChatMessageProcessor : ClientPacketProcessor - { - private readonly PlayerManager remotePlayerManager; - private readonly LocalPlayer localPlayer; - private readonly PlayerChatManager playerChatManager = PlayerChatManager.Instance; + private readonly LocalPlayer localPlayer = localPlayer; + private readonly PlayerChatManager playerChatManager = PlayerChatManager.Instance; + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - private readonly Color32 serverMessageColor = new Color32(0x8c, 0x00, 0xFF, 0xFF); + private readonly Color32 serverMessageColor = new Color32(0x8c, 0x00, 0xFF, 0xFF); - public ChatMessageProcessor(PlayerManager remotePlayerManager, LocalPlayer localPlayer) + public Task Process(ClientProcessorContext context, ChatMessage message) + { + if (message.SessionId != SessionId.SERVER_ID) { - this.remotePlayerManager = remotePlayerManager; - this.localPlayer = localPlayer; + LogClientMessage(message); } - - public override void Process(ChatMessage message) + else { - if (message.PlayerId != ChatMessage.SERVER_ID) - { - LogClientMessage(message); - } - else - { - LogServerMessage(message); - } + LogServerMessage(message); } + return Task.CompletedTask; + } - private void LogClientMessage(ChatMessage message) + private void LogClientMessage(ChatMessage message) + { + // The message can come from either the local player or other players + string playerName; + NitroxColor color; + if (localPlayer.SessionId == message.SessionId) { - // The message can come from either the local player or other players - string playerName; - NitroxColor color; - if (localPlayer.PlayerId == message.PlayerId) + playerName = localPlayer.PlayerName; + color = localPlayer.PlayerSettings.PlayerColor; + } + else + { + Optional remotePlayer = remotePlayerManager.Find(message.SessionId); + if (!remotePlayer.HasValue) { - playerName = localPlayer.PlayerName; - color = localPlayer.PlayerSettings.PlayerColor; + string playerTableFormatted = string.Join("\n", remotePlayerManager.GetAll().Select(ply => $"Name: '{ply.PlayerName}', Id: {ply.SessionId}")); + Log.Error($"Tried to add chat message for remote player that could not be found with id '${message.SessionId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}"); + throw new Exception($"Tried to add chat message for remote player that could not be found with id '${message.SessionId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}"); } - else - { - Optional remotePlayer = remotePlayerManager.Find(message.PlayerId); - if (!remotePlayer.HasValue) - { - string playerTableFormatted = string.Join("\n", remotePlayerManager.GetAll().Select(ply => $"Name: '{ply.PlayerName}', Id: {ply.PlayerId}")); - Log.Error($"Tried to add chat message for remote player that could not be found with id '${message.PlayerId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}"); - throw new Exception($"Tried to add chat message for remote player that could not be found with id '${message.PlayerId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}"); - } - playerName = remotePlayer.Value.PlayerName; - color = remotePlayer.Value.PlayerSettings.PlayerColor; - } - - playerChatManager.AddMessage(playerName, message.Text, color.ToUnity()); - if (NitroxPrefs.ChatAutoOpen.Value) - { - playerChatManager.ShowChat(); - } + playerName = remotePlayer.Value.PlayerName; + color = remotePlayer.Value.PlayerSettings.PlayerColor; } - private void LogServerMessage(ChatMessage message) + playerChatManager.AddMessage(playerName, message.Text, color.ToUnity()); + if (NitroxPrefs.ChatAutoOpen.Value) { - playerChatManager.AddMessage("Server", message.Text, serverMessageColor); - if (NitroxPrefs.ChatAutoOpen.Value) - { - playerChatManager.ShowChat(); - } + playerChatManager.ShowChat(); + } + } + + private void LogServerMessage(ChatMessage message) + { + playerChatManager.AddMessage("Server", message.Text, serverMessageColor); + if (NitroxPrefs.ChatAutoOpen.Value) + { + playerChatManager.ShowChat(); } } } diff --git a/NitroxClient/Communication/Packets/Processors/CoffeeMachineUseProcessor.cs b/NitroxClient/Communication/Packets/Processors/CoffeeMachineUseProcessor.cs index 8aa10dc30f..0ea2ea4c70 100644 --- a/NitroxClient/Communication/Packets/Processors/CoffeeMachineUseProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CoffeeMachineUseProcessor.cs @@ -1,15 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using NitroxClient.MonoBehaviours; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; using UnityEngine; using static Nitrox.Model.Subnautica.Packets.CoffeeMachineUse; namespace NitroxClient.Communication.Packets.Processors; -public sealed class CoffeeMachineUseProcessor : ClientPacketProcessor +internal sealed class CoffeeMachineUseProcessor : IClientPacketProcessor { private readonly LocalPlayer localPlayer; private readonly float machineSoundRange; @@ -21,19 +20,19 @@ public CoffeeMachineUseProcessor(LocalPlayer localPlayer, FMODWhitelist soundWhi machineSoundRange = coffeeSoundData.Radius; } - public override void Process(CoffeeMachineUse packet) + public Task Process(ClientProcessorContext context, CoffeeMachineUse packet) { - if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject machineGO)) + if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject machineGo)) { Log.Warn("Failed to get CoffeeVendingMachine gameobject while processing CoffeeMachineUse packet"); - return; + return Task.CompletedTask; } - if (!machineGO.TryGetComponent(out CoffeeVendingMachine machine)) + if (!machineGo.TryGetComponent(out CoffeeVendingMachine machine)) { Log.Warn("Failed to get CoffeeVendingMachine component while processing CoffeeMachineUse packet"); - return; + return Task.CompletedTask; } - bool bPlaySound = Vector3.Distance(machineGO.transform.position, localPlayer.Body.transform.position) < machineSoundRange; + bool bPlaySound = Vector3.Distance(machineGo.transform.position, localPlayer.Body.transform.position) < machineSoundRange; if (packet.Slot == CoffeeMachineSlot.ONE) { @@ -53,5 +52,6 @@ public override void Process(CoffeeMachineUse packet) machine.vfxController.Play(1); machine.timeLastUseSlot2 = Time.time; } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/Core/ClientProcessorContext.cs b/NitroxClient/Communication/Packets/Processors/Core/ClientProcessorContext.cs new file mode 100644 index 0000000000..0b0ba2dec9 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/Core/ClientProcessorContext.cs @@ -0,0 +1,17 @@ +using Nitrox.Model.Packets; +using Nitrox.Model.Packets.Core; +using NitroxClient.Communication.Abstract; + +namespace NitroxClient.Communication.Packets.Processors.Core; + +public record ClientProcessorContext : IPacketProcessContext +{ + private readonly IPacketSender packetSender; + + public ClientProcessorContext(IPacketSender packetSender) + { + this.packetSender = packetSender; + } + + public void Send(T packet) where T : Packet => packetSender.Send(packet); +} diff --git a/NitroxClient/Communication/Packets/Processors/Core/IClientPacketProcessor.cs b/NitroxClient/Communication/Packets/Processors/Core/IClientPacketProcessor.cs new file mode 100644 index 0000000000..72dd8a06a7 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/Core/IClientPacketProcessor.cs @@ -0,0 +1,8 @@ +using Nitrox.Model.Packets; +using Nitrox.Model.Packets.Core; + +namespace NitroxClient.Communication.Packets.Processors.Core; + +public interface IClientPacketProcessor : IPacketProcessor; + +public interface IClientPacketProcessor : IClientPacketProcessor, IPacketProcessor where TPacket : Packet; diff --git a/NitroxClient/Communication/Packets/Processors/CreatureActionProcessor.cs b/NitroxClient/Communication/Packets/Processors/CreatureActionProcessor.cs index 9eef165a49..df058b31fa 100644 --- a/NitroxClient/Communication/Packets/Processors/CreatureActionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CreatureActionProcessor.cs @@ -1,21 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class CreatureActionProcessor : ClientPacketProcessor +internal sealed class CreatureActionProcessor(AI ai) : IClientPacketProcessor { - private readonly AI ai; - - public CreatureActionProcessor(AI ai) - { - this.ai = ai; - } + private readonly AI ai = ai; - public override void Process(CreatureActionChanged packet) + public Task Process(ClientProcessorContext context, CreatureActionChanged packet) { ai.CreatureActionChanged(packet.CreatureId, packet.CreatureActionType); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/CreaturePoopPerformedProcessor.cs b/NitroxClient/Communication/Packets/Processors/CreaturePoopPerformedProcessor.cs index 3389844284..5cffb1e8fd 100644 --- a/NitroxClient/Communication/Packets/Processors/CreaturePoopPerformedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CreaturePoopPerformedProcessor.cs @@ -1,14 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class CreaturePoopPerformedProcessor : ClientPacketProcessor +internal sealed class CreaturePoopPerformedProcessor : IClientPacketProcessor { - public override void Process(CreaturePoopPerformed packet) + public Task Process(ClientProcessorContext context, CreaturePoopPerformed packet) { AI.CreaturePoopPerformed(packet.CreatureId); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/CyclopsDamagePointHealthChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/CyclopsDamagePointHealthChangedProcessor.cs index 7de4cdef05..2dcb25634c 100644 --- a/NitroxClient/Communication/Packets/Processors/CyclopsDamagePointHealthChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CyclopsDamagePointHealthChangedProcessor.cs @@ -1,30 +1,22 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class CyclopsDamagePointHealthChangedProcessor : IClientPacketProcessor { - public class CyclopsDamagePointHealthChangedProcessor : ClientPacketProcessor + public Task Process(ClientProcessorContext context, CyclopsDamagePointRepaired packet) { - private readonly IPacketSender packetSender; + GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); + SubRoot cyclops = gameObject.RequireComponent(); - public CyclopsDamagePointHealthChangedProcessor(IPacketSender packetSender) + using (PacketSuppressor.Suppress()) + using (PacketSuppressor.Suppress()) { - this.packetSender = packetSender; - } - - public override void Process(CyclopsDamagePointRepaired packet) - { - GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); - SubRoot cyclops = gameObject.RequireComponent(); - - using (PacketSuppressor.Suppress()) - using (PacketSuppressor.Suppress()) - { - cyclops.damageManager.damagePoints[packet.DamagePointIndex].liveMixin.AddHealth(packet.RepairAmount); - } + cyclops.damageManager.damagePoints[packet.DamagePointIndex].liveMixin.AddHealth(packet.RepairAmount); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/CyclopsDamageProcessor.cs b/NitroxClient/Communication/Packets/Processors/CyclopsDamageProcessor.cs index 75af3ee806..f955b9ec5a 100644 --- a/NitroxClient/Communication/Packets/Processors/CyclopsDamageProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CyclopsDamageProcessor.cs @@ -1,201 +1,199 @@ using System.Collections.Generic; using System.Linq; -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +/// +/// Add/remove s and s to match the +/// packet received +/// +internal sealed class CyclopsDamageProcessor(Fires fires) : IClientPacketProcessor { - /// - /// Add/remove s and s to match the packet received - /// - public class CyclopsDamageProcessor : ClientPacketProcessor + private readonly Fires fires = fires; + + public Task Process(ClientProcessorContext context, CyclopsDamage packet) { - private readonly IPacketSender packetSender; - private readonly Fires fires; + SubRoot subRoot = NitroxEntity.RequireObjectFrom(packet.Id).GetComponent(); - public CyclopsDamageProcessor(IPacketSender packetSender, Fires fires) + using (PacketSuppressor.Suppress()) { - this.packetSender = packetSender; - this.fires = fires; + SetActiveDamagePoints(subRoot, packet.DamagePointIndexes); } - public override void Process(CyclopsDamage packet) + using (PacketSuppressor.Suppress()) { - SubRoot subRoot = NitroxEntity.RequireObjectFrom(packet.Id).GetComponent(); - - using (PacketSuppressor.Suppress()) - { - SetActiveDamagePoints(subRoot, packet.DamagePointIndexes); - } - - using (PacketSuppressor.Suppress()) - { - SetActiveRoomFires(subRoot, packet.RoomFires); - } + SetActiveRoomFires(subRoot, packet.RoomFires); + } - LiveMixin subHealth = subRoot.gameObject.RequireComponent(); + LiveMixin subHealth = subRoot.gameObject.RequireComponent(); - float oldHPPercent = subRoot.oldHPPercent; + float oldHPPercent = subRoot.oldHPPercent; - // Client side noises. Not necessary for keeping the health synced - if (subHealth.GetHealthFraction() < 0.5f && oldHPPercent >= 0.5f) - { - subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullLowNotification, true, false); - } - else if (subHealth.GetHealthFraction() < 0.25f && oldHPPercent >= 0.25f) - { - subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullCriticalNotification, true, false); - } + // Client side noises. Not necessary for keeping the health synced + if (subHealth.GetHealthFraction() < 0.5f && oldHPPercent >= 0.5f) + { + subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullLowNotification); + } + else if (subHealth.GetHealthFraction() < 0.25f && oldHPPercent >= 0.25f) + { + subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullCriticalNotification); + } - using (PacketSuppressor.Suppress()) - { - // Not necessary, but used by above code whenever damage is done - subRoot.oldHPPercent = subHealth.GetHealthFraction(); + using (PacketSuppressor.Suppress()) + { + // Not necessary, but used by above code whenever damage is done + subRoot.oldHPPercent = subHealth.GetHealthFraction(); - // Apply the actual health changes - subRoot.gameObject.RequireComponent().health = packet.SubHealth; - subRoot.gameObject.RequireComponentInChildren().subLiveMixin.health = packet.DamageManagerHealth; - subRoot.gameObject.RequireComponent().liveMixin.health = packet.SubFireHealth; - } + // Apply the actual health changes + subRoot.gameObject.RequireComponent().health = packet.SubHealth; + subRoot.gameObject.RequireComponentInChildren().subLiveMixin.health = packet.DamageManagerHealth; + subRoot.gameObject.RequireComponent().liveMixin.health = packet.SubFireHealth; } + return Task.CompletedTask; + } - /// - /// Add/remove s until it matches the array passed. Can trigger packets - /// - private void SetActiveDamagePoints(SubRoot cyclops, int[] damagePointIndexes) + /// + /// Add/remove s until it matches the array + /// passed. Can trigger packets + /// + private void SetActiveDamagePoints(SubRoot cyclops, int[] damagePointIndexes) + { + CyclopsExternalDamageManager damageManager = cyclops.gameObject.RequireComponentInChildren(); + List unusedDamagePoints = damageManager.unusedDamagePoints; + + // CyclopsExternalDamageManager.damagePoints is an unchanged list. It will never have items added/removed from it. Since packet.DamagePointIndexes is also an array + // generated in an ordered manner, we can match them without worrying about unordered items. + if (damagePointIndexes != null && damagePointIndexes.Length > 0) { - CyclopsExternalDamageManager damageManager = cyclops.gameObject.RequireComponentInChildren(); - List unusedDamagePoints = damageManager.unusedDamagePoints; + int packetDamagePointsIndex = 0; - // CyclopsExternalDamageManager.damagePoints is an unchanged list. It will never have items added/removed from it. Since packet.DamagePointIndexes is also an array - // generated in an ordered manner, we can match them without worrying about unordered items. - if (damagePointIndexes != null && damagePointIndexes.Length > 0) + for (int damagePointsIndex = 0; damagePointsIndex < damageManager.damagePoints.Length; damagePointsIndex++) { - int packetDamagePointsIndex = 0; - - for (int damagePointsIndex = 0; damagePointsIndex < damageManager.damagePoints.Length; damagePointsIndex++) + // Loop over all of the packet.DamagePointIndexes as long as there's more to match + if (packetDamagePointsIndex < damagePointIndexes.Length + && damagePointIndexes[packetDamagePointsIndex] == damagePointsIndex) { - // Loop over all of the packet.DamagePointIndexes as long as there's more to match - if (packetDamagePointsIndex < damagePointIndexes.Length - && damagePointIndexes[packetDamagePointsIndex] == damagePointsIndex) - { - if (!damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf) - { - // Copied from CyclopsExternalDamageManager.CreatePoint(), except without the random index pick. - damageManager.damagePoints[damagePointsIndex].gameObject.SetActive(true); - damageManager.damagePoints[damagePointsIndex].RestoreHealth(); - GameObject prefabGo = damageManager.fxPrefabs[UnityEngine.Random.Range(0, damageManager.fxPrefabs.Length - 1)]; - damageManager.damagePoints[damagePointsIndex].SpawnFx(prefabGo); - unusedDamagePoints.Remove(damageManager.damagePoints[damagePointsIndex]); - } - - packetDamagePointsIndex++; - } - else + if (!damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf) { - // If it's active, but not in the list, it must have been repaired. - if (damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf) - { - RepairDamagePoint(cyclops, damagePointsIndex, 999); - } + // Copied from CyclopsExternalDamageManager.CreatePoint(), except without the random index pick. + damageManager.damagePoints[damagePointsIndex].gameObject.SetActive(true); + damageManager.damagePoints[damagePointsIndex].RestoreHealth(); + GameObject prefabGo = damageManager.fxPrefabs[Random.Range(0, damageManager.fxPrefabs.Length - 1)]; + damageManager.damagePoints[damagePointsIndex].SpawnFx(prefabGo); + unusedDamagePoints.Remove(damageManager.damagePoints[damagePointsIndex]); } - } - // Looks like the list came in unordered. I've uttered "That shouldn't happen" enough to do sanity checks for what should be impossible. - if (packetDamagePointsIndex < damagePointIndexes.Length) - { - Log.Error($"[CyclopsDamageProcessor packet.DamagePointIds did not fully iterate! Id: {damagePointIndexes[packetDamagePointsIndex]} had no matching Id in damageManager.damagePoints, or the order is incorrect!]"); + packetDamagePointsIndex++; } - } - else - { - // None should be active. - for (int i = 0; i < damageManager.damagePoints.Length; i++) + else { - if (damageManager.damagePoints[i].gameObject.activeSelf) + // If it's active, but not in the list, it must have been repaired. + if (damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf) { - RepairDamagePoint(cyclops, i, 999); + RepairDamagePoint(cyclops, damagePointsIndex, 999); } } } - // unusedDamagePoints is checked against damagePoints to determine if there's enough damage points. Failing to set the new list - // of unusedDamagePoints will cause random DamagePoints to appear. - damageManager.unusedDamagePoints = unusedDamagePoints; - // Visual update only to show the water leaking through the window and various hull points based on missing health. - damageManager.ToggleLeakPointsBasedOnDamage(); + // Looks like the list came in unordered. I've uttered "That shouldn't happen" enough to do sanity checks for what should be impossible. + if (packetDamagePointsIndex < damagePointIndexes.Length) + { + Log.Error($"[CyclopsDamageProcessor packet.DamagePointIds did not fully iterate! Id: {damagePointIndexes[packetDamagePointsIndex]} had no matching Id in damageManager.damagePoints, or the order is incorrect!]"); + } } - - /// - /// Add/remove fires until it matches the array. Can trigger packets - /// - private void SetActiveRoomFires(SubRoot subRoot, CyclopsFireData[] roomFires) + else { - SubFire subFire = subRoot.gameObject.RequireComponent(); - Dictionary roomFiresDict = subFire.roomFires; - - if (!subRoot.TryGetIdOrWarn(out NitroxId subRootId)) + // None should be active. + for (int i = 0; i < damageManager.damagePoints.Length; i++) { - return; + if (damageManager.damagePoints[i].gameObject.activeSelf) + { + RepairDamagePoint(cyclops, i, 999); + } } + } + + // unusedDamagePoints is checked against damagePoints to determine if there's enough damage points. Failing to set the new list + // of unusedDamagePoints will cause random DamagePoints to appear. + damageManager.unusedDamagePoints = unusedDamagePoints; + // Visual update only to show the water leaking through the window and various hull points based on missing health. + damageManager.ToggleLeakPointsBasedOnDamage(); + } + + /// + /// Add/remove fires until it matches the array. Can trigger + /// packets + /// + private void SetActiveRoomFires(SubRoot subRoot, CyclopsFireData[] roomFires) + { + SubFire subFire = subRoot.gameObject.RequireComponent(); + Dictionary roomFiresDict = subFire.roomFires; - if (roomFires != null && roomFires.Length > 0) + if (!subRoot.TryGetIdOrWarn(out NitroxId subRootId)) + { + return; + } + + if (roomFires != null && roomFires.Length > 0) + { + // Removing and adding fires will happen in the same loop + foreach (KeyValuePair keyValuePair in roomFiresDict) { - // Removing and adding fires will happen in the same loop - foreach (KeyValuePair keyValuePair in roomFiresDict) + for (int nodeIndex = 0; nodeIndex < keyValuePair.Value.spawnNodes.Length; nodeIndex++) { - for (int nodeIndex = 0; nodeIndex < keyValuePair.Value.spawnNodes.Length; nodeIndex++) - { - CyclopsFireData fireNode = roomFires.SingleOrDefault(x => x.Room == keyValuePair.Key && x.NodeIndex == nodeIndex); + CyclopsFireData fireNode = roomFires.SingleOrDefault(x => x.Room == keyValuePair.Key && x.NodeIndex == nodeIndex); - // If there's a matching node index, add a fire if there isn't one already. Otherwise remove a fire if there is one - if (fireNode == null) + // If there's a matching node index, add a fire if there isn't one already. Otherwise remove a fire if there is one + if (fireNode == null) + { + if (keyValuePair.Value.spawnNodes[nodeIndex].childCount > 0) { - if (keyValuePair.Value.spawnNodes[nodeIndex].childCount > 0) - { - keyValuePair.Value.spawnNodes[nodeIndex].GetComponentInChildren().Douse(10000); - } + keyValuePair.Value.spawnNodes[nodeIndex].GetComponentInChildren().Douse(10000); } - else + } + else + { + if (keyValuePair.Value.spawnNodes[nodeIndex].childCount < 1) { - if (keyValuePair.Value.spawnNodes[nodeIndex].childCount < 1) - { - fires.Create(new CyclopsFireData(fireNode.FireId, subRootId, fireNode.Room, fireNode.NodeIndex)); - } + fires.Create(new CyclopsFireData(fireNode.FireId, subRootId, fireNode.Room, fireNode.NodeIndex)); } } } } - // Clear out the fires, there should be none active - else + } + // Clear out the fires, there should be none active + else + { + foreach (KeyValuePair keyValuePair in roomFiresDict) { - foreach (KeyValuePair keyValuePair in roomFiresDict) + foreach (Transform spawnNode in keyValuePair.Value.spawnNodes) { - foreach (Transform spawnNode in keyValuePair.Value.spawnNodes) + if (spawnNode.childCount > 0) { - if (spawnNode.childCount > 0) - { - spawnNode.GetComponentInChildren().Douse(10000); - } + spawnNode.GetComponentInChildren().Douse(10000); } } } } + } - /// - /// Set the health of a . This can trigger sending packets - /// - /// The max health of the point is 1. 999 is passed to trigger a full repair of the - private void RepairDamagePoint(SubRoot subRoot, int damagePointIndex, float repairAmount) - { - subRoot.damageManager.damagePoints[damagePointIndex].liveMixin.AddHealth(repairAmount); - } + /// + /// Set the health of a . This can trigger sending + /// packets + /// + /// + /// The max health of the point is 1. 999 is passed to trigger a full repair of the + /// + /// + private void RepairDamagePoint(SubRoot subRoot, int damagePointIndex, float repairAmount) + { + subRoot.damageManager.damagePoints[damagePointIndex].liveMixin.AddHealth(repairAmount); } } diff --git a/NitroxClient/Communication/Packets/Processors/CyclopsDecoyLaunchProcessor.cs b/NitroxClient/Communication/Packets/Processors/CyclopsDecoyLaunchProcessor.cs index 6fcb2b116a..0ecb43ecd0 100644 --- a/NitroxClient/Communication/Packets/Processors/CyclopsDecoyLaunchProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CyclopsDecoyLaunchProcessor.cs @@ -1,24 +1,16 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors -{ - class CyclopsDecoyLaunchProcessor : ClientPacketProcessor - { - private readonly IPacketSender packetSender; - private readonly Cyclops cyclops; +namespace NitroxClient.Communication.Packets.Processors; - public CyclopsDecoyLaunchProcessor(IPacketSender packetSender, Cyclops cyclops) - { - this.packetSender = packetSender; - this.cyclops = cyclops; - } +internal sealed class CyclopsDecoyLaunchProcessor(Cyclops cyclops) : IClientPacketProcessor +{ + private readonly Cyclops cyclops = cyclops; - public override void Process(CyclopsDecoyLaunch decoyLaunchPacket) - { - cyclops.LaunchDecoy(decoyLaunchPacket.Id); - } + public Task Process(ClientProcessorContext context, CyclopsDecoyLaunch decoyLaunchPacket) + { + cyclops.LaunchDecoy(decoyLaunchPacket.Id); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/CyclopsFireCreatedProcessor.cs b/NitroxClient/Communication/Packets/Processors/CyclopsFireCreatedProcessor.cs index 484f221ea8..93d9c04cf6 100644 --- a/NitroxClient/Communication/Packets/Processors/CyclopsFireCreatedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CyclopsFireCreatedProcessor.cs @@ -1,24 +1,16 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors -{ - public class CyclopsFireCreatedProcessor : ClientPacketProcessor - { - private readonly IPacketSender packetSender; - private readonly Fires fires; +namespace NitroxClient.Communication.Packets.Processors; - public CyclopsFireCreatedProcessor(IPacketSender packetSender, Fires fires) - { - this.packetSender = packetSender; - this.fires = fires; - } +internal sealed class CyclopsFireCreatedProcessor(Fires fires) : IClientPacketProcessor +{ + private readonly Fires fires = fires; - public override void Process(CyclopsFireCreated packet) - { - fires.Create(packet.FireCreatedData); - } + public Task Process(ClientProcessorContext context, CyclopsFireCreated packet) + { + fires.Create(packet.FireCreatedData); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/CyclopsFireSuppressionProcessor.cs b/NitroxClient/Communication/Packets/Processors/CyclopsFireSuppressionProcessor.cs index 5c0614e034..d1ac52cb0a 100644 --- a/NitroxClient/Communication/Packets/Processors/CyclopsFireSuppressionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/CyclopsFireSuppressionProcessor.cs @@ -1,24 +1,16 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors -{ - public class CyclopsFireSuppressionProcessor : ClientPacketProcessor - { - private readonly IPacketSender packetSender; - private readonly Cyclops cyclops; +namespace NitroxClient.Communication.Packets.Processors; - public CyclopsFireSuppressionProcessor(IPacketSender packetSender, Cyclops cyclops) - { - this.packetSender = packetSender; - this.cyclops = cyclops; - } +internal sealed class CyclopsFireSuppressionProcessor(Cyclops cyclops) : IClientPacketProcessor +{ + private readonly Cyclops cyclops = cyclops; - public override void Process(CyclopsFireSuppression fireSuppressionPacket) - { - cyclops.StartFireSuppression(fireSuppressionPacket.Id); - } + public Task Process(ClientProcessorContext context, CyclopsFireSuppression fireSuppressionPacket) + { + cyclops.StartFireSuppression(fireSuppressionPacket.Id); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/DebugStartMapProcessor.cs b/NitroxClient/Communication/Packets/Processors/DebugStartMapProcessor.cs index 14686c3399..cc34d8c67b 100644 --- a/NitroxClient/Communication/Packets/Processors/DebugStartMapProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/DebugStartMapProcessor.cs @@ -1,20 +1,28 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using System.Collections.Generic; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class DebugStartMapProcessor : IClientPacketProcessor { - class DebugStartMapProcessor : ClientPacketProcessor + private readonly List activeCubes = []; + + public Task Process(ClientProcessorContext context, DebugStartMapPacket packet) { - public override void Process(DebugStartMapPacket packet) + foreach (GameObject cube in activeCubes) + { + Object.Destroy(cube); + } + activeCubes.Clear(); + foreach (NitroxVector3 position in packet.StartPositions) { - foreach (NitroxVector3 position in packet.StartPositions) - { - GameObject prim = GameObject.CreatePrimitive(PrimitiveType.Cube); - prim.transform.position = new Vector3(position.X, position.Y + 10, position.Z); - } + GameObject prim = GameObject.CreatePrimitive(PrimitiveType.Cube); + prim.transform.position = new Vector3(position.X, position.Y + 10, position.Z); + activeCubes.Add(prim); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/DeconstructionBeginProcessor.cs b/NitroxClient/Communication/Packets/Processors/DeconstructionBeginProcessor.cs index 2f13338b3b..a614440165 100644 --- a/NitroxClient/Communication/Packets/Processors/DeconstructionBeginProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/DeconstructionBeginProcessor.cs @@ -1,44 +1,34 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic.Helper; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; using static NitroxClient.GameLogic.Helper.TransientLocalObjectManager; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class DeconstructionBeginProcessor : IClientPacketProcessor { - public class DeconstructionBeginProcessor : ClientPacketProcessor + public Task Process(ClientProcessorContext context, DeconstructionBegin packet) { - private readonly IPacketSender packetSender; + Log.Info($"Received deconstruction packet for id: {packet.Id}"); - public DeconstructionBeginProcessor(IPacketSender packetSender) - { - this.packetSender = packetSender; - } - - public override void Process(DeconstructionBegin packet) - { - Log.Info($"Received deconstruction packet for id: {packet.Id}"); + GameObject deconstructing = NitroxEntity.RequireObjectFrom(packet.Id); - GameObject deconstructing = NitroxEntity.RequireObjectFrom(packet.Id); + Constructable constructable = deconstructing.GetComponent(); + BaseDeconstructable baseDeconstructable = deconstructing.GetComponent(); - Constructable constructable = deconstructing.GetComponent(); - BaseDeconstructable baseDeconstructable = deconstructing.GetComponent(); - - using (PacketSuppressor.Suppress()) + using (PacketSuppressor.Suppress()) + { + if (baseDeconstructable != null) + { + Add(TransientObjectType.LATEST_DECONSTRUCTED_BASE_PIECE_GUID, packet.Id); + baseDeconstructable.Deconstruct(); + } + else if (constructable != null) { - if (baseDeconstructable != null) - { - TransientLocalObjectManager.Add(TransientObjectType.LATEST_DECONSTRUCTED_BASE_PIECE_GUID, packet.Id); - baseDeconstructable.Deconstruct(); - } - else if (constructable != null) - { - constructable.SetState(false, false); - } + constructable.SetState(false, false); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/DisconnectProcessor.cs b/NitroxClient/Communication/Packets/Processors/DisconnectProcessor.cs index f084fb445e..d267fb46b5 100644 --- a/NitroxClient/Communication/Packets/Processors/DisconnectProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/DisconnectProcessor.cs @@ -1,37 +1,30 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.HUD; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class DisconnectProcessor(PlayerManager remotePlayerManager, PlayerVitalsManager vitalsManager) : IClientPacketProcessor { - class DisconnectProcessor : ClientPacketProcessor - { - private readonly PlayerManager remotePlayerManager; - private readonly PlayerVitalsManager vitalsManager; + private readonly PlayerManager remotePlayerManager = remotePlayerManager; + private readonly PlayerVitalsManager vitalsManager = vitalsManager; - public DisconnectProcessor(PlayerManager remotePlayerManager, PlayerVitalsManager vitalsManager) - { - this.remotePlayerManager = remotePlayerManager; - this.vitalsManager = vitalsManager; - } + public Task Process(ClientProcessorContext context, Disconnect disconnect) + { + // TODO: don't remove right away... maybe grey out and start + // a coroutine to finally remove. + vitalsManager.RemoveForPlayer(disconnect.SessionId); - public override void Process(Disconnect disconnect) + Optional remotePlayer = remotePlayerManager.Find(disconnect.SessionId); + if (remotePlayer.HasValue) { - // TODO: don't remove right away... maybe grey out and start - // a coroutine to finally remove. - vitalsManager.RemoveForPlayer(disconnect.PlayerId); - - Optional remotePlayer = remotePlayerManager.Find(disconnect.PlayerId); - if (remotePlayer.HasValue) - { - remotePlayer.Value.PlayerDisconnectEvent.Trigger(remotePlayer.Value); - remotePlayerManager.RemovePlayer(disconnect.PlayerId); - Log.Info($"{remotePlayer.Value.PlayerName} disconnected"); - Log.InGame(Language.main.Get("Nitrox_PlayerDisconnected").Replace("{PLAYER}", remotePlayer.Value.PlayerName)); - } + remotePlayer.Value.PlayerDisconnectEvent.Trigger(remotePlayer.Value); + remotePlayerManager.RemovePlayer(disconnect.SessionId); + Log.Info($"{remotePlayer.Value.PlayerName} disconnected"); + Log.InGame(Language.main.Get("Nitrox_PlayerDisconnected").Replace("{PLAYER}", remotePlayer.Value.PlayerName)); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/DiscordRequestIPProcessor.cs b/NitroxClient/Communication/Packets/Processors/DiscordRequestIPProcessor.cs index b76552ac8e..f575920f93 100644 --- a/NitroxClient/Communication/Packets/Processors/DiscordRequestIPProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/DiscordRequestIPProcessor.cs @@ -1,14 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours.Discord; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.Packets.Processors; -public class DiscordRequestIPProcessor : ClientPacketProcessor +internal sealed class DiscordRequestIPProcessor : IClientPacketProcessor { - public override void Process(DiscordRequestIP packet) + public Task Process(ClientProcessorContext context, DiscordRequestIP packet) { DiscordClient.UpdateIpPort(packet.IpPort); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/DropSimulationOwnershipProcessor.cs b/NitroxClient/Communication/Packets/Processors/DropSimulationOwnershipProcessor.cs index 17d179a049..d7abdeec7c 100644 --- a/NitroxClient/Communication/Packets/Processors/DropSimulationOwnershipProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/DropSimulationOwnershipProcessor.cs @@ -1,21 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class DropSimulationOwnershipProcessor : ClientPacketProcessor +internal sealed class DropSimulationOwnershipProcessor(SimulationOwnership simulationOwnershipManager) : IClientPacketProcessor { - private readonly SimulationOwnership simulationOwnershipManager; - - public DropSimulationOwnershipProcessor(SimulationOwnership simulationOwnershipManager) - { - this.simulationOwnershipManager = simulationOwnershipManager; - } + private readonly SimulationOwnership simulationOwnershipManager = simulationOwnershipManager; - public override void Process(DropSimulationOwnership packet) + public Task Process(ClientProcessorContext context, DropSimulationOwnership packet) { simulationOwnershipManager.DropSimulationFrom(packet.EntityId); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs b/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs index 44847b54c3..ca2b9696fd 100644 --- a/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EntityDestroyedProcessor.cs @@ -1,28 +1,22 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.PlayerLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class EntityDestroyedProcessor : ClientPacketProcessor +public sealed class EntityDestroyedProcessor(Entities entities) : IClientPacketProcessor { public const DamageType DAMAGE_TYPE_RUN_ORIGINAL = (DamageType)100; - private readonly Entities entities; - - public EntityDestroyedProcessor(Entities entities) - { - this.entities = entities; - } + private readonly Entities entities = entities; - public override void Process(EntityDestroyed packet) + public Task Process(ClientProcessorContext context, EntityDestroyed packet) { entities.RemoveEntity(packet.Id); - + if (entities.SpawningEntities) { entities.MarkForDeletion(packet.Id); @@ -31,7 +25,7 @@ public override void Process(EntityDestroyed packet) if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject gameObject)) { Log.Warn($"[{nameof(EntityDestroyedProcessor)}] Could not find entity with id: {packet.Id} to destroy."); - return; + return Task.CompletedTask; } using (PacketSuppressor.Suppress()) @@ -54,6 +48,7 @@ public override void Process(EntityDestroyed packet) Entities.DestroyObject(gameObject); } } + return Task.CompletedTask; } private void DestroyVehicle(Vehicle vehicle) @@ -62,7 +57,7 @@ private void DestroyVehicle(Vehicle vehicle) { vehicle.OnPilotModeEnd(); - if (!Player.main.ToNormalMode(true)) + if (!Player.main.ToNormalMode()) { Player.main.ToNormalMode(false); Player.main.transform.parent = null; diff --git a/NitroxClient/Communication/Packets/Processors/EntityMetadataUpdateProcessor.cs b/NitroxClient/Communication/Packets/Processors/EntityMetadataUpdateProcessor.cs index 976a087dd0..6a7869ef69 100644 --- a/NitroxClient/Communication/Packets/Processors/EntityMetadataUpdateProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EntityMetadataUpdateProcessor.cs @@ -1,28 +1,21 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Helper; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Helper; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class EntityMetadataUpdateProcessor : ClientPacketProcessor +internal sealed class EntityMetadataUpdateProcessor(Entities entities, EntityMetadataManager entityMetadataManager) : IClientPacketProcessor { - private readonly Entities entities; - private readonly EntityMetadataManager entityMetadataManager; - - public EntityMetadataUpdateProcessor(Entities entities, EntityMetadataManager entityMetadataManager) - { - this.entities = entities; - this.entityMetadataManager = entityMetadataManager; - } + private readonly Entities entities = entities; + private readonly EntityMetadataManager entityMetadataManager = entityMetadataManager; - public override void Process(EntityMetadataUpdate update) + public Task Process(ClientProcessorContext context, EntityMetadataUpdate update) { if (entities.SpawningEntities) { @@ -31,12 +24,13 @@ public override void Process(EntityMetadataUpdate update) if (!NitroxEntity.TryGetObjectFrom(update.Id, out GameObject gameObject)) { - return; + return Task.CompletedTask; } Optional metadataProcessor = entityMetadataManager.FromMetaData(update.NewValue); Validate.IsTrue(metadataProcessor.HasValue, $"No processor found for EntityMetadata of type {update.NewValue.GetType()}"); metadataProcessor.Value.ProcessMetadata(gameObject, update.NewValue); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/EntityReparentedProcessor.cs b/NitroxClient/Communication/Packets/Processors/EntityReparentedProcessor.cs index f5135ce96a..a7c7cb1b00 100644 --- a/NitroxClient/Communication/Packets/Processors/EntityReparentedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EntityReparentedProcessor.cs @@ -1,26 +1,20 @@ using System; using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.Helper; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class EntityReparentedProcessor : ClientPacketProcessor +internal sealed class EntityReparentedProcessor(Entities entities) : IClientPacketProcessor { - private readonly Entities entities; + private readonly Entities entities = entities; - public EntityReparentedProcessor(Entities entities) - { - this.entities = entities; - } - - public override void Process(EntityReparented packet) + public Task Process(ClientProcessorContext context, EntityReparented packet) { Optional entity = NitroxEntity.GetObjectFrom(packet.Id); @@ -29,7 +23,7 @@ public override void Process(EntityReparented packet) // In some cases, the affected entity may be pending spawning or out of range. // we only require the parent (in this case, the visible entity is undergoing // some change that must be shown, and if not is an error). - return; + return Task.CompletedTask; } GameObject newParent = NitroxEntity.RequireObjectFrom(packet.NewParentId); @@ -44,16 +38,16 @@ public override void Process(EntityReparented packet) if (waterParkItem.currentWaterPark) { waterParkItem.SetWaterPark(waterPark); - return; + return Task.CompletedTask; } pickupable.SetVisible(false); pickupable.Activate(false); waterPark.AddItem(pickupable); // The reparenting is automatic here so we don't need to continue - return; + return Task.CompletedTask; } // If the entity was parented to a WaterPark but is picked up by someone - else if (waterParkItem) + if (waterParkItem) { pickupable.Deactivate(); waterParkItem.SetWaterPark(null); @@ -74,6 +68,7 @@ public override void Process(EntityReparented packet) PerformDefaultReparenting(entity.Value, newParent); } } + return Task.CompletedTask; } private void InventoryItemReparented(GameObject entity, GameObject newParent) diff --git a/NitroxClient/Communication/Packets/Processors/EntityTransformUpdatesProcessor.cs b/NitroxClient/Communication/Packets/Processors/EntityTransformUpdatesProcessor.cs index 39aed38fab..ca110f698e 100644 --- a/NitroxClient/Communication/Packets/Processors/EntityTransformUpdatesProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EntityTransformUpdatesProcessor.cs @@ -1,24 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; using static Nitrox.Model.Subnautica.Packets.EntityTransformUpdates; namespace NitroxClient.Communication.Packets.Processors; -public class EntityTransformUpdatesProcessor : ClientPacketProcessor +internal sealed class EntityTransformUpdatesProcessor(SimulationOwnership simulationOwnership) : IClientPacketProcessor { - private readonly SimulationOwnership simulationOwnership; - - public EntityTransformUpdatesProcessor(SimulationOwnership simulationOwnership) - { - this.simulationOwnership = simulationOwnership; - } + private readonly SimulationOwnership simulationOwnership = simulationOwnership; - public override void Process(EntityTransformUpdates packet) + public Task Process(ClientProcessorContext context, EntityTransformUpdates packet) { foreach (EntityTransformUpdate update in packet.Updates) { @@ -29,7 +22,8 @@ public override void Process(EntityTransformUpdates packet) continue; } - RemotelyControlled remotelyControlled = RemotelyControlled.Ensure(gameObject);; + RemotelyControlled remotelyControlled = RemotelyControlled.Ensure(gameObject); + ; Vector3 position = update.Position.ToUnity(); Quaternion rotation = update.Rotation.ToUnity(); @@ -43,5 +37,6 @@ public override void Process(EntityTransformUpdates packet) remotelyControlled.UpdateOrientation(position, rotation); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/EscapePodChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/EscapePodChangedProcessor.cs index 02118a9a80..39f3892706 100644 --- a/NitroxClient/Communication/Packets/Processors/EscapePodChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/EscapePodChangedProcessor.cs @@ -1,38 +1,32 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class EscapePodChangedProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - public class EscapePodChangedProcessor : ClientPacketProcessor - { - private readonly PlayerManager remotePlayerManager; + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public EscapePodChangedProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } + public Task Process(ClientProcessorContext context, EscapePodChanged packet) + { + Optional remotePlayer = remotePlayerManager.Find(packet.SessionId); - public override void Process(EscapePodChanged packet) + if (remotePlayer.HasValue) { - Optional remotePlayer = remotePlayerManager.Find(packet.PlayerId); + EscapePod? escapePod = null; - if (remotePlayer.HasValue) + if (packet.EscapePodId.HasValue) { - EscapePod escapePod = null; - - if (packet.EscapePodId.HasValue) - { - GameObject sub = NitroxEntity.RequireObjectFrom(packet.EscapePodId.Value); - escapePod = sub.GetComponent(); - } - - remotePlayer.Value.SetEscapePod(escapePod); + GameObject sub = NitroxEntity.RequireObjectFrom(packet.EscapePodId.Value); + escapePod = sub.GetComponent(); } + + remotePlayer.Value.SetEscapePod(escapePod); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs b/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs index 5dc1f98343..674d559084 100644 --- a/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ExosuitArmActionProcessor.cs @@ -1,19 +1,19 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class ExosuitArmActionProcessor : ClientPacketProcessor +internal sealed class ExosuitArmActionProcessor : IClientPacketProcessor { - public override void Process(ExosuitArmActionPacket packet) + public Task Process(ClientProcessorContext context, ExosuitArmActionPacket packet) { if (!NitroxEntity.TryGetObjectFrom(packet.ExosuitId, out GameObject gameObject)) { Log.Error("Could not find exosuit for arm action"); - return; + return Task.CompletedTask; } Exosuit exosuit = gameObject.RequireComponent(); @@ -40,5 +40,6 @@ public override void Process(ExosuitArmActionPacket packet) Log.Error($"Unhandled arm tech or invalid arm type: {packet.TechType} with action {packet.ArmAction} on {arm.GetGameObject().name} for exosuit {packet.ExosuitId}"); break; } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FMODAssetProcessor.cs b/NitroxClient/Communication/Packets/Processors/FMODAssetProcessor.cs index f5dd99e9f0..e4a8ecbc21 100644 --- a/NitroxClient/Communication/Packets/Processors/FMODAssetProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FMODAssetProcessor.cs @@ -1,29 +1,23 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; -using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Model.Packets; +using Nitrox.Model.GameLogic.FMOD; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; namespace NitroxClient.Communication.Packets.Processors; -public class FMODAssetProcessor : ClientPacketProcessor +internal sealed class FMODAssetProcessor(FMODWhitelist fmodWhitelist) : IClientPacketProcessor { - private readonly FMODWhitelist fmodWhitelist; - - public FMODAssetProcessor(FMODWhitelist fmodWhitelist) - { - this.fmodWhitelist = fmodWhitelist; - } + private readonly FMODWhitelist fmodWhitelist = fmodWhitelist; - public override void Process(FMODAssetPacket packet) + public Task Process(ClientProcessorContext context, FMODAssetPacket packet) { if (!fmodWhitelist.TryGetSoundData(packet.AssetPath, out SoundData soundData)) { Log.ErrorOnce($"[{nameof(FMODAssetProcessor)}] Whitelist has no item for {packet.AssetPath}."); - return; + return Task.CompletedTask; } FMODEmitterController.PlayEventOneShot(packet.AssetPath, soundData.Radius, packet.Position.ToUnity(), packet.Volume); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FMODCustomEmitterProcessor.cs b/NitroxClient/Communication/Packets/Processors/FMODCustomEmitterProcessor.cs index 0302f557ee..f4505caed5 100644 --- a/NitroxClient/Communication/Packets/Processors/FMODCustomEmitterProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FMODCustomEmitterProcessor.cs @@ -1,19 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class FMODCustomEmitterProcessor : ClientPacketProcessor +internal sealed class FMODCustomEmitterProcessor : IClientPacketProcessor { - public override void Process(FMODCustomEmitterPacket packet) + public Task Process(ClientProcessorContext context, FMODCustomEmitterPacket packet) { if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerEntity)) { Log.ErrorOnce($"[{nameof(FMODCustomEmitterProcessor)}] Couldn't find entity {packet.Id}"); - return; + return Task.CompletedTask; } if (!emitterControllerEntity.TryGetComponent(out FMODEmitterController fmodEmitterController)) @@ -34,5 +33,6 @@ public override void Process(FMODCustomEmitterPacket packet) fmodEmitterController.StopCustomEmitter(packet.AssetPath); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FMODCustomLoopingEmitterProcessor.cs b/NitroxClient/Communication/Packets/Processors/FMODCustomLoopingEmitterProcessor.cs index 2f01b5a256..1b69f237f9 100644 --- a/NitroxClient/Communication/Packets/Processors/FMODCustomLoopingEmitterProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FMODCustomLoopingEmitterProcessor.cs @@ -1,19 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class FMODCustomLoopingEmitterProcessor : ClientPacketProcessor +internal sealed class FMODCustomLoopingEmitterProcessor : IClientPacketProcessor { - public override void Process(FMODCustomLoopingEmitterPacket packet) + public Task Process(ClientProcessorContext context, FMODCustomLoopingEmitterPacket packet) { if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject)) { Log.ErrorOnce($"[{nameof(FMODCustomLoopingEmitterProcessor)}] Couldn't find entity {packet.Id}"); - return; + return Task.CompletedTask; } if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController)) @@ -23,5 +22,6 @@ public override void Process(FMODCustomLoopingEmitterPacket packet) } fmodEmitterController.PlayCustomLoopingEmitter(packet.AssetPath); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FMODEventInstanceProcessor.cs b/NitroxClient/Communication/Packets/Processors/FMODEventInstanceProcessor.cs index 9c09d2ec88..a320521142 100644 --- a/NitroxClient/Communication/Packets/Processors/FMODEventInstanceProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FMODEventInstanceProcessor.cs @@ -1,19 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class FMODEventInstanceProcessor : ClientPacketProcessor +internal sealed class FMODEventInstanceProcessor : IClientPacketProcessor { - public override void Process(FMODEventInstancePacket packet) + public Task Process(ClientProcessorContext context, FMODEventInstancePacket packet) { if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject)) { Log.ErrorOnce($"[{nameof(FMODEventInstanceProcessor)}] Couldn't find entity {packet.Id}"); - return; + return Task.CompletedTask; } if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController)) @@ -30,5 +29,6 @@ public override void Process(FMODEventInstancePacket packet) { fmodEmitterController.StopEventInstance(packet.AssetPath); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FMODStudioEventEmitterProcessor.cs b/NitroxClient/Communication/Packets/Processors/FMODStudioEventEmitterProcessor.cs index a54b6c8822..92f927fe9c 100644 --- a/NitroxClient/Communication/Packets/Processors/FMODStudioEventEmitterProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FMODStudioEventEmitterProcessor.cs @@ -1,19 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class FMODStudioEventEmitterProcessor : ClientPacketProcessor +internal sealed class FMODStudioEventEmitterProcessor : IClientPacketProcessor { - public override void Process(FMODStudioEmitterPacket packet) + public Task Process(ClientProcessorContext context, FMODStudioEmitterPacket packet) { if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject)) { Log.ErrorOnce($"[{nameof(FMODStudioEventEmitterProcessor)}] Couldn't find entity {packet.Id}"); - return; + return Task.CompletedTask; } if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController)) @@ -33,5 +32,6 @@ public override void Process(FMODStudioEmitterPacket packet) fmodEmitterController.StopStudioEmitter(packet.AssetPath, packet.AllowFadeout); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FastCheatChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/FastCheatChangedProcessor.cs index 23efca0fe2..1e500404b4 100644 --- a/NitroxClient/Communication/Packets/Processors/FastCheatChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FastCheatChangedProcessor.cs @@ -1,11 +1,11 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient.Communication.Packets.Processors; -public class FastCheatChangedProcessor : ClientPacketProcessor +internal sealed class FastCheatChangedProcessor : IClientPacketProcessor { - public override void Process(FastCheatChanged packet) + public Task Process(ClientProcessorContext context, FastCheatChanged packet) { switch (packet.Cheat) { @@ -17,5 +17,6 @@ public override void Process(FastCheatChanged packet) NoCostConsoleCommand.main.fastGrowCheat = packet.Value; break; } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FireDousedProcessor.cs b/NitroxClient/Communication/Packets/Processors/FireDousedProcessor.cs index e43b03cd4b..0cd96cf551 100644 --- a/NitroxClient/Communication/Packets/Processors/FireDousedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FireDousedProcessor.cs @@ -1,33 +1,28 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class FireDousedProcessor(IPacketSender packetSender) : IClientPacketProcessor { - public class FireDousedProcessor : ClientPacketProcessor - { - private readonly IPacketSender packetSender; + private readonly IPacketSender packetSender = packetSender; - public FireDousedProcessor(IPacketSender packetSender) - { - this.packetSender = packetSender; - } + /// + /// Finds and executes . If the fire is extinguished, it will pass a large float to + /// trigger the private + /// method. + /// + public Task Process(ClientProcessorContext context, FireDoused packet) + { + GameObject fireGameObject = NitroxEntity.RequireObjectFrom(packet.Id); - /// - /// Finds and executes . If the fire is extinguished, it will pass a large float to trigger the private - /// method. - /// - public override void Process(FireDoused packet) + using (PacketSuppressor.Suppress()) { - GameObject fireGameObject = NitroxEntity.RequireObjectFrom(packet.Id); - - using (PacketSuppressor.Suppress()) - { - fireGameObject.RequireComponent().Douse(packet.DouseAmount); - } + fireGameObject.RequireComponent().Douse(packet.DouseAmount); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/FootstepPacketProcessor.cs b/NitroxClient/Communication/Packets/Processors/FootstepPacketProcessor.cs index 9581460e9a..696ff995a7 100644 --- a/NitroxClient/Communication/Packets/Processors/FootstepPacketProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/FootstepPacketProcessor.cs @@ -3,22 +3,21 @@ using FMOD.Studio; using FMODUnity; using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using NitroxClient.GameLogic.FMOD; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; +using NitroxClient.GameLogic.FMOD; namespace NitroxClient.Communication.Packets.Processors; -public class FootstepPacketProcessor : ClientPacketProcessor +internal sealed class FootstepPacketProcessor : IClientPacketProcessor { - private readonly PlayerManager remotePlayerManager; + private const float FOOTSTEP_AUDIO_MAX_VOLUME = 0.5f; + private readonly float footstepAudioRadius; // To modify this value, modify the last value in the SoundWhitelist_Subnautica.csv file private readonly Lazy localFootstepSounds = new(() => Player.mainObject.GetComponent()); + private readonly PlayerManager remotePlayerManager; private PARAMETER_ID fmodIndexSpeed = FMODUWE.invalidParameterId; - private readonly float footstepAudioRadius; // To modify this value, modify the last value in the SoundWhitelist_Subnautica.csv file - private const float FOOTSTEP_AUDIO_MAX_VOLUME = 0.5f; public FootstepPacketProcessor(PlayerManager remotePlayerManager, FMODWhitelist whitelist) { @@ -27,9 +26,9 @@ public FootstepPacketProcessor(PlayerManager remotePlayerManager, FMODWhitelist footstepAudioRadius = soundData.Radius; } - public override void Process(FootstepPacket packet) + public Task Process(ClientProcessorContext context, FootstepPacket packet) { - Optional player = remotePlayerManager.Find(packet.PlayerID); + Optional player = remotePlayerManager.Find(packet.SessionId); if (player.HasValue) { FMODAsset asset = packet.AssetIndex switch @@ -54,5 +53,6 @@ public override void Process(FootstepPacket packet) evt.release(); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/GameModeChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/GameModeChangedProcessor.cs index 5f4d41ffa7..bd383e782c 100644 --- a/NitroxClient/Communication/Packets/Processors/GameModeChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/GameModeChangedProcessor.cs @@ -1,24 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class GameModeChangedProcessor : ClientPacketProcessor +internal sealed class GameModeChangedProcessor(LocalPlayer localPlayer, PlayerManager playerManager) : IClientPacketProcessor { - private readonly LocalPlayer localPlayer; - private readonly PlayerManager playerManager; - - public GameModeChangedProcessor(LocalPlayer localPlayer, PlayerManager playerManager) - { - this.localPlayer = localPlayer; - this.playerManager = playerManager; - } + private readonly LocalPlayer localPlayer = localPlayer; + private readonly PlayerManager playerManager = playerManager; - public override void Process(GameModeChanged packet) + public Task Process(ClientProcessorContext context, GameModeChanged packet) { - if (packet.AllPlayers || packet.PlayerId == localPlayer.PlayerId) + if (packet.AllPlayers || packet.SessionId == localPlayer.SessionId) { GameModeUtils.SetGameMode((GameModeOption)(int)packet.GameMode, GameModeOption.None); } @@ -29,9 +22,10 @@ public override void Process(GameModeChanged packet) remotePlayer.SetGameMode(packet.GameMode); } } - else if (playerManager.TryFind(packet.PlayerId, out RemotePlayer remotePlayer)) + else if (playerManager.TryFind(packet.SessionId, out RemotePlayer remotePlayer)) { remotePlayer.SetGameMode(packet.GameMode); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/GrapplingHookMovementProcessor.cs b/NitroxClient/Communication/Packets/Processors/GrapplingHookMovementProcessor.cs index f6195aeb80..f78c58693b 100644 --- a/NitroxClient/Communication/Packets/Processors/GrapplingHookMovementProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/GrapplingHookMovementProcessor.cs @@ -1,13 +1,13 @@ using Nitrox.Model.Subnautica.Packets; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class GrapplingHookMovementProcessor : ClientPacketProcessor +internal sealed class GrapplingHookMovementProcessor : IClientPacketProcessor { - public override void Process(GrapplingHookMovement packet) + public Task Process(ClientProcessorContext context, GrapplingHookMovement packet) { Exosuit exosuit = NitroxEntity.RequireObjectFrom(packet.ExosuitId).RequireComponent(); IExosuitArm arm = packet.ArmSide == Exosuit.Arm.Left ? exosuit.leftArm : exosuit.rightArm; @@ -15,7 +15,7 @@ public override void Process(GrapplingHookMovement packet) if (arm is not ExosuitGrapplingArm grapplingArm) { Log.Error($"{packet.ArmSide} arm of exosuit {packet.ExosuitId} is not a grappling arm"); - return; + return Task.CompletedTask; } if (grapplingArm.hook.resting) @@ -28,5 +28,6 @@ public override void Process(GrapplingHookMovement packet) rb.position = packet.Position.ToUnity(); rb.velocity = packet.Velocity.ToUnity(); rb.rotation = packet.Rotation.ToUnity(); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs index 839653e8e2..f93a68b352 100644 --- a/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/InitialPlayerSyncProcessor.cs @@ -1,122 +1,114 @@ using System; using System.Collections; using System.Collections.Generic; +using Nitrox.Model.Subnautica.Packets; using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class InitialPlayerSyncProcessor(IEnumerable processors) : IClientPacketProcessor { - public class InitialPlayerSyncProcessor : ClientPacketProcessor - { - private readonly IPacketSender packetSender; - private readonly HashSet processors; - private readonly HashSet alreadyRan = []; - private InitialPlayerSync packet; + private readonly HashSet alreadyRan = []; + private readonly HashSet processors = processors.ToSet(); - private WaitScreen.ManualWaitItem loadingMultiplayerWaitItem; - private WaitScreen.ManualWaitItem subWaitScreenItem; + private int cumulativeProcessorsRan; - private int cumulativeProcessorsRan; - private int processorsRanLastCycle; + private WaitScreen.ManualWaitItem loadingMultiplayerWaitItem; + private InitialPlayerSync packet; + private int processorsRanLastCycle; + private WaitScreen.ManualWaitItem subWaitScreenItem; - public InitialPlayerSyncProcessor(IPacketSender packetSender, IEnumerable processors) - { - this.packetSender = packetSender; - this.processors = processors.ToSet(); - } - - public override void Process(InitialPlayerSync packet) - { - this.packet = packet; + public Task Process(ClientProcessorContext context, InitialPlayerSync packet) + { + this.packet = packet; - loadingMultiplayerWaitItem = WaitScreen.Add(Language.main.Get("Nitrox_SyncingWorld")); - Log.InGame(Language.main.Get("Nitrox_SyncingWorld")); + loadingMultiplayerWaitItem = WaitScreen.Add(Language.main.Get("Nitrox_SyncingWorld")); + Log.InGame(Language.main.Get("Nitrox_SyncingWorld")); - cumulativeProcessorsRan = 0; - Multiplayer.Main.StartCoroutine(ProcessInitialSyncPacket(this, null)); - } + cumulativeProcessorsRan = 0; + Multiplayer.Main.StartCoroutine(ProcessInitialSyncPacket(context)); + return Task.CompletedTask; + } - private IEnumerator ProcessInitialSyncPacket(object sender, EventArgs eventArgs) + private IEnumerator ProcessInitialSyncPacket(ClientProcessorContext context) + { + bool moreProcessorsToRun; + do { - bool moreProcessorsToRun; - do + yield return Multiplayer.Main.StartCoroutine(RunPendingProcessors()); + + moreProcessorsToRun = alreadyRan.Count < processors.Count; + if (moreProcessorsToRun && processorsRanLastCycle == 0) { - yield return Multiplayer.Main.StartCoroutine(RunPendingProcessors()); + throw new Exception($"Detected circular dependencies in initial packet sync between: {GetRemainingProcessorsText()}"); + } + } while (moreProcessorsToRun); - moreProcessorsToRun = alreadyRan.Count < processors.Count; - if (moreProcessorsToRun && processorsRanLastCycle == 0) - { - throw new Exception($"Detected circular dependencies in initial packet sync between: {GetRemainingProcessorsText()}"); - } - } while (moreProcessorsToRun); + WaitScreen.Remove(loadingMultiplayerWaitItem); + Multiplayer.Main.InitialSyncCompleted = true; - WaitScreen.Remove(loadingMultiplayerWaitItem); - Multiplayer.Main.InitialSyncCompleted = true; + // When the player finishes loading, we can take back his invincibility + Player.main.liveMixin.invincible = false; + Player.main.UnfreezeStats(); - // When the player finishes loading, we can take back his invincibility - Player.main.liveMixin.invincible = false; - Player.main.UnfreezeStats(); + context.Send(new PlayerSyncFinished()); + } - packetSender.Send(new PlayerSyncFinished()); - } + private IEnumerator RunPendingProcessors() + { + processorsRanLastCycle = 0; - private IEnumerator RunPendingProcessors() + foreach (IInitialSyncProcessor processor in processors) { - processorsRanLastCycle = 0; - - foreach (IInitialSyncProcessor processor in processors) + if (IsWaitingToRun(processor.GetType()) && HasDependenciesSatisfied(processor)) { - if (IsWaitingToRun(processor.GetType()) && HasDependenciesSatisfied(processor)) - { - loadingMultiplayerWaitItem.SetProgress(cumulativeProcessorsRan, processors.Count); - - alreadyRan.Add(processor.GetType()); - processorsRanLastCycle++; - cumulativeProcessorsRan++; - - Log.Info($"Running {processor.GetType()}"); - subWaitScreenItem = WaitScreen.Add($"Running {processor.GetType().Name}"); - yield return Multiplayer.Main.StartCoroutine(processor.Process(packet, subWaitScreenItem)); - WaitScreen.Remove(subWaitScreenItem); - } + loadingMultiplayerWaitItem.SetProgress(cumulativeProcessorsRan, processors.Count); + + alreadyRan.Add(processor.GetType()); + processorsRanLastCycle++; + cumulativeProcessorsRan++; + + Log.Info($"Running {processor.GetType()}"); + subWaitScreenItem = WaitScreen.Add($"Running {processor.GetType().Name}"); + yield return Multiplayer.Main.StartCoroutine(processor.Process(packet, subWaitScreenItem)); + WaitScreen.Remove(subWaitScreenItem); } } + } - private bool HasDependenciesSatisfied(IInitialSyncProcessor processor) + private bool HasDependenciesSatisfied(IInitialSyncProcessor processor) + { + foreach (Type dependentType in processor.DependentProcessors) { - foreach (Type dependentType in processor.DependentProcessors) + if (IsWaitingToRun(dependentType)) { - if (IsWaitingToRun(dependentType)) - { - return false; - } + return false; } - - return true; } - private bool IsWaitingToRun(Type processor) - { - return alreadyRan.Contains(processor) == false; - } + return true; + } - private string GetRemainingProcessorsText() - { - string remaining = ""; + private bool IsWaitingToRun(Type processor) + { + return !alreadyRan.Contains(processor); + } + + private string GetRemainingProcessorsText() + { + string remaining = ""; - foreach (IInitialSyncProcessor processor in processors) + foreach (IInitialSyncProcessor processor in processors) + { + if (IsWaitingToRun(processor.GetType())) { - if (IsWaitingToRun(processor.GetType())) - { - remaining += $" {processor.GetType()}"; - } + remaining += $" {processor.GetType()}"; } - - return remaining; } + + return remaining; } } diff --git a/NitroxClient/Communication/Packets/Processors/ItemPositionProcessor.cs b/NitroxClient/Communication/Packets/Processors/ItemPositionProcessor.cs index 769ed79de9..531fcbe189 100644 --- a/NitroxClient/Communication/Packets/Processors/ItemPositionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ItemPositionProcessor.cs @@ -1,26 +1,24 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal class ItemPositionProcessor : IClientPacketProcessor { - class ItemPositionProcessor : ClientPacketProcessor + private const float ITEM_TRANSFORM_SMOOTH_PERIOD = 0.25f; + + public Task Process(ClientProcessorContext context, ItemPosition drop) { - private const float ITEM_TRANSFORM_SMOOTH_PERIOD = 0.25f; + Optional opItem = NitroxEntity.GetObjectFrom(drop.Id); - public override void Process(ItemPosition drop) + if (opItem.HasValue) { - Optional opItem = NitroxEntity.GetObjectFrom(drop.Id); - - if (opItem.HasValue) - { - MovementHelper.MoveRotateGameObject(opItem.Value, drop.Position.ToUnity(), drop.Rotation.ToUnity(), ITEM_TRANSFORM_SMOOTH_PERIOD); - } + MovementHelper.MoveRotateGameObject(opItem.Value, drop.Position.ToUnity(), drop.Rotation.ToUnity(), ITEM_TRANSFORM_SMOOTH_PERIOD); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs index c196267ca6..88fce9e3e8 100644 --- a/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/JoinQueueInfoProcessor.cs @@ -1,15 +1,16 @@ using System; using Nitrox.Model.Subnautica.Packets; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient.Communication.Packets.Processors; -public class JoinQueueInfoProcessor : ClientPacketProcessor +internal sealed class JoinQueueInfoProcessor : IClientPacketProcessor { - public override void Process(JoinQueueInfo packet) + public Task Process(ClientProcessorContext context, JoinQueueInfo packet) { Log.InGame(Language.main.Get("Nitrox_QueueInfo") - .Replace("{POSITION}", packet.Position.ToString()) - .Replace("{TIME}", TimeSpan.FromMilliseconds(packet.Timeout * packet.Position).ToString(@"mm\:ss"))); + .Replace("{POSITION}", packet.Position.ToString()) + .Replace("{TIME}", TimeSpan.FromMilliseconds(packet.Timeout * packet.Position).ToString(@"mm\:ss"))); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/KeepInventoryChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/KeepInventoryChangedProcessor.cs new file mode 100644 index 0000000000..1cc6c49509 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/KeepInventoryChangedProcessor.cs @@ -0,0 +1,16 @@ +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; + +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class KeepInventoryChangedProcessor(LocalPlayer localPlayer) : IClientPacketProcessor +{ + private readonly LocalPlayer localPlayer = localPlayer; + + public Task Process(ClientProcessorContext context, KeepInventoryChanged packet) + { + localPlayer.KeepInventoryOnDeath = packet.KeepInventoryOnDeath; + return Task.CompletedTask; + } +} diff --git a/NitroxClient/Communication/Packets/Processors/KnownTechEntryAddProcessor.cs b/NitroxClient/Communication/Packets/Processors/KnownTechEntryAddProcessor.cs index 1e43351e55..4b20813c4b 100644 --- a/NitroxClient/Communication/Packets/Processors/KnownTechEntryAddProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/KnownTechEntryAddProcessor.cs @@ -1,22 +1,13 @@ using System; -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient.Communication.Packets.Processors; -public class KnownTechEntryProcessorAdd : ClientPacketProcessor +internal sealed class KnownTechEntryProcessorAdd : IClientPacketProcessor { - private readonly IPacketSender packetSender; - - public KnownTechEntryProcessorAdd(IPacketSender packetSender) - { - this.packetSender = packetSender; - } - - public override void Process(KnownTechEntryAdd packet) + public Task Process(ClientProcessorContext context, KnownTechEntryAdd packet) { using (PacketSuppressor.Suppress()) { @@ -34,5 +25,6 @@ public override void Process(KnownTechEntryAdd packet) break; } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/LeakRepairedProcessor.cs b/NitroxClient/Communication/Packets/Processors/LeakRepairedProcessor.cs index e9a59fd585..10630d9f3c 100644 --- a/NitroxClient/Communication/Packets/Processors/LeakRepairedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/LeakRepairedProcessor.cs @@ -1,18 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; namespace NitroxClient.Communication.Packets.Processors; -public class LeakRepairedProcessor : ClientPacketProcessor +internal sealed class LeakRepairedProcessor : IClientPacketProcessor { - public override void Process(LeakRepaired packet) + public Task Process(ClientProcessorContext context, LeakRepaired packet) { if (NitroxEntity.TryGetComponentFrom(packet.BaseId, out BaseLeakManager baseLeakManager)) { baseLeakManager.HealLeakToMax(packet.RelativeCell.ToUnity()); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/MedicalCabinetClickedProcessor.cs b/NitroxClient/Communication/Packets/Processors/MedicalCabinetClickedProcessor.cs index 786c8ac148..6c16dff07c 100644 --- a/NitroxClient/Communication/Packets/Processors/MedicalCabinetClickedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/MedicalCabinetClickedProcessor.cs @@ -1,15 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic.FMOD; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class MedicalCabinetClickedProcessor : ClientPacketProcessor +internal sealed class MedicalCabinetClickedProcessor : IClientPacketProcessor { - public override void Process(MedicalCabinetClicked packet) + public Task Process(ClientProcessorContext context, MedicalCabinetClicked packet) { GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); MedicalCabinet cabinet = gameObject.RequireComponent(); @@ -32,5 +31,6 @@ public override void Process(MedicalCabinetClicked packet) cabinet.Invoke(nameof(MedicalCabinet.ToggleDoorState), 2f); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/MultiplayerSessionPolicyProcessor.cs b/NitroxClient/Communication/Packets/Processors/MultiplayerSessionPolicyProcessor.cs index be0959647f..a819689748 100644 --- a/NitroxClient/Communication/Packets/Processors/MultiplayerSessionPolicyProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/MultiplayerSessionPolicyProcessor.cs @@ -1,23 +1,17 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; -namespace NitroxClient.Communication.Packets.Processors -{ - public class MultiplayerSessionPolicyProcessor : ClientPacketProcessor - { - private readonly IMultiplayerSession multiplayerSession; +namespace NitroxClient.Communication.Packets.Processors; - public MultiplayerSessionPolicyProcessor(IMultiplayerSession multiplayerSession) - { - this.multiplayerSession = multiplayerSession; - } +internal sealed class MultiplayerSessionPolicyProcessor(IMultiplayerSession multiplayerSession) : IClientPacketProcessor +{ + private readonly IMultiplayerSession multiplayerSession = multiplayerSession; - public override void Process(MultiplayerSessionPolicy packet) - { - Log.Info("Processing session policy information."); - multiplayerSession.ProcessSessionPolicy(packet); - } + public Task Process(ClientProcessorContext context, MultiplayerSessionPolicy packet) + { + Log.Info("Processing session policy information."); + multiplayerSession.ProcessSessionPolicy(packet); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/MultiplayerSessionReservationProcessor.cs b/NitroxClient/Communication/Packets/Processors/MultiplayerSessionReservationProcessor.cs index 5d4797746a..be00a0c2dc 100644 --- a/NitroxClient/Communication/Packets/Processors/MultiplayerSessionReservationProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/MultiplayerSessionReservationProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; -namespace NitroxClient.Communication.Packets.Processors -{ - public class MultiplayerSessionReservationProcessor : ClientPacketProcessor - { - private readonly IMultiplayerSession multiplayerSession; +namespace NitroxClient.Communication.Packets.Processors; - public MultiplayerSessionReservationProcessor(IMultiplayerSession multiplayerSession) - { - this.multiplayerSession = multiplayerSession; - } +internal sealed class MultiplayerSessionReservationProcessor(IMultiplayerSession multiplayerSession) : IClientPacketProcessor +{ + private readonly IMultiplayerSession multiplayerSession = multiplayerSession; - public override void Process(MultiplayerSessionReservation packet) - { - multiplayerSession.ProcessReservationResponsePacket(packet); - } + public Task Process(ClientProcessorContext context, MultiplayerSessionReservation packet) + { + multiplayerSession.ProcessReservationResponsePacket(packet); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/MutePlayerProcessor.cs b/NitroxClient/Communication/Packets/Processors/MutePlayerProcessor.cs index bf1da6857b..cc249cc54b 100644 --- a/NitroxClient/Communication/Packets/Processors/MutePlayerProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/MutePlayerProcessor.cs @@ -1,31 +1,27 @@ -using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class MutePlayerProcessor : ClientPacketProcessor +internal sealed class MutePlayerProcessor(PlayerManager playerManager) : IClientPacketProcessor { - private readonly PlayerManager playerManager; + public delegate void PlayerMuted(SessionId sessionId, bool muted); - public delegate void PlayerMuted(ushort playerId, bool muted); + private readonly PlayerManager playerManager = playerManager; public PlayerMuted OnPlayerMuted; - public MutePlayerProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } - - public override void Process(MutePlayer packet) + public Task Process(ClientProcessorContext context, MutePlayer packet) { // We only need to notice if that's another player than local player - Optional player = playerManager.Find(packet.PlayerId); + Optional player = playerManager.Find(packet.SessionId); if (player.HasValue) { player.Value.PlayerContext.IsMuted = packet.Muted; } - OnPlayerMuted(packet.PlayerId, packet.Muted); + OnPlayerMuted(packet.SessionId, packet.Muted); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/OpenableStateChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/OpenableStateChangedProcessor.cs index f30a575526..6b838b6add 100644 --- a/NitroxClient/Communication/Packets/Processors/OpenableStateChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/OpenableStateChangedProcessor.cs @@ -1,30 +1,22 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class OpenableStateChangedProcessor : IClientPacketProcessor { - public class OpenableStateChangedProcessor : ClientPacketProcessor + public Task Process(ClientProcessorContext context, OpenableStateChanged packet) { - private readonly IPacketSender packetSender; + GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); + Openable openable = gameObject.RequireComponent(); - public OpenableStateChangedProcessor(IPacketSender packetSender) + using (PacketSuppressor.Suppress()) { - this.packetSender = packetSender; - } - - public override void Process(OpenableStateChanged packet) - { - GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); - Openable openable = gameObject.RequireComponent(); - - using (PacketSuppressor.Suppress()) - { - openable.PlayOpenAnimation(packet.IsOpen, packet.Duration); - } + openable.PlayOpenAnimation(packet.IsOpen, packet.Duration); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs b/NitroxClient/Communication/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs index 4b49ddbb37..4e2953d7f1 100644 --- a/NitroxClient/Communication/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PDAEncyclopediaEntryAddProcessor.cs @@ -1,24 +1,17 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient.Communication.Packets.Processors; -public class PDAEncyclopediaEntryAddProcessor : ClientPacketProcessor +internal sealed class PDAEncyclopediaEntryAddProcessor : IClientPacketProcessor { - private readonly IPacketSender packetSender; - - public PDAEncyclopediaEntryAddProcessor(IPacketSender packetSender) - { - this.packetSender = packetSender; - } - - public override void Process(PDAEncyclopediaEntryAdd packet) + public Task Process(ClientProcessorContext context, PDAEncyclopediaEntryAdd packet) { using (PacketSuppressor.Suppress()) { PDAEncyclopedia.Add(packet.Key, packet.Verbose); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PDALogEntryAddProcessor.cs b/NitroxClient/Communication/Packets/Processors/PDALogEntryAddProcessor.cs index 174a094c5c..49f2eddf6b 100644 --- a/NitroxClient/Communication/Packets/Processors/PDALogEntryAddProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PDALogEntryAddProcessor.cs @@ -1,25 +1,17 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class PDALogEntryAddProcessor : IClientPacketProcessor { - public class PDALogEntryAddProcessor : ClientPacketProcessor + public Task Process(ClientProcessorContext context, PDALogEntryAdd packet) { - private readonly IPacketSender packetSender; - - public PDALogEntryAddProcessor(IPacketSender packetSender) - { - this.packetSender = packetSender; - } - - public override void Process(PDALogEntryAdd packet) + using (PacketSuppressor.Suppress()) { - using (PacketSuppressor.Suppress()) - { - PDALog.Add(packet.Key); - } + PDALog.Add(packet.Key); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PDAScanFinishedProcessor.cs b/NitroxClient/Communication/Packets/Processors/PDAScanFinishedProcessor.cs index 0ec91ce661..988f759783 100644 --- a/NitroxClient/Communication/Packets/Processors/PDAScanFinishedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PDAScanFinishedProcessor.cs @@ -1,14 +1,12 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class PDAScanFinishedProcessor : ClientPacketProcessor +internal sealed class PDAScanFinishedProcessor : IClientPacketProcessor { - public override void Process(PDAScanFinished packet) + public Task Process(ClientProcessorContext context, PDAScanFinished packet) { if (packet.Id != null) { @@ -16,14 +14,14 @@ public override void Process(PDAScanFinished packet) } if (packet.WasAlreadyResearched) { - return; + return Task.CompletedTask; } TechType packetTechType = packet.TechType.ToUnity(); if (packet.FullyResearched) { PDAScanner.partial.RemoveAllFast(packetTechType, static (item, techType) => item.techType == techType); PDAScanner.complete.Add(packetTechType); - return; + return Task.CompletedTask; } if (PDAScanner.GetPartialEntryByKey(packetTechType, out PDAScanner.Entry entry)) { @@ -33,5 +31,6 @@ public override void Process(PDAScanFinished packet) { PDAScanner.Add(packetTechType, packet.UnlockedAmount); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PermsChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/PermsChangedProcessor.cs index 1608110e60..5434a5b5a5 100644 --- a/NitroxClient/Communication/Packets/Processors/PermsChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PermsChangedProcessor.cs @@ -1,26 +1,21 @@ using Nitrox.Model.DataStructures.GameLogic; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class PermsChangedProcessor : ClientPacketProcessor +internal sealed class PermsChangedProcessor(LocalPlayer localPlayer) : IClientPacketProcessor { - private LocalPlayer localPlayer; - public delegate void PermissionsChanged(Perms perms); - public PermissionsChanged OnPermissionsChanged; - public PermsChangedProcessor(LocalPlayer localPlayer) - { - this.localPlayer = localPlayer; - } + private readonly LocalPlayer localPlayer = localPlayer; + public PermissionsChanged OnPermissionsChanged; - public override void Process(PermsChanged packet) + public Task Process(ClientProcessorContext context, PermsChanged packet) { localPlayer.Permissions = packet.NewPerms; OnPermissionsChanged(packet.NewPerms); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlaySunbeamEventProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlaySunbeamEventProcessor.cs index 94df9d6513..c14ee6d2b5 100644 --- a/NitroxClient/Communication/Packets/Processors/PlaySunbeamEventProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlaySunbeamEventProcessor.cs @@ -1,19 +1,19 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using Story; namespace NitroxClient.Communication.Packets.Processors; -public class PlaySunbeamEventProcessor : ClientPacketProcessor +internal sealed class PlaySunbeamEventProcessor : IClientPacketProcessor { - public override void Process(PlaySunbeamEvent packet) + public Task Process(ClientProcessorContext context, PlaySunbeamEvent packet) { // TODO: Look into compound goals and OnUnlock goals to bring back the necessary ones int beginIndex = PlaySunbeamEvent.SunbeamGoals.GetIndex(packet.EventKey); if (beginIndex == -1) { Log.Error($"Couldn't find the corresponding sunbeam event in {nameof(PlaySunbeamEvent.SunbeamGoals)} for key {packet.EventKey}"); - return; + return Task.CompletedTask; } for (int i = beginIndex; i < PlaySunbeamEvent.SunbeamGoals.Length; i++) { @@ -21,5 +21,6 @@ public override void Process(PlaySunbeamEvent packet) } // Same execution as for StoryGoalCustomEventHandler commands StoryGoalManager.main.OnGoalComplete(packet.EventKey); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerCinematicControllerCallProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerCinematicControllerCallProcessor.cs index a2e51b3848..85623fa68b 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerCinematicControllerCallProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerCinematicControllerCallProcessor.cs @@ -1,38 +1,32 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Helper; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using NitroxClient.MonoBehaviours.CinematicController; -using Nitrox.Model.Helper; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerCinematicControllerCallProcessor : ClientPacketProcessor +internal sealed class PlayerCinematicControllerCallProcessor(PlayerManager playerManager) : IClientPacketProcessor { - private readonly PlayerManager playerManager; - - public PlayerCinematicControllerCallProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } + private readonly PlayerManager playerManager = playerManager; - public override void Process(PlayerCinematicControllerCall packet) + public Task Process(ClientProcessorContext context, PlayerCinematicControllerCall packet) { if (!NitroxEntity.TryGetObjectFrom(packet.ControllerID, out GameObject entity)) { - return; // Entity can be not spawned yet bc async. + return Task.CompletedTask; } if (!entity.TryGetComponent(out MultiplayerCinematicReference reference)) { Log.Warn($"Couldn't find {nameof(MultiplayerCinematicReference)} on {entity.name}:{packet.ControllerID}"); - return; + return Task.CompletedTask; } - Optional opPlayer = playerManager.Find(packet.PlayerId); + Optional opPlayer = playerManager.Find(packet.SessionId); Validate.IsPresent(opPlayer); if (packet.StartPlaying) @@ -43,5 +37,6 @@ public override void Process(PlayerCinematicControllerCall packet) { reference.CallCinematicModeEnd(packet.Key, packet.ControllerNameHash, opPlayer.Value); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs index e03151017b..dc41599701 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerDeathProcessor.cs @@ -1,26 +1,21 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Helper; -using Nitrox.Model.Packets; +using Nitrox.Model.Helper; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerDeathProcessor : ClientPacketProcessor +internal sealed class PlayerDeathProcessor(PlayerManager playerManager) : IClientPacketProcessor { - private readonly PlayerManager playerManager; - - public PlayerDeathProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } + private readonly PlayerManager playerManager = playerManager; - public override void Process(PlayerDeathEvent playerDeath) + public Task Process(ClientProcessorContext context, PlayerDeathEvent playerDeath) { - RemotePlayer player = Validate.IsPresent(playerManager.Find(playerDeath.PlayerId)); + RemotePlayer player = Validate.IsPresent(playerManager.Find(playerDeath.SessionId)); Log.Debug($"{player.PlayerName} died"); Log.InGame(Language.main.Get("Nitrox_PlayerDied").Replace("{PLAYER}", player.PlayerName)); player.PlayerDeathEvent.Trigger(player); + return Task.CompletedTask; // TODO: Add any death related triggers (i.e. scoreboard updates, rewards, etc.) } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerHeldItemChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerHeldItemChangedProcessor.cs index c2a4136594..661da79820 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerHeldItemChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerHeldItemChangedProcessor.cs @@ -3,18 +3,18 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.Helper; using Nitrox.Model.Subnautica.Packets; -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerHeldItemChangedProcessor : ClientPacketProcessor +internal sealed class PlayerHeldItemChangedProcessor : IClientPacketProcessor { + private readonly PlayerManager playerManager; private int defaultLayer; private int viewModelLayer; - private readonly PlayerManager playerManager; public PlayerHeldItemChangedProcessor(PlayerManager playerManager) { @@ -26,20 +26,14 @@ public PlayerHeldItemChangedProcessor(PlayerManager playerManager) } } - private void SetupLayers() - { - defaultLayer = LayerMask.NameToLayer("Default"); - viewModelLayer = LayerMask.NameToLayer("Viewmodel"); - } - - public override void Process(PlayerHeldItemChanged packet) + public Task Process(ClientProcessorContext context, PlayerHeldItemChanged packet) { - Optional opPlayer = playerManager.Find(packet.PlayerId); + Optional opPlayer = playerManager.Find(packet.SessionId); Validate.IsPresent(opPlayer); if (!NitroxEntity.TryGetObjectFrom(packet.ItemId, out GameObject item)) { - return; // Item can be not spawned yet bc async. + return Task.CompletedTask; } Pickupable pickupable = item.GetComponent(); @@ -124,5 +118,12 @@ public override void Process(PlayerHeldItemChanged packet) default: throw new ArgumentOutOfRangeException(nameof(packet.Type)); } + return Task.CompletedTask; + } + + private void SetupLayers() + { + defaultLayer = LayerMask.NameToLayer("Default"); + viewModelLayer = LayerMask.NameToLayer("Viewmodel"); } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerInCyclopsMovementProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerInCyclopsMovementProcessor.cs index 91b5ce513b..4e588aabdc 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerInCyclopsMovementProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerInCyclopsMovementProcessor.cs @@ -1,25 +1,19 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerInCyclopsMovementProcessor : ClientPacketProcessor +internal sealed class PlayerInCyclopsMovementProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - private readonly PlayerManager remotePlayerManager; - - public PlayerInCyclopsMovementProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public override void Process(PlayerInCyclopsMovement movement) + public Task Process(ClientProcessorContext context, PlayerInCyclopsMovement movement) { - if (remotePlayerManager.TryFind(movement.PlayerId, out RemotePlayer remotePlayer) && remotePlayer.Pawn != null) + if (remotePlayerManager.TryFind(movement.SessionId, out RemotePlayer remotePlayer) && remotePlayer.Pawn != null) { remotePlayer.UpdatePositionInCyclops(movement.LocalPosition.ToUnity(), movement.LocalRotation.ToUnity()); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerJoinedMultiplayerSessionProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerJoinedMultiplayerSessionProcessor.cs index 682f0e8b90..d7321a705c 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerJoinedMultiplayerSessionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerJoinedMultiplayerSessionProcessor.cs @@ -1,26 +1,20 @@ using System.Collections; -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; using UWE; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerJoinedMultiplayerSessionProcessor : ClientPacketProcessor +internal sealed class PlayerJoinedMultiplayerSessionProcessor(PlayerManager playerManager, Entities entities) : IClientPacketProcessor { - private readonly PlayerManager playerManager; - private readonly Entities entities; - - public PlayerJoinedMultiplayerSessionProcessor(PlayerManager playerManager, Entities entities) - { - this.playerManager = playerManager; - this.entities = entities; - } + private readonly Entities entities = entities; + private readonly PlayerManager playerManager = playerManager; - public override void Process(PlayerJoinedMultiplayerSession packet) + public Task Process(ClientProcessorContext context, PlayerJoinedMultiplayerSession packet) { CoroutineHost.StartCoroutine(SpawnRemotePlayer(packet)); + return Task.CompletedTask; } private IEnumerator SpawnRemotePlayer(PlayerJoinedMultiplayerSession packet) diff --git a/NitroxClient/Communication/Packets/Processors/PlayerKickedProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerKickedProcessor.cs index 0982a029d5..74f4eb2fe6 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerKickedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerKickedProcessor.cs @@ -1,31 +1,25 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours.Gui.Modals; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class UserKickedProcessor(IMultiplayerSession session) : IClientPacketProcessor { - public class UserKickedProcessor : ClientPacketProcessor + private readonly IMultiplayerSession session = session; + + public Task Process(ClientProcessorContext context, PlayerKicked packet) { - private readonly IMultiplayerSession session; + string message = Language.main.Get("Nitrox_PlayerKicked"); - public UserKickedProcessor(IMultiplayerSession session) + if (!string.IsNullOrEmpty(packet.Reason)) { - this.session = session; + message += $"\n {packet.Reason}"; } - public override void Process(PlayerKicked packet) - { - string message = Language.main.Get("Nitrox_PlayerKicked"); - - if (!string.IsNullOrEmpty(packet.Reason)) - { - message += $"\n {packet.Reason}"; - } - - session.Disconnect(); - Modal.Get()?.Show(message); - } + session.Disconnect(); + Modal.Get().Show(message); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerMovementProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerMovementProcessor.cs index 34ac7eba1f..15785b3ac1 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerMovementProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerMovementProcessor.cs @@ -1,28 +1,22 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerMovementProcessor : ClientPacketProcessor +internal sealed class PlayerMovementProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - private readonly PlayerManager remotePlayerManager; - - public PlayerMovementProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public override void Process(PlayerMovement movement) + public Task Process(ClientProcessorContext context, PlayerMovement movement) { - if (remotePlayerManager.TryFind(movement.PlayerId, out RemotePlayer remotePlayer)) + if (remotePlayerManager.TryFind(movement.SessionId, out RemotePlayer remotePlayer)) { remotePlayer.UpdatePosition(movement.Position.ToUnity(), movement.Velocity.ToUnity(), movement.BodyRotation.ToUnity(), movement.AimingRotation.ToUnity()); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerStatsProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerStatsProcessor.cs index cc11da72f4..04380e421c 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerStatsProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerStatsProcessor.cs @@ -1,23 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours.Gui.HUD; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerStatsProcessor : ClientPacketProcessor +internal sealed class PlayerStatsProcessor(PlayerManager playerManager) : IClientPacketProcessor { - private readonly PlayerManager playerManager; - - public PlayerStatsProcessor(PlayerManager playerManager) - { - this.playerManager = playerManager; - } + private readonly PlayerManager playerManager = playerManager; - public override void Process(PlayerStats playerStats) + public Task Process(ClientProcessorContext context, PlayerStats playerStats) { - if (playerManager.TryFind(playerStats.PlayerId, out RemotePlayer remotePlayer)) + if (playerManager.TryFind(playerStats.SessionId, out RemotePlayer remotePlayer)) { RemotePlayerVitals vitals = remotePlayer.vitals; if (vitals) @@ -29,5 +23,6 @@ public override void Process(PlayerStats playerStats) } remotePlayer.UpdateHealthAndInfection(playerStats.Health, playerStats.InfectionAmount); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PlayerTeleportedProcessor.cs b/NitroxClient/Communication/Packets/Processors/PlayerTeleportedProcessor.cs index b2c5c72769..5e9e13224e 100644 --- a/NitroxClient/Communication/Packets/Processors/PlayerTeleportedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PlayerTeleportedProcessor.cs @@ -1,16 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; using UWE; using Terrain = NitroxClient.GameLogic.Terrain; namespace NitroxClient.Communication.Packets.Processors; -public class PlayerTeleportedProcessor : ClientPacketProcessor +internal sealed class PlayerTeleportedProcessor : IClientPacketProcessor { - public override void Process(PlayerTeleported packet) + public Task Process(ClientProcessorContext context, PlayerTeleported packet) { Player.main.OnPlayerPositionCheat(); @@ -19,21 +17,22 @@ public override void Process(PlayerTeleported packet) { currentVehicle.TeleportVehicle(packet.DestinationTo.ToUnity(), currentVehicle.transform.rotation); Player.main.WaitForTeleportation(); - return; + return Task.CompletedTask; } Player.main.SetPosition(packet.DestinationTo.ToUnity()); - + if (packet.SubRootID.HasValue && NitroxEntity.TryGetComponentFrom(packet.SubRootID.Value, out SubRoot subRoot)) { Player.main.SetCurrentSub(subRoot, true); - return; + return Task.CompletedTask; } - + // Freeze the player while it's loading its new position Player.main.cinematicModeActive = true; Player.main.WaitForTeleportation(); CoroutineHost.StartCoroutine(Terrain.SafeWaitForWorldLoad()); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs b/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs index a0f61cf1a1..5de2102bd0 100644 --- a/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/PvPAttackProcessor.cs @@ -1,16 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; namespace NitroxClient.Communication.Packets.Processors; -public class PvPAttackProcessor : ClientPacketProcessor +internal sealed class PvPAttackProcessor : IClientPacketProcessor { - public override void Process(PvPAttack packet) + public Task Process(ClientProcessorContext context, PvPAttack packet) { if (Player.main && Player.main.liveMixin) { Player.main.liveMixin.TakeDamage(packet.Damage); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/RadioPlayPendingMessageProcessor.cs b/NitroxClient/Communication/Packets/Processors/RadioPlayPendingMessageProcessor.cs index d414ef05b4..f941195b17 100644 --- a/NitroxClient/Communication/Packets/Processors/RadioPlayPendingMessageProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/RadioPlayPendingMessageProcessor.cs @@ -1,15 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using Story; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class RadioPlayPendingMessageProcessor : IClientPacketProcessor { - public class RadioPlayPendingMessageProcessor : ClientPacketProcessor + public Task Process(ClientProcessorContext context, RadioPlayPendingMessage packet) { - public override void Process(RadioPlayPendingMessage packet) - { - StoryGoalManager.main.ExecutePendingRadioMessage(); - } + StoryGoalManager.main.ExecutePendingRadioMessage(); + return Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/NitroxClient/Communication/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs b/NitroxClient/Communication/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs index 590f620330..8b776d40da 100644 --- a/NitroxClient/Communication/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/RangedAttackLastTargetUpdateProcessor.cs @@ -1,14 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class RangedAttackLastTargetUpdateProcessor : ClientPacketProcessor +internal sealed class RangedAttackLastTargetUpdateProcessor : IClientPacketProcessor { - public override void Process(RangedAttackLastTargetUpdate packet) + public Task Process(ClientProcessorContext context, RangedAttackLastTargetUpdate packet) { AI.RangedAttackLastTargetUpdate(packet.CreatureId, packet.TargetId, packet.AttackTypeIndex, packet.State); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/RemoveCreatureCorpseProcessor.cs b/NitroxClient/Communication/Packets/Processors/RemoveCreatureCorpseProcessor.cs index 89bb9d80bb..5e83d13dbc 100644 --- a/NitroxClient/Communication/Packets/Processors/RemoveCreatureCorpseProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/RemoveCreatureCorpseProcessor.cs @@ -1,51 +1,21 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; using UWE; namespace NitroxClient.Communication.Packets.Processors; -public class RemoveCreatureCorpseProcessor : ClientPacketProcessor +internal sealed class RemoveCreatureCorpseProcessor(Entities entities, LiveMixinManager liveMixinManager, SimulationOwnership simulationOwnership) : IClientPacketProcessor { - private readonly Entities entities; - private readonly LiveMixinManager liveMixinManager; - private readonly SimulationOwnership simulationOwnership; - - public RemoveCreatureCorpseProcessor(Entities entities, LiveMixinManager liveMixinManager, SimulationOwnership simulationOwnership) - { - this.entities = entities; - this.liveMixinManager = liveMixinManager; - this.simulationOwnership = simulationOwnership; - } - - public override void Process(RemoveCreatureCorpse packet) - { - entities.RemoveEntity(packet.CreatureId); - - if (entities.SpawningEntities) - { - entities.MarkForDeletion(packet.CreatureId); - } - - if (!NitroxEntity.TryGetComponentFrom(packet.CreatureId, out CreatureDeath creatureDeath)) - { - Log.Warn($"[{nameof(RemoveCreatureCorpseProcessor)}] Could not find entity with id: {packet.CreatureId} to remove corpse from."); - return; - } - - creatureDeath.transform.localPosition = packet.DeathPosition.ToUnity(); - creatureDeath.transform.localRotation = packet.DeathRotation.ToUnity(); - - SafeOnKillAsync(creatureDeath, packet.CreatureId, simulationOwnership, liveMixinManager); - } + private readonly Entities entities = entities; + private readonly LiveMixinManager liveMixinManager = liveMixinManager; + private readonly SimulationOwnership simulationOwnership = simulationOwnership; /// - /// Calls only some parts from to avoid sending packets from it - /// or already synced behaviour (like spawning another respawner from the remote clients) + /// Calls only some parts from to avoid sending packets from it + /// or already synced behaviour (like spawning another respawner from the remote clients) /// public static void SafeOnKillAsync(CreatureDeath creatureDeath, NitroxId creatureId, SimulationOwnership simulationOwnership, LiveMixinManager liveMixinManager) { @@ -76,4 +46,26 @@ public static void SafeOnKillAsync(CreatureDeath creatureDeath, NitroxId creatur CoroutineUtils.PumpCoroutine(creatureDeath.OnKillAsync()); } } + + public Task Process(ClientProcessorContext context, RemoveCreatureCorpse packet) + { + entities.RemoveEntity(packet.CreatureId); + + if (entities.SpawningEntities) + { + entities.MarkForDeletion(packet.CreatureId); + } + + if (!NitroxEntity.TryGetComponentFrom(packet.CreatureId, out CreatureDeath creatureDeath)) + { + Log.Warn($"[{nameof(RemoveCreatureCorpseProcessor)}] Could not find entity with id: {packet.CreatureId} to remove corpse from."); + return Task.CompletedTask; + } + + creatureDeath.transform.localPosition = packet.DeathPosition.ToUnity(); + creatureDeath.transform.localRotation = packet.DeathRotation.ToUnity(); + + SafeOnKillAsync(creatureDeath, packet.CreatureId, simulationOwnership, liveMixinManager); + return Task.CompletedTask; + } } diff --git a/NitroxClient/Communication/Packets/Processors/RocketLaunchProcessor.cs b/NitroxClient/Communication/Packets/Processors/RocketLaunchProcessor.cs index 12fe73a5f5..637ff158a5 100644 --- a/NitroxClient/Communication/Packets/Processors/RocketLaunchProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/RocketLaunchProcessor.cs @@ -1,20 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.Packets.Processors; -public class RocketLaunchProcessor : ClientPacketProcessor +internal sealed class RocketLaunchProcessor(Rockets rockets) : IClientPacketProcessor { - private readonly Rockets rockets; + private readonly Rockets rockets = rockets; - public RocketLaunchProcessor(Rockets rockets) + public Task Process(ClientProcessorContext context, RocketLaunch rocketLaunch) { - this.rockets = rockets; - } - - public override void Process(RocketLaunch rocketLaunch) - { - rockets.RocketLaunch(rocketLaunch.RocketId); + rockets.RocketLaunch(rocketLaunch.RocketId); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/ScheduleProcessor.cs b/NitroxClient/Communication/Packets/Processors/ScheduleProcessor.cs index 8e1d5fda33..7c98a886a8 100644 --- a/NitroxClient/Communication/Packets/Processors/ScheduleProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ScheduleProcessor.cs @@ -1,14 +1,13 @@ using System.Linq; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using Story; namespace NitroxClient.Communication.Packets.Processors; -public class ScheduleProcessor : ClientPacketProcessor +internal sealed class ScheduleProcessor : IClientPacketProcessor { - public override void Process(Schedule schedulePacket) + public Task Process(ClientProcessorContext context, Schedule schedulePacket) { ScheduledGoal goal = new() { @@ -21,6 +20,7 @@ public override void Process(Schedule schedulePacket) StoryGoalScheduler.main.schedule.Add(goal); } Log.Debug($"Processed a Schedule packet [Key: {goal.goalKey}, Type: {goal.goalType}, TimeExecute: {goal.timeExecute}]"); + return Task.CompletedTask; } private bool ShouldSchedule(ScheduledGoal goal) diff --git a/NitroxClient/Communication/Packets/Processors/SeaDragonAttackTargetProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeaDragonAttackTargetProcessor.cs index e630ec56ae..a4aeb21c74 100644 --- a/NitroxClient/Communication/Packets/Processors/SeaDragonAttackTargetProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeaDragonAttackTargetProcessor.cs @@ -1,20 +1,19 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic.PlayerLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class SeaDragonAttackTargetProcessor : ClientPacketProcessor +internal sealed class SeaDragonAttackTargetProcessor : IClientPacketProcessor { - public override void Process(SeaDragonAttackTarget packet) + public Task Process(ClientProcessorContext context, SeaDragonAttackTarget packet) { if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragonMeleeAttack seaDragonMeleeAttack) || !NitroxEntity.TryGetObjectFrom(packet.TargetId, out GameObject target)) { - return; + return Task.CompletedTask; } seaDragonMeleeAttack.seaDragon.Aggression.Value = packet.Aggression; @@ -24,10 +23,9 @@ public override void Process(SeaDragonAttackTarget packet) seaDragonMeleeAttack.animator.SetTrigger("shove"); seaDragonMeleeAttack.SendMessage("OnMeleeAttack", target, SendMessageOptions.DontRequireReceiver); seaDragonMeleeAttack.timeLastBite = Time.time; - return; + return Task.CompletedTask; } - // SeaDragonMeleeAttack.OnTouchFront's useful part about local player attack Collider collider; if (target.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier)) @@ -40,7 +38,7 @@ public override void Process(SeaDragonAttackTarget packet) } else { - return; + return Task.CompletedTask; } seaDragonMeleeAttack.timeLastBite = Time.time; @@ -48,9 +46,10 @@ public override void Process(SeaDragonAttackTarget packet) { using (PacketSuppressor.Suppress()) { - Utils.PlayEnvSound(seaDragonMeleeAttack.attackSound, collider.transform.position, 20f); + Utils.PlayEnvSound(seaDragonMeleeAttack.attackSound, collider.transform.position); } } seaDragonMeleeAttack.OnTouch(collider); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SeaDragonGrabExosuitProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeaDragonGrabExosuitProcessor.cs index 6c503a7a97..3de9ef2073 100644 --- a/NitroxClient/Communication/Packets/Processors/SeaDragonGrabExosuitProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeaDragonGrabExosuitProcessor.cs @@ -1,18 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; namespace NitroxClient.Communication.Packets.Processors; -public class SeaDragonGrabExosuitProcessor : ClientPacketProcessor +internal sealed class SeaDragonGrabExosuitProcessor : IClientPacketProcessor { - public override void Process(SeaDragonGrabExosuit packet) + public Task Process(ClientProcessorContext context, SeaDragonGrabExosuit packet) { if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragon seaDragon) || !NitroxEntity.TryGetComponentFrom(packet.TargetId, out Exosuit exosuit)) { - return; + return Task.CompletedTask; } using (PacketSuppressor.Suppress()) @@ -20,5 +19,6 @@ public override void Process(SeaDragonGrabExosuit packet) seaDragon.GrabExosuit(exosuit); seaDragon.CancelInvoke(nameof(SeaDragon.DamageExosuit)); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SeaDragonSwatAttackProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeaDragonSwatAttackProcessor.cs index 40dbf9ed1b..f7c27318f9 100644 --- a/NitroxClient/Communication/Packets/Processors/SeaDragonSwatAttackProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeaDragonSwatAttackProcessor.cs @@ -1,19 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class SeaDragonSwatAttackProcessor : ClientPacketProcessor +internal sealed class SeaDragonSwatAttackProcessor : IClientPacketProcessor { - public override void Process(SeaDragonSwatAttack packet) + public Task Process(ClientProcessorContext context, SeaDragonSwatAttack packet) { if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragonMeleeAttack seaDragonMeleeAttack) || !NitroxEntity.TryGetObjectFrom(packet.TargetId, out GameObject targetObject)) { - return; + return Task.CompletedTask; } using (PacketSuppressor.Suppress()) @@ -21,5 +20,6 @@ public override void Process(SeaDragonSwatAttack packet) seaDragonMeleeAttack.seaDragon.Aggression.Value = packet.Aggression; seaDragonMeleeAttack.SwatAttack(targetObject, packet.IsRightHand); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SeaTreaderChunkPickedUpProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeaTreaderChunkPickedUpProcessor.cs index ef5d51a079..886032f62e 100644 --- a/NitroxClient/Communication/Packets/Processors/SeaTreaderChunkPickedUpProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeaTreaderChunkPickedUpProcessor.cs @@ -1,18 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class SeaTreaderChunkPickedUpProcessor : ClientPacketProcessor +internal sealed class SeaTreaderChunkPickedUpProcessor : IClientPacketProcessor { - public override void Process(SeaTreaderChunkPickedUp packet) + public Task Process(ClientProcessorContext context, SeaTreaderChunkPickedUp packet) { if (NitroxEntity.TryGetComponentFrom(packet.ChunkId, out SinkingGroundChunk sinkingGroundChunk)) { - GameObject.Destroy(sinkingGroundChunk.gameObject); + Object.Destroy(sinkingGroundChunk.gameObject); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs index f0d258bd13..9275b79bc8 100644 --- a/NitroxClient/Communication/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeaTreaderSpawnedChunkProcessor.cs @@ -1,15 +1,13 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class SeaTreaderSpawnedChunkProcessor : ClientPacketProcessor +internal sealed class SeaTreaderSpawnedChunkProcessor : IClientPacketProcessor { - public override void Process(SeaTreaderSpawnedChunk packet) + public Task Process(ClientProcessorContext context, SeaTreaderSpawnedChunk packet) { if (NitroxEntity.TryGetComponentFrom(packet.CreatureId, out SeaTreader seaTreader) && seaTreader.TryGetComponentInChildren(out SeaTreaderSounds seaTreaderSounds)) @@ -18,5 +16,6 @@ public override void Process(SeaTreaderSpawnedChunk packet) chunkObject.transform.position = packet.Position.ToUnity(); chunkObject.transform.rotation = packet.Rotation.ToUnity(); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SeamothModuleActionProcessor.cs b/NitroxClient/Communication/Packets/Processors/SeamothModuleActionProcessor.cs index d9056f502d..64a8437732 100644 --- a/NitroxClient/Communication/Packets/Processors/SeamothModuleActionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SeamothModuleActionProcessor.cs @@ -1,22 +1,20 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public sealed class SeamothModuleActionProcessor : ClientPacketProcessor +internal sealed class SeamothModuleActionProcessor : IClientPacketProcessor { - public override void Process(SeamothModulesAction packet) + public Task Process(ClientProcessorContext context, SeamothModulesAction packet) { using (PacketSuppressor.Suppress()) { if (!NitroxEntity.TryGetComponentFrom(packet.Id, out SeaMoth seamoth)) { Log.Error($"[{nameof(SeamothModuleActionProcessor)}] Couldn't find SeaMoth component on {packet.Id}"); - return; + return Task.CompletedTask; } switch (packet.TechType.ToUnity()) @@ -27,7 +25,7 @@ public override void Process(SeamothModulesAction packet) float charge = chargeArray[packet.SlotID]; float slotCharge = seamoth.GetSlotCharge(packet.SlotID); - GameObject gameObject = Utils.SpawnZeroedAt(seamoth.seamothElectricalDefensePrefab, seamoth.transform, false); + GameObject gameObject = Utils.SpawnZeroedAt(seamoth.seamothElectricalDefensePrefab, seamoth.transform); ElectricalDefense component = gameObject.GetComponent(); component.charge = charge; component.chargeScalar = slotCharge; @@ -36,5 +34,6 @@ public override void Process(SeamothModulesAction packet) } } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/ServerStoppedProcessor.cs b/NitroxClient/Communication/Packets/Processors/ServerStoppedProcessor.cs index 077bd249bc..add337a1e2 100644 --- a/NitroxClient/Communication/Packets/Processors/ServerStoppedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ServerStoppedProcessor.cs @@ -1,24 +1,19 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.MonoBehaviours.Gui.Modals; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.Packets.Processors; -public class ServerStoppedProcessor : ClientPacketProcessor +internal sealed class ServerStoppedProcessor(IClient client) : IClientPacketProcessor { - private readonly IClient client; + private readonly IClient client = client; - public ServerStoppedProcessor(IClient client) - { - this.client = client; - } - - public override void Process(ServerStopped packet) + public Task Process(ClientProcessorContext context, ServerStopped packet) { // We can send the stop instruction right now instead of waiting for the timeout client.Stop(); Modal.Get()?.Show(); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SetIntroCinematicModeProcessor.cs b/NitroxClient/Communication/Packets/Processors/SetIntroCinematicModeProcessor.cs index b437c7168e..2afe16a5b1 100644 --- a/NitroxClient/Communication/Packets/Processors/SetIntroCinematicModeProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SetIntroCinematicModeProcessor.cs @@ -1,27 +1,19 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.GameLogic.PlayerLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.Packets.Processors; -public class SetIntroCinematicModeProcessor : ClientPacketProcessor +internal sealed class SetIntroCinematicModeProcessor(PlayerManager playerManager, PlayerCinematics playerCinematics, LocalPlayer localPlayer) : IClientPacketProcessor { - private readonly PlayerManager playerManager; - private readonly PlayerCinematics playerCinematics; - private readonly LocalPlayer localPlayer; + private readonly LocalPlayer localPlayer = localPlayer; + private readonly PlayerCinematics playerCinematics = playerCinematics; + private readonly PlayerManager playerManager = playerManager; - public SetIntroCinematicModeProcessor(PlayerManager playerManager, PlayerCinematics playerCinematics, LocalPlayer localPlayer) + public Task Process(ClientProcessorContext context, SetIntroCinematicMode packet) { - this.playerManager = playerManager; - this.playerCinematics = playerCinematics; - this.localPlayer = localPlayer; - } - - public override void Process(SetIntroCinematicMode packet) - { - if (localPlayer.PlayerId == packet.PlayerId) + if (localPlayer.SessionId == packet.SessionId) { if (packet.PartnerId.HasValue) { @@ -29,15 +21,16 @@ public override void Process(SetIntroCinematicMode packet) } localPlayer.IntroCinematicMode = packet.Mode; - return; + return Task.CompletedTask; } - if (playerManager.TryFind(packet.PlayerId, out RemotePlayer remotePlayer)) + if (playerManager.TryFind(packet.SessionId, out RemotePlayer remotePlayer)) { remotePlayer.PlayerContext.IntroCinematicMode = packet.Mode; - return; + return Task.CompletedTask; } - Log.Debug($"SetIntroCinematicMode couldn't find Player with id {packet.PlayerId}. This is normal if player has not yet officially joined."); + Log.Debug($"SetIntroCinematicMode couldn't find Player with id {packet.SessionId}. This is normal if player has not yet officially joined."); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SimulationOwnershipChangeProcessor.cs b/NitroxClient/Communication/Packets/Processors/SimulationOwnershipChangeProcessor.cs index 7ae6d0ead1..69bff07e13 100644 --- a/NitroxClient/Communication/Packets/Processors/SimulationOwnershipChangeProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SimulationOwnershipChangeProcessor.cs @@ -1,26 +1,20 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class SimulationOwnershipChangeProcessor : ClientPacketProcessor +internal sealed class SimulationOwnershipChangeProcessor(SimulationOwnership simulationOwnershipManager) : IClientPacketProcessor { - private readonly SimulationOwnership simulationOwnershipManager; - - public SimulationOwnershipChangeProcessor(SimulationOwnership simulationOwnershipManager) - { - this.simulationOwnershipManager = simulationOwnershipManager; - } + private readonly SimulationOwnership simulationOwnershipManager = simulationOwnershipManager; - public override void Process(SimulationOwnershipChange simulationOwnershipChange) + public Task Process(ClientProcessorContext context, SimulationOwnershipChange simulationOwnershipChange) { foreach (SimulatedEntity simulatedEntity in simulationOwnershipChange.Entities) { simulationOwnershipManager.TreatSimulatedEntity(simulatedEntity); } + return Task.CompletedTask; } } - diff --git a/NitroxClient/Communication/Packets/Processors/SimulationOwnershipResponseProcessor.cs b/NitroxClient/Communication/Packets/Processors/SimulationOwnershipResponseProcessor.cs index 57fd6675b2..e800c59d69 100644 --- a/NitroxClient/Communication/Packets/Processors/SimulationOwnershipResponseProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SimulationOwnershipResponseProcessor.cs @@ -1,53 +1,46 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.DataStructures; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class SimulationOwnershipResponseProcessor(IMultiplayerSession multiplayerSession, SimulationOwnership simulationOwnershipManager) : IClientPacketProcessor { - public class SimulationOwnershipResponseProcessor : ClientPacketProcessor + private readonly IMultiplayerSession multiplayerSession = multiplayerSession; + private readonly SimulationOwnership simulationOwnershipManager = simulationOwnershipManager; + + public Task Process(ClientProcessorContext context, SimulationOwnershipResponse response) { - private readonly IMultiplayerSession multiplayerSession; - private readonly SimulationOwnership simulationOwnershipManager; + /* + * For now, we expect the simulation lock callback to setup entity broadcasting as + * most items that are requesting an exclusive lock have custom broadcast code, ex: + * vehicles like the cyclops. However, we may one day want to add a watcher here + * to ensure broadcast one day, ex: + * + * EntityPositionBroadcaster.WatchEntity(simulatedEntity.Id, gameObject.Value); + * + */ + simulationOwnershipManager.ReceivedSimulationLockResponse(response.Id, response.LockAcquired, response.LockType); - public SimulationOwnershipResponseProcessor(IMultiplayerSession multiplayerSession, SimulationOwnership simulationOwnershipManager) + if (response.LockAcquired) { - this.multiplayerSession = multiplayerSession; - this.simulationOwnershipManager = simulationOwnershipManager; + RemoveRemoteController(response.Id); } + return Task.CompletedTask; + } - public override void Process(SimulationOwnershipResponse response) - { - /* - * For now, we expect the simulation lock callback to setup entity broadcasting as - * most items that are requesting an exclusive lock have custom broadcast code, ex: - * vehicles like the cyclops. However, we may one day want to add a watcher here - * to ensure broadcast one day, ex: - * - * EntityPositionBroadcaster.WatchEntity(simulatedEntity.Id, gameObject.Value); - * - */ - simulationOwnershipManager.ReceivedSimulationLockResponse(response.Id, response.LockAcquired, response.LockType); - - if (response.LockAcquired) - { - RemoveRemoteController(response.Id); - } - } + private void RemoveRemoteController(NitroxId id) + { + Optional gameObject = NitroxEntity.GetObjectFrom(id); - private void RemoveRemoteController(NitroxId id) + if (gameObject.HasValue) { - Optional gameObject = NitroxEntity.GetObjectFrom(id); - - if (gameObject.HasValue) - { - RemotelyControlled remotelyControlled = gameObject.Value.GetComponent(); - Object.Destroy(remotelyControlled); - } + RemotelyControlled remotelyControlled = gameObject.Value.GetComponent(); + Object.Destroy(remotelyControlled); } } } diff --git a/NitroxClient/Communication/Packets/Processors/SleepCompleteProcessor.cs b/NitroxClient/Communication/Packets/Processors/SleepCompleteProcessor.cs index 5124d7feb7..4d1a956e21 100644 --- a/NitroxClient/Communication/Packets/Processors/SleepCompleteProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SleepCompleteProcessor.cs @@ -1,20 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class SleepCompleteProcessor : ClientPacketProcessor +internal sealed class SleepCompleteProcessor(SleepManager sleepManager) : IClientPacketProcessor { - private readonly SleepManager sleepManager; - - public SleepCompleteProcessor(SleepManager sleepManager) - { - this.sleepManager = sleepManager; - } + private readonly SleepManager sleepManager = sleepManager; - public override void Process(SleepComplete packet) + public Task Process(ClientProcessorContext context, SleepComplete packet) { sleepManager.OnSleepComplete(); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SleepStatusUpdateProcessor.cs b/NitroxClient/Communication/Packets/Processors/SleepStatusUpdateProcessor.cs index 6487895f7a..ea41cfe316 100644 --- a/NitroxClient/Communication/Packets/Processors/SleepStatusUpdateProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SleepStatusUpdateProcessor.cs @@ -1,19 +1,14 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class SleepStatusUpdateProcessor : ClientPacketProcessor +internal sealed class SleepStatusUpdateProcessor(SleepManager sleepManager) : IClientPacketProcessor { - private readonly SleepManager sleepManager; - - public SleepStatusUpdateProcessor(SleepManager sleepManager) - { - this.sleepManager = sleepManager; - } + private readonly SleepManager sleepManager = sleepManager; - public override void Process(SleepStatusUpdate packet) + public Task Process(ClientProcessorContext context, SleepStatusUpdate packet) { if (packet.PlayersInBed > 0) { @@ -24,5 +19,6 @@ public override void Process(SleepStatusUpdate packet) { sleepManager.OnAllPlayersSleeping(); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SpawnEntitiesProcessor.cs b/NitroxClient/Communication/Packets/Processors/SpawnEntitiesProcessor.cs index 1dbdfa6143..9c99e3e091 100644 --- a/NitroxClient/Communication/Packets/Processors/SpawnEntitiesProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SpawnEntitiesProcessor.cs @@ -1,27 +1,18 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class SpawnEntitiesProcessor : ClientPacketProcessor +internal sealed class SpawnEntitiesProcessor(Entities entities, SimulationOwnership simulationOwnership, Terrain terrain) : IClientPacketProcessor { - private readonly Entities entities; - private readonly SimulationOwnership simulationOwnership; - private readonly Terrain terrain; - - public SpawnEntitiesProcessor(Entities entities, SimulationOwnership simulationOwnership, Terrain terrain) - { - this.entities = entities; - this.simulationOwnership = simulationOwnership; - this.terrain = terrain; - } + private readonly Entities entities = entities; + private readonly SimulationOwnership simulationOwnership = simulationOwnership; + private readonly Terrain terrain = terrain; - public override void Process(SpawnEntities packet) + public Task Process(ClientProcessorContext context, SpawnEntities packet) { if (packet.ForceRespawn) { @@ -30,18 +21,15 @@ public override void Process(SpawnEntities packet) if (packet.Entities.Count > 0) { - if (packet.Simulations != null) + foreach (SimulatedEntity simulatedEntity in packet.Simulations) { - foreach (SimulatedEntity simulatedEntity in packet.Simulations) - { - simulationOwnership.RegisterNewerSimulation(simulatedEntity.Id, simulatedEntity); - } + simulationOwnership.RegisterNewerSimulation(simulatedEntity.Id, simulatedEntity); } // Packet processing is done in the main thread so there's no issue calling this // We need a cold start so that all cleaned up entities (if force respawn is true) have time to be fully destroyed entities.EnqueueEntitiesToSpawn(packet.Entities, packet.SpawnedCells, packet.ForceRespawn); - return; + return Task.CompletedTask; } // Even if there was nothing to be spawned in the cell, we need to know about it as fully spawned @@ -49,5 +37,6 @@ public override void Process(SpawnEntities packet) { terrain.AddFullySpawnedCell(spawnedEntityCell); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/StasisSphereHitProcessor.cs b/NitroxClient/Communication/Packets/Processors/StasisSphereHitProcessor.cs index a5ad170b24..310def336b 100644 --- a/NitroxClient/Communication/Packets/Processors/StasisSphereHitProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/StasisSphereHitProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class StasisSphereHitProcessor : ClientPacketProcessor +internal sealed class StasisSphereHitProcessor(BulletManager bulletManager) : IClientPacketProcessor { - private readonly BulletManager bulletManager; - - public StasisSphereHitProcessor(BulletManager bulletManager) - { - this.bulletManager = bulletManager; - } + private readonly BulletManager bulletManager = bulletManager; - public override void Process(StasisSphereHit packet) + public Task Process(ClientProcessorContext context, StasisSphereHit packet) { - bulletManager.StasisSphereHit(packet.PlayerId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.ChargeNormalized, packet.Consumption); + bulletManager.StasisSphereHit(packet.SessionId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.ChargeNormalized, packet.Consumption); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/StasisSphereShotProcessor.cs b/NitroxClient/Communication/Packets/Processors/StasisSphereShotProcessor.cs index 8c3d9bf491..31ca7c1d05 100644 --- a/NitroxClient/Communication/Packets/Processors/StasisSphereShotProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/StasisSphereShotProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class StasisSphereShotProcessor : ClientPacketProcessor +internal sealed class StasisSphereShotProcessor(BulletManager bulletManager) : IClientPacketProcessor { - private readonly BulletManager bulletManager; - - public StasisSphereShotProcessor(BulletManager bulletManager) - { - this.bulletManager = bulletManager; - } + private readonly BulletManager bulletManager = bulletManager; - public override void Process(StasisSphereShot packet) + public Task Process(ClientProcessorContext context, StasisSphereShot packet) { - bulletManager.ShootStasisSphere(packet.PlayerId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.Speed, packet.LifeTime, packet.ChargeNormalized); + bulletManager.ShootStasisSphere(packet.SessionId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.Speed, packet.LifeTime, packet.ChargeNormalized); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/StoryGoalExecutedClientProcessor.cs b/NitroxClient/Communication/Packets/Processors/StoryGoalExecutedClientProcessor.cs index b5d3393bb0..6202f2575d 100644 --- a/NitroxClient/Communication/Packets/Processors/StoryGoalExecutedClientProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/StoryGoalExecutedClientProcessor.cs @@ -1,22 +1,15 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using Story; namespace NitroxClient.Communication.Packets.Processors; -public class StoryGoalExecutedClientProcessor : ClientPacketProcessor +internal sealed class StoryGoalExecutedClientProcessor(IPacketSender packetSender) : IClientPacketProcessor { - private readonly IPacketSender packetSender; - - public StoryGoalExecutedClientProcessor(IPacketSender packetSender) - { - this.packetSender = packetSender; - } + private readonly IPacketSender packetSender = packetSender; - public override void Process(StoryGoalExecuted packet) + public Task Process(ClientProcessorContext context, StoryGoalExecuted packet) { StoryGoalScheduler.main.schedule.RemoveAllFast(packet.Key, static (goal, packetGoalKey) => goal.goalKey == packetGoalKey); @@ -27,5 +20,6 @@ public override void Process(StoryGoalExecuted packet) { StoryGoal.Execute(packet.Key, packet.Type.ToUnity()); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/SubRootChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/SubRootChangedProcessor.cs index be1b34b695..b0f0addbb4 100644 --- a/NitroxClient/Communication/Packets/Processors/SubRootChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/SubRootChangedProcessor.cs @@ -1,38 +1,32 @@ using Nitrox.Model.DataStructures; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class SubRootChangedProcessor(PlayerManager remotePlayerManager) : IClientPacketProcessor { - public class SubRootChangedProcessor : ClientPacketProcessor - { - private readonly PlayerManager remotePlayerManager; + private readonly PlayerManager remotePlayerManager = remotePlayerManager; - public SubRootChangedProcessor(PlayerManager remotePlayerManager) - { - this.remotePlayerManager = remotePlayerManager; - } + public Task Process(ClientProcessorContext context, SubRootChanged packet) + { + Optional remotePlayer = remotePlayerManager.Find(packet.SessionId); - public override void Process(SubRootChanged packet) + if (remotePlayer.HasValue) { - Optional remotePlayer = remotePlayerManager.Find(packet.PlayerId); + SubRoot subRoot = null; - if (remotePlayer.HasValue) + if (packet.SubRootId.HasValue) { - SubRoot subRoot = null; - - if (packet.SubRootId.HasValue) - { - GameObject sub = NitroxEntity.RequireObjectFrom(packet.SubRootId.Value); - subRoot = sub.GetComponent(); - } - - remotePlayer.Value.SetSubRoot(subRoot); + GameObject sub = NitroxEntity.RequireObjectFrom(packet.SubRootId.Value); + subRoot = sub.GetComponent(); } + + remotePlayer.Value.SetSubRoot(subRoot); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/TextAutoCompleteProcessor.cs b/NitroxClient/Communication/Packets/Processors/TextAutoCompleteProcessor.cs new file mode 100644 index 0000000000..2cc5188e55 --- /dev/null +++ b/NitroxClient/Communication/Packets/Processors/TextAutoCompleteProcessor.cs @@ -0,0 +1,25 @@ +using Nitrox.Model.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic.ChatUI; + +namespace NitroxClient.Communication.Packets.Processors; + +internal sealed class TextAutoCompleteProcessor : IClientPacketProcessor +{ + private readonly PlayerChatManager playerChatManager = PlayerChatManager.Instance; + + public Task Process(ClientProcessorContext context, TextAutoComplete packet) + { + if (string.IsNullOrWhiteSpace(packet.Text)) + { + return Task.CompletedTask; + } + switch (packet.Context) + { + case TextAutoComplete.AutoCompleteContext.COMMAND_NAME: + playerChatManager.SetAutoCompleteText($"/{packet.Text} "); + break; + } + return Task.CompletedTask; + } +} diff --git a/NitroxClient/Communication/Packets/Processors/TimeChangeProcessor.cs b/NitroxClient/Communication/Packets/Processors/TimeChangeProcessor.cs index 3c5cf723bb..d3c4a58ff1 100644 --- a/NitroxClient/Communication/Packets/Processors/TimeChangeProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/TimeChangeProcessor.cs @@ -1,21 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class TimeChangeProcessor : ClientPacketProcessor +internal sealed class TimeChangeProcessor(TimeManager timeManager) : IClientPacketProcessor { - private readonly TimeManager timeManager; - - public TimeChangeProcessor(TimeManager timeManager) - { - this.timeManager = timeManager; - } + private readonly TimeManager timeManager = timeManager; - public override void Process(TimeChange timeChangePacket) + public Task Process(ClientProcessorContext context, TimeChange timeChangePacket) { timeManager.ProcessUpdate(timeChangePacket); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/ToggleLightsProcessor.cs b/NitroxClient/Communication/Packets/Processors/ToggleLightsProcessor.cs index 44fc4c3d70..620a1246ad 100644 --- a/NitroxClient/Communication/Packets/Processors/ToggleLightsProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/ToggleLightsProcessor.cs @@ -1,23 +1,20 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic.FMOD; using NitroxClient.MonoBehaviours; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class ToggleLightsProcessor : ClientPacketProcessor +internal sealed class ToggleLightsProcessor : IClientPacketProcessor { - public ToggleLightsProcessor() - { } - - public override void Process(Nitrox.Model.Subnautica.Packets.ToggleLights packet) + public Task Process(ClientProcessorContext context, Nitrox.Model.Subnautica.Packets.ToggleLights packet) { GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); ToggleLights toggleLights = gameObject.RequireComponent(); if (packet.IsOn == toggleLights.GetLightsActive()) { - return; + return Task.CompletedTask; } using (PacketSuppressor.Suppress()) @@ -25,5 +22,6 @@ public override void Process(Nitrox.Model.Subnautica.Packets.ToggleLights packet { toggleLights.SetLightsActive(packet.IsOn); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/TorpedoHitProcessor.cs b/NitroxClient/Communication/Packets/Processors/TorpedoHitProcessor.cs index c172bfd5f6..584371813d 100644 --- a/NitroxClient/Communication/Packets/Processors/TorpedoHitProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/TorpedoHitProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class TorpedoHitProcessor : ClientPacketProcessor +internal sealed class TorpedoHitProcessor(BulletManager bulletManager) : IClientPacketProcessor { - private readonly BulletManager bulletManager; - - public TorpedoHitProcessor(BulletManager bulletManager) - { - this.bulletManager = bulletManager; - } + private readonly BulletManager bulletManager = bulletManager; - public override void Process(TorpedoHit packet) + public Task Process(ClientProcessorContext context, TorpedoHit packet) { bulletManager.TorpedoHit(packet.BulletId, packet.Position.ToUnity(), packet.Rotation.ToUnity()); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/TorpedoShotProcessor.cs b/NitroxClient/Communication/Packets/Processors/TorpedoShotProcessor.cs index bc169f080b..b3da2cd3d3 100644 --- a/NitroxClient/Communication/Packets/Processors/TorpedoShotProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/TorpedoShotProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class TorpedoShotProcessor : ClientPacketProcessor +internal sealed class TorpedoShotProcessor(BulletManager bulletManager) : IClientPacketProcessor { - private readonly BulletManager bulletManager; - - public TorpedoShotProcessor(BulletManager bulletManager) - { - this.bulletManager = bulletManager; - } + private readonly BulletManager bulletManager = bulletManager; - public override void Process(TorpedoShot packet) + public Task Process(ClientProcessorContext context, TorpedoShot packet) { bulletManager.ShootSeamothTorpedo(packet.BulletId, packet.TechType.ToUnity(), packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.Speed, packet.LifeTime); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/TorpedoTargetAcquiredProcessor.cs b/NitroxClient/Communication/Packets/Processors/TorpedoTargetAcquiredProcessor.cs index f77241d53a..c0729fe15f 100644 --- a/NitroxClient/Communication/Packets/Processors/TorpedoTargetAcquiredProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/TorpedoTargetAcquiredProcessor.cs @@ -1,22 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.GameLogic; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.GameLogic; namespace NitroxClient.Communication.Packets.Processors; -public class TorpedoTargetAcquiredProcessor : ClientPacketProcessor +internal sealed class TorpedoTargetAcquiredProcessor(BulletManager bulletManager) : IClientPacketProcessor { - private readonly BulletManager bulletManager; - - public TorpedoTargetAcquiredProcessor(BulletManager bulletManager) - { - this.bulletManager = bulletManager; - } + private readonly BulletManager bulletManager = bulletManager; - public override void Process(TorpedoTargetAcquired packet) + public Task Process(ClientProcessorContext context, TorpedoTargetAcquired packet) { bulletManager.TorpedoTargetAcquired(packet.BulletId, packet.TargetId, packet.Position.ToUnity(), packet.Rotation.ToUnity()); + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/VehicleDockingProcessor.cs b/NitroxClient/Communication/Packets/Processors/VehicleDockingProcessor.cs index 97b4575d54..e7df172b11 100644 --- a/NitroxClient/Communication/Packets/Processors/VehicleDockingProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/VehicleDockingProcessor.cs @@ -1,37 +1,32 @@ using System.Collections; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Core; +using Nitrox.Model.DataStructures; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using NitroxClient.MonoBehaviours.Vehicles; using NitroxClient.Unity.Helper; -using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class VehicleDockingProcessor : ClientPacketProcessor +internal sealed class VehicleDockingProcessor(Vehicles vehicles) : IClientPacketProcessor { - private readonly Vehicles vehicles; + private readonly Vehicles vehicles = vehicles; - public VehicleDockingProcessor(Vehicles vehicles) - { - this.vehicles = vehicles; - } - - public override void Process(VehicleDocking packet) + public Task Process(ClientProcessorContext context, VehicleDocking packet) { if (!NitroxEntity.TryGetComponentFrom(packet.VehicleId, out Vehicle vehicle)) { Log.Error($"[{nameof(VehicleDockingProcessor)}] could not find Vehicle component on {packet.VehicleId}"); - return; + return Task.CompletedTask; } if (!NitroxEntity.TryGetComponentFrom(packet.DockId, out VehicleDockingBay dockingBay)) { Log.Error($"[{nameof(VehicleDockingProcessor)}] could not find VehicleDockingBay component on {packet.DockId}"); - return; + return Task.CompletedTask; } if (vehicle.TryGetComponent(out VehicleMovementReplicator vehicleMovementReplicator)) @@ -40,10 +35,11 @@ public override void Process(VehicleDocking packet) Log.Debug($"[{nameof(VehicleDockingProcessor)}] Disabled VehicleMovementReplicator on {packet.VehicleId}"); } - vehicle.StartCoroutine(DelayAnimationAndDisablePiloting(vehicle, vehicleMovementReplicator, dockingBay, packet.VehicleId, packet.PlayerId)); + vehicle.StartCoroutine(DelayAnimationAndDisablePiloting(vehicle, vehicleMovementReplicator, dockingBay, packet.VehicleId, packet.SessionId)); + return Task.CompletedTask; } - private IEnumerator DelayAnimationAndDisablePiloting(Vehicle vehicle, VehicleMovementReplicator vehicleMovementReplicator, VehicleDockingBay vehicleDockingBay, NitroxId vehicleId, ushort playerId) + private IEnumerator DelayAnimationAndDisablePiloting(Vehicle vehicle, VehicleMovementReplicator vehicleMovementReplicator, VehicleDockingBay vehicleDockingBay, NitroxId vehicleId, SessionId sessionId) { // Consider the vehicle movement latency (we don't teleport the vehicle to the docking position) if (vehicleMovementReplicator) @@ -56,17 +52,19 @@ private IEnumerator DelayAnimationAndDisablePiloting(Vehicle vehicle, VehicleMov { yield return Yielders.WaitFor1Second; } - + // DockVehicle sets the rigid body kinematic of the vehicle to true, we don't want that behaviour // Therefore disable kinematic (again) to remove the bouncing behavior DockRemoteVehicle(vehicleDockingBay, vehicle); vehicle.useRigidbody.isKinematic = false; yield return Yielders.WaitFor2Seconds; - vehicles.SetOnPilotMode(vehicleId, playerId, false); + vehicles.SetOnPilotMode(vehicleId, sessionId, false); } - /// Copy of without the player centric bits + /// Copy of + /// + /// without the player centric bits private void DockRemoteVehicle(VehicleDockingBay bay, Vehicle vehicle) { bay.dockedVehicle = vehicle; @@ -76,7 +74,7 @@ private void DockRemoteVehicle(VehicleDockingBay bay, Vehicle vehicle) bay.vehicle_docked_param = true; SkyEnvironmentChanged.Broadcast(vehicle.gameObject, bay.subRoot); bay.GetSubRoot().BroadcastMessage("UnlockDoors", SendMessageOptions.DontRequireReceiver); - + // We are only actually adding the health if we have a lock on the vehicle so we're fine to keep this routine going on. // If vehicle ownership changes then it'll still be fine because the verification will still be on the vehicle ownership. bay.CancelInvoke(nameof(VehicleDockingBay.RepairVehicle)); diff --git a/NitroxClient/Communication/Packets/Processors/VehicleMovementsProcessor.cs b/NitroxClient/Communication/Packets/Processors/VehicleMovementsProcessor.cs index 4f11c70ca0..d91d21a4e3 100644 --- a/NitroxClient/Communication/Packets/Processors/VehicleMovementsProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/VehicleMovementsProcessor.cs @@ -1,17 +1,16 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; -using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; +using NitroxClient.MonoBehaviours; namespace NitroxClient.Communication.Packets.Processors; -public class VehicleMovementsProcessor : ClientPacketProcessor +internal sealed class VehicleMovementsProcessor : IClientPacketProcessor { - public override void Process(VehicleMovements packet) + public Task Process(ClientProcessorContext context, VehicleMovements packet) { if (!MovementBroadcaster.Instance) { - return; + return Task.CompletedTask; } foreach (MovementData movementData in packet.Data) @@ -21,5 +20,6 @@ public override void Process(VehicleMovements packet) movementReplicator.AddSnapshot(movementData, (float)packet.RealTime); } } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs b/NitroxClient/Communication/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs index fec72ab448..ce58c1569a 100644 --- a/NitroxClient/Communication/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/VehicleOnPilotModeChangedProcessor.cs @@ -1,24 +1,17 @@ -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class VehicleOnPilotModeChangedProcessor : ClientPacketProcessor +internal sealed class VehicleOnPilotModeChangedProcessor(Vehicles vehicles, PlayerManager playerManager) : IClientPacketProcessor { - private readonly Vehicles vehicles; - private readonly PlayerManager playerManager; - - public VehicleOnPilotModeChangedProcessor(Vehicles vehicles, PlayerManager playerManager) - { - this.vehicles = vehicles; - this.playerManager = playerManager; - } + private readonly PlayerManager playerManager = playerManager; + private readonly Vehicles vehicles = vehicles; - public override void Process(VehicleOnPilotModeChanged packet) + public Task Process(ClientProcessorContext context, VehicleOnPilotModeChanged packet) { if (NitroxEntity.TryGetObjectFrom(packet.VehicleId, out GameObject gameObject)) { @@ -28,10 +21,11 @@ public override void Process(VehicleOnPilotModeChanged packet) // before the animation completes on the remote player.) if (gameObject.TryGetComponent(out Vehicle vehicle) && vehicle.docked) { - return; + return Task.CompletedTask; } - vehicles.SetOnPilotMode(gameObject, packet.PlayerId, packet.IsPiloting); + vehicles.SetOnPilotMode(gameObject, packet.SessionId, packet.IsPiloting); } + return Task.CompletedTask; } } diff --git a/NitroxClient/Communication/Packets/Processors/VehicleUndockingProcessor.cs b/NitroxClient/Communication/Packets/Processors/VehicleUndockingProcessor.cs index f9cd8e4a1f..aabf49f3f1 100644 --- a/NitroxClient/Communication/Packets/Processors/VehicleUndockingProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/VehicleUndockingProcessor.cs @@ -1,26 +1,19 @@ using System.Collections; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; namespace NitroxClient.Communication.Packets.Processors; -public class VehicleUndockingProcessor : ClientPacketProcessor +internal sealed class VehicleUndockingProcessor(Vehicles vehicles, PlayerManager remotePlayerManager) : IClientPacketProcessor { - private readonly Vehicles vehicles; - private readonly PlayerManager remotePlayerManager; - - public VehicleUndockingProcessor(Vehicles vehicles, PlayerManager remotePlayerManager) - { - this.vehicles = vehicles; - this.remotePlayerManager = remotePlayerManager; - } + private readonly PlayerManager remotePlayerManager = remotePlayerManager; + private readonly Vehicles vehicles = vehicles; - public override void Process(VehicleUndocking packet) + public Task Process(ClientProcessorContext context, VehicleUndocking packet) { GameObject vehicleGo = NitroxEntity.RequireObjectFrom(packet.VehicleId); GameObject vehicleDockingBayGo = NitroxEntity.RequireObjectFrom(packet.DockId); @@ -39,6 +32,13 @@ public override void Process(VehicleUndocking packet) FinishVehicleUndocking(packet, vehicle, vehicleDockingBay); } } + return Task.CompletedTask; + } + + private static IEnumerator StartUndockingAnimation(VehicleDockingBay vehicleDockingBay) + { + yield return Yielders.WaitFor2Seconds; + vehicleDockingBay.vehicle_docked_param = false; } private void StartVehicleUndocking(VehicleUndocking packet, GameObject vehicleGo, Vehicle vehicle, VehicleDockingBay vehicleDockingBay) @@ -46,11 +46,11 @@ private void StartVehicleUndocking(VehicleUndocking packet, GameObject vehicleGo vehicleDockingBay.subRoot.BroadcastMessage("OnLaunchBayOpening", SendMessageOptions.DontRequireReceiver); SkyEnvironmentChanged.Broadcast(vehicleGo, (GameObject)null); - if (remotePlayerManager.TryFind(packet.PlayerId, out RemotePlayer player)) + if (remotePlayerManager.TryFind(packet.SessionId, out RemotePlayer player)) { // It can happen that the player turns in circles around himself in the vehicle. This stops it. player.RigidBody.angularVelocity = Vector3.zero; - vehicles.SetOnPilotMode(packet.VehicleId, packet.PlayerId, true); + vehicles.SetOnPilotMode(packet.VehicleId, packet.SessionId, true); } vehicleDockingBay.StartCoroutine(StartUndockingAnimation(vehicleDockingBay)); @@ -61,12 +61,6 @@ private void StartVehicleUndocking(VehicleUndocking packet, GameObject vehicleGo } } - private static IEnumerator StartUndockingAnimation(VehicleDockingBay vehicleDockingBay) - { - yield return Yielders.WaitFor2Seconds; - vehicleDockingBay.vehicle_docked_param = false; - } - private void FinishVehicleUndocking(VehicleUndocking packet, Vehicle vehicle, VehicleDockingBay vehicleDockingBay) { if (vehicleDockingBay.GetSubRoot().isCyclops) @@ -76,7 +70,7 @@ private void FinishVehicleUndocking(VehicleUndocking packet, Vehicle vehicle, Ve vehicleDockingBay.dockedVehicle = null; vehicleDockingBay.CancelInvoke(nameof(VehicleDockingBay.RepairVehicle)); vehicle.docked = false; - if (remotePlayerManager.TryFind(packet.PlayerId, out RemotePlayer player)) + if (remotePlayerManager.TryFind(packet.SessionId, out RemotePlayer player)) { // Sometimes the player is not set accordingly which stretches the player's model instead of putting them in place // after undocking. This fixes it (the player rigid body seems to not be set right sometimes) @@ -84,7 +78,7 @@ private void FinishVehicleUndocking(VehicleUndocking packet, Vehicle vehicle, Ve player.SetVehicle(null); player.SetVehicle(vehicle); } - vehicles.SetOnPilotMode(packet.VehicleId, packet.PlayerId, true); + vehicles.SetOnPilotMode(packet.VehicleId, packet.SessionId, true); if (vehicle.TryGetComponent(out MovementReplicator vehicleMovementReplicator)) { diff --git a/NitroxClient/Communication/Packets/Processors/WeldActionProcessor.cs b/NitroxClient/Communication/Packets/Processors/WeldActionProcessor.cs index 873c6b252f..5e54262ba6 100644 --- a/NitroxClient/Communication/Packets/Processors/WeldActionProcessor.cs +++ b/NitroxClient/Communication/Packets/Processors/WeldActionProcessor.cs @@ -1,42 +1,34 @@ -using NitroxClient.Communication.Abstract; -using NitroxClient.Communication.Packets.Processors.Abstract; +using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Communication.Abstract; +using NitroxClient.Communication.Packets.Processors.Core; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.Packets; using UnityEngine; -namespace NitroxClient.Communication.Packets.Processors +namespace NitroxClient.Communication.Packets.Processors; + +internal class WeldActionProcessor(SimulationOwnership simulationOwnership) : IClientPacketProcessor { - class WeldActionProcessor : ClientPacketProcessor + private readonly SimulationOwnership simulationOwnership = simulationOwnership; + + public Task Process(ClientProcessorContext context, WeldAction packet) { - private IMultiplayerSession multiplayerSession; - private SimulationOwnership simulationOwnership; + GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); - public WeldActionProcessor(IMultiplayerSession multiplayerSession, SimulationOwnership simulationOwnership) + if (!simulationOwnership.HasAnyLockType(packet.Id)) { - this.multiplayerSession = multiplayerSession; - this.simulationOwnership = simulationOwnership; + Log.Error($"Got WeldAction packet for {packet.Id} but did not find the lock corresponding to it"); + return Task.CompletedTask; } - public override void Process(WeldAction packet) + LiveMixin liveMixin = gameObject.GetComponent(); + if (!liveMixin) { - GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id); - - if (!simulationOwnership.HasAnyLockType(packet.Id)) - { - Log.Error($"Got WeldAction packet for {packet.Id} but did not find the lock corresponding to it"); - return; - } - - LiveMixin liveMixin = gameObject.GetComponent(); - if (!liveMixin) - { - Log.Error($"Did not find LiveMixin for GameObject {packet.Id} even though it was welded."); - return; - } - // If we add other player sounds/animations, this is the place to do it for welding - liveMixin.AddHealth(packet.HealthAdded); + Log.Error($"Did not find LiveMixin for GameObject {packet.Id} even though it was welded."); + return Task.CompletedTask; } + // If we add other player sounds/animations, this is the place to do it for welding + liveMixin.AddHealth(packet.HealthAdded); + return Task.CompletedTask; } } diff --git a/NitroxClient/GameLogic/AI.cs b/NitroxClient/GameLogic/AI.cs index 1d0b85ea1c..c76e6d8d38 100644 --- a/NitroxClient/GameLogic/AI.cs +++ b/NitroxClient/GameLogic/AI.cs @@ -5,14 +5,13 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; using static Nitrox.Model.Subnautica.Packets.RangedAttackLastTargetUpdate; namespace NitroxClient.GameLogic; -public class AI +public sealed class AI { private readonly IPacketSender packetSender; private readonly Dictionary actions = []; diff --git a/NitroxClient/GameLogic/Bases/BuildUtils.cs b/NitroxClient/GameLogic/Bases/BuildUtils.cs index f546acf98d..2c0d5a7bbc 100644 --- a/NitroxClient/GameLogic/Bases/BuildUtils.cs +++ b/NitroxClient/GameLogic/Bases/BuildUtils.cs @@ -4,7 +4,6 @@ using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Bases; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; diff --git a/NitroxClient/GameLogic/Bases/BuildingHandler.cs b/NitroxClient/GameLogic/Bases/BuildingHandler.cs index 4e99650514..1581d2c361 100644 --- a/NitroxClient/GameLogic/Bases/BuildingHandler.cs +++ b/NitroxClient/GameLogic/Bases/BuildingHandler.cs @@ -9,7 +9,6 @@ using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Bases; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; diff --git a/NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs b/NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs index 743880daa2..6e0bbc6d20 100644 --- a/NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs +++ b/NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs @@ -1,5 +1,4 @@ using System.Collections; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata.Bases; using UnityEngine; diff --git a/NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs b/NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs index f191e6dca4..464c40efd1 100644 --- a/NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs +++ b/NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs @@ -1,4 +1,3 @@ -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata.Bases; using UnityEngine; diff --git a/NitroxClient/GameLogic/BulletManager.cs b/NitroxClient/GameLogic/BulletManager.cs index f749e6a7ed..16a4af4df5 100644 --- a/NitroxClient/GameLogic/BulletManager.cs +++ b/NitroxClient/GameLogic/BulletManager.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using Nitrox.Model.Core; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; @@ -17,7 +18,7 @@ public class BulletManager // This only allows for one stasis sphere per player // (which is the normal capacity, but could be adapted for a mod letting multiple stasis spheres) - private readonly Dictionary stasisSpherePerPlayerId = []; + private readonly Dictionary stasisSpherePerSessionId = []; /// /// TechTypes of objects which should have a Vehicle MB @@ -80,16 +81,16 @@ public void TorpedoTargetAcquired(NitroxId bulletId, NitroxId targetId, Vector3 } } - public void ShootStasisSphere(ushort playerId, Vector3 position, Quaternion rotation, float speed, float lifeTime, float chargeNormalized) + public void ShootStasisSphere(SessionId sessionId, Vector3 position, Quaternion rotation, float speed, float lifeTime, float chargeNormalized) { - StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId); + StasisSphere cloneSphere = EnsurePlayerHasSphere(sessionId); cloneSphere.Shoot(position, rotation, speed, lifeTime, chargeNormalized); } - public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotation, float chargeNormalized, float consumption) + public void StasisSphereHit(SessionId sessionId, Vector3 position, Quaternion rotation, float chargeNormalized, float consumption) { - StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId); + StasisSphere cloneSphere = EnsurePlayerHasSphere(sessionId); // Setup the sphere in case the shot was sent earlier cloneSphere.Shoot(position, rotation, 0, 0, chargeNormalized); @@ -103,31 +104,31 @@ public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotati cloneSphere.Deactivate(); } - private StasisSphere EnsurePlayerHasSphere(ushort playerId) + private StasisSphere EnsurePlayerHasSphere(SessionId sessionId) { - if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere remoteSphere) && remoteSphere) + if (stasisSpherePerSessionId.TryGetValue(sessionId, out StasisSphere remoteSphere) && remoteSphere) { return remoteSphere; } // It should be set to inactive automatically in Bullet.Awake - GameObject playerSphereClone = GameObject.Instantiate(stasisSpherePrefab); - playerSphereClone.name = $"remote-{playerId}-{playerSphereClone.name}"; + GameObject playerSphereClone = Object.Instantiate(stasisSpherePrefab); + playerSphereClone.name = $"remote-{sessionId}-{playerSphereClone.name}"; // We mark it to be able to ignore events from remote bullets playerSphereClone.AddComponent(); StasisSphere stasisSphere = playerSphereClone.GetComponent(); - stasisSpherePerPlayerId[playerId] = stasisSphere; + stasisSpherePerSessionId[sessionId] = stasisSphere; return stasisSphere; } - private void DestroyPlayerSphere(ushort playerId) + private void DestroyPlayerSphere(SessionId sessionId) { - if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere stasisSphere) && stasisSphere) + if (stasisSpherePerSessionId.TryGetValue(sessionId, out StasisSphere stasisSphere) && stasisSphere) { - GameObject.Destroy(stasisSphere.gameObject); + Object.Destroy(stasisSphere.gameObject); } - stasisSpherePerPlayerId.Remove(playerId); + stasisSpherePerSessionId.Remove(sessionId); } public IEnumerator Initialize() @@ -158,11 +159,11 @@ public IEnumerator Initialize() // Setup remote players' stasis spheres foreach (RemotePlayer remotePlayer in playerManager.GetAll()) { - EnsurePlayerHasSphere(remotePlayer.PlayerId); + EnsurePlayerHasSphere(remotePlayer.SessionId); } - playerManager.OnCreate += (playerId, _) => { EnsurePlayerHasSphere(playerId); }; - playerManager.OnRemove += (playerId, _) => { DestroyPlayerSphere(playerId); }; + playerManager.OnCreate += (sessionId, _) => { EnsurePlayerHasSphere(sessionId); }; + playerManager.OnRemove += (sessionId, _) => { DestroyPlayerSphere(sessionId); }; } public class RemotePlayerBullet : MonoBehaviour; diff --git a/NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs b/NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs index c2b40709e0..128670955d 100644 --- a/NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs +++ b/NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs @@ -2,7 +2,6 @@ using Nitrox.Model.Core; using NitroxClient.GameLogic.Settings; using NitroxClient.MonoBehaviours.Gui.Chat; -using Nitrox.Model.Helper; using UnityEngine; using UnityEngine.UI; using UWE; @@ -10,13 +9,13 @@ namespace NitroxClient.GameLogic.ChatUI; -public class PlayerChatManager +public sealed class PlayerChatManager { public delegate void PlayerChatDelegate(string message); public delegate void PlayerCommandDelegate(string message); - private const char SERVER_COMMAND_PREFIX = '/'; + public const char SERVER_COMMAND_PREFIX = '/'; public static readonly PlayerChatManager Instance = new(); private GameObject chatKeyHint; @@ -25,10 +24,7 @@ public class PlayerChatManager private PlayerChat playerChat; - public bool IsChatSelected - { - get => PlayerChat.IsReady && playerChat.selected; - } + public bool IsChatSelected => PlayerChat.IsReady && playerChat.selected; public Transform PlayerChatTransform => playerChat.transform; @@ -143,6 +139,11 @@ public void SendMessage() OnPlayerChat?.Invoke(trimmedInput); } + public void SetAutoCompleteText(string text) + { + playerChat.AutoCompleteText = text; + } + public IEnumerator LoadChatKeyHint() { if (!NitroxPrefs.ChatUsed.Value) diff --git a/NitroxClient/GameLogic/Cyclops.cs b/NitroxClient/GameLogic/Cyclops.cs index 3e6f759cf4..b3d44d1c82 100644 --- a/NitroxClient/GameLogic/Cyclops.cs +++ b/NitroxClient/GameLogic/Cyclops.cs @@ -5,9 +5,7 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Entities.cs b/NitroxClient/GameLogic/Entities.cs index 9affdb6f15..bbd78e757d 100644 --- a/NitroxClient/GameLogic/Entities.cs +++ b/NitroxClient/GameLogic/Entities.cs @@ -57,7 +57,7 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke entitySpawnersByType[typeof(InstalledBatteryEntity)] = new InstalledBatteryEntitySpawner(); entitySpawnersByType[typeof(InventoryEntity)] = new InventoryEntitySpawner(); entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(entityMetadataManager); - entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, playerManager, localPlayer, this, simulationOwnership); + entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, this, simulationOwnership); entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)]; entitySpawnersByType[typeof(EscapePodEntity)] = new EscapePodEntitySpawner(localPlayer); @@ -161,7 +161,9 @@ public void EnqueueEntitiesToSpawn(List entitiesToEnqueue, List + /// /// Should children be spawned even if already marked as spawned + /// public IEnumerator SpawnBatchAsync(List batch, bool forceRespawn = false, bool skipFrames = true) { // we divide the FPS by 2.5 because we consider (time for 1 frame + spawning time without a frame + extra computing time) diff --git a/NitroxClient/GameLogic/FMOD/FMODSystem.cs b/NitroxClient/GameLogic/FMOD/FMODSystem.cs index 8e98441070..e78f236090 100644 --- a/NitroxClient/GameLogic/FMOD/FMODSystem.cs +++ b/NitroxClient/GameLogic/FMOD/FMODSystem.cs @@ -3,7 +3,6 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; using Nitrox.Model.GameLogic.FMOD; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Fires.cs b/NitroxClient/GameLogic/Fires.cs index 3f1d3ed2f6..e916a0689a 100644 --- a/NitroxClient/GameLogic/Fires.cs +++ b/NitroxClient/GameLogic/Fires.cs @@ -3,7 +3,6 @@ using NitroxClient.Communication.Packets.Processors; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs b/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs index 13f5003f3b..b345d0e9e2 100644 --- a/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs +++ b/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs @@ -126,8 +126,8 @@ public override void OnLateUpdate(bool _) } _isDirty = false; - Dictionary players = playerManager.GetAll().ToDictionary(player => player.PlayerId.ToString(), player => player); - players.Add(localPlayer.PlayerId.ToString(), localPlayer); + Dictionary players = playerManager.GetAll().ToDictionary(player => player.SessionId.ToString(), player => player); + players.Add(localPlayer.SessionId.ToString(), localPlayer); foreach (KeyValuePair entry in players) { @@ -151,7 +151,7 @@ public override void OnLateUpdate(bool _) List sorted = new(tempSort.Keys); sorted.Sort(); - entries[localPlayer.PlayerId.ToString()].rectTransform.SetSiblingIndex(0); + entries[localPlayer.SessionId.ToString()].rectTransform.SetSiblingIndex(0); for (int j = 0; j < sorted.Count; j++) { string id = tempSort[sorted[j]].id; @@ -224,19 +224,18 @@ private void AddNewEntry(string playerId, INitroxPlayer player) entries.Add(playerId, entry); } - private void OnAdd(ushort playerId, RemotePlayer remotePlayer) + private void OnAdd(SessionId sessionId, RemotePlayer remotePlayer) { _isDirty = true; } - private void OnRemove(ushort playerId, RemotePlayer remotePlayers) + private void OnRemove(SessionId sessionId, RemotePlayer remotePlayers) { - string playerIdString = playerId.ToString(); - if (!entries.ContainsKey(playerIdString)) + string playerIdString = sessionId.ToString(); + if (!entries.TryGetValue(playerIdString, out uGUI_PlayerPingEntry? entry)) { return; } - uGUI_PlayerPingEntry entry = entries[playerIdString]; entries.Remove(playerIdString); pool.Release(entry); _isDirty = true; diff --git a/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs b/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs index d508effdd6..b17c395659 100644 --- a/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs +++ b/NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs @@ -5,10 +5,8 @@ using NitroxClient.GameLogic.HUD.Components; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.MonoBehaviours.Gui.Modals; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Core; using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; using UnityEngine.UI; @@ -54,7 +52,7 @@ private bool muted { NitroxServiceLocator.LocateService().OnPlayerMuted += (playerId, _) => { - if (player is RemotePlayer remotePlayer && remotePlayer.PlayerId == playerId) + if (player is RemotePlayer remotePlayer && remotePlayer.SessionId == playerId) { RefreshMuteButton(); } diff --git a/NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs b/NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs index 2aaf236441..2c42055d7a 100644 --- a/NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs +++ b/NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Nitrox.Model.Core; using NitroxClient.MonoBehaviours.Gui.HUD; using UnityEngine; @@ -6,28 +7,28 @@ namespace NitroxClient.GameLogic.HUD; public class PlayerVitalsManager { - private readonly Dictionary vitalsByPlayerId = new(); + private readonly Dictionary vitalsBySessionId = new(); public RemotePlayerVitals CreateOrFindForPlayer(RemotePlayer remotePlayer) { - if (!vitalsByPlayerId.TryGetValue(remotePlayer.PlayerId, out RemotePlayerVitals vitals)) + if (!vitalsBySessionId.TryGetValue(remotePlayer.SessionId, out RemotePlayerVitals vitals)) { - vitalsByPlayerId[remotePlayer.PlayerId] = vitals = RemotePlayerVitals.CreateForPlayer(remotePlayer); + vitalsBySessionId[remotePlayer.SessionId] = vitals = RemotePlayerVitals.CreateForPlayer(remotePlayer); } return vitals; } - public void RemoveForPlayer(ushort playerId) + public void RemoveForPlayer(SessionId sessionId) { - if (vitalsByPlayerId.TryGetValue(playerId, out RemotePlayerVitals vitals)) + if (vitalsBySessionId.TryGetValue(sessionId, out RemotePlayerVitals vitals)) { - vitalsByPlayerId.Remove(playerId); + vitalsBySessionId.Remove(sessionId); Object.Destroy(vitals.gameObject); } } - public bool TryFindForPlayer(ushort playerId, out RemotePlayerVitals vitals) + public bool TryFindForPlayer(SessionId sessionId, out RemotePlayerVitals vitals) { - return vitalsByPlayerId.TryGetValue(playerId, out vitals); + return vitalsBySessionId.TryGetValue(sessionId, out vitals); } } diff --git a/NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs b/NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs index 7971a7579f..ecb51c420f 100644 --- a/NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs +++ b/NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs @@ -2,55 +2,54 @@ using System.Collections.Generic; using Nitrox.Model.DataStructures; -namespace NitroxClient.GameLogic.Helper +namespace NitroxClient.GameLogic.Helper; + +/** + * Class used for temporarily storing variables local to patched methods. Certain circumstances require that these + * be referenced at a later point and most of the time it is too prohibitive to expose global statics. + * + * An example use-case is the created gameobject from the vehicle constructor class. This gameobject is only accessible + * locally when crafted. We need to access it at future times to retrieve and set its GUID. + */ +internal static class TransientLocalObjectManager { - /** - * Class used for temporarily storing variables local to patched methods. Certain circumstances require that these - * be referenced at a later point and most of the time it is too prohibitive to expose global statics. - * - * An example use-case is the created gameobject from the vehicle constructor class. This gameobject is only accessible - * locally when crafted. We need to access it at future times to retrieve and set its GUID. - */ - public static class TransientLocalObjectManager + public enum TransientObjectType { - public enum TransientObjectType - { - BASE_GHOST_NEWLY_CONSTRUCTED_BASE_GAMEOBJECT, + BASE_GHOST_NEWLY_CONSTRUCTED_BASE_GAMEOBJECT, - LATEST_DECONSTRUCTED_BASE_PIECE_GHOST, - LATEST_DECONSTRUCTED_BASE_PIECE_GUID, + LATEST_DECONSTRUCTED_BASE_PIECE_GHOST, + LATEST_DECONSTRUCTED_BASE_PIECE_GUID, - LATER_CONSTRUCTED_BASE, - LATER_OBJECT_LATEST_BASE, - LATER_OBJECT_LATEST_CELL, - } + LATER_CONSTRUCTED_BASE, + LATER_OBJECT_LATEST_BASE, + LATER_OBJECT_LATEST_CELL, + } - private static readonly Dictionary localObjectsById = new(); + private static readonly Dictionary localObjectsById = new(); - public static void Add(TransientObjectType key, object o) - { - localObjectsById[key] = o; - } + public static void Add(TransientObjectType key, object o) + { + localObjectsById[key] = o; + } - public static void Remove(TransientObjectType key) - { - localObjectsById.Remove(key); - } + public static void Remove(TransientObjectType key) + { + localObjectsById.Remove(key); + } - public static Optional Get(TransientObjectType key) - { - localObjectsById.TryGetValue(key, out object obj); - return Optional.OfNullable(obj); - } + public static Optional Get(TransientObjectType key) + { + localObjectsById.TryGetValue(key, out object obj); + return Optional.OfNullable(obj); + } - public static T Require(TransientObjectType key) + public static T Require(TransientObjectType key) + { + if (!localObjectsById.TryGetValue(key, out object obj)) { - if (!localObjectsById.TryGetValue(key, out object obj)) - { - throw new Exception($"Did not have an entry for key: {key}"); - } - - return (T)obj; + throw new Exception($"Did not have an entry for key: {key}"); } + + return (T)obj; } } diff --git a/NitroxClient/GameLogic/InitialSync/Abstract/InitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/Abstract/InitialSyncProcessor.cs index 8d654122f2..c2c98da206 100644 --- a/NitroxClient/GameLogic/InitialSync/Abstract/InitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/Abstract/InitialSyncProcessor.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.GameLogic.InitialSync.Abstract; diff --git a/NitroxClient/GameLogic/InitialSync/EquippedItemInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/EquippedItemInitialSyncProcessor.cs index da05fef695..92edef8b1b 100644 --- a/NitroxClient/GameLogic/InitialSync/EquippedItemInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/EquippedItemInitialSyncProcessor.cs @@ -5,7 +5,6 @@ using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/InitialSync/GlobalRootInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/GlobalRootInitialSyncProcessor.cs index 490aaab9ad..351074f346 100644 --- a/NitroxClient/GameLogic/InitialSync/GlobalRootInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/GlobalRootInitialSyncProcessor.cs @@ -3,8 +3,6 @@ using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours.Cyclops; using Nitrox.Model.GameLogic.PlayerAnimation; -using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.MultiplayerSession; using Nitrox.Model.Subnautica.Packets; using UnityEngine; @@ -72,8 +70,8 @@ public void RestoreDrivers(InitialPlayerSync packet) if (playerContext.DrivingVehicle != null) { Log.Info($"Restoring driver state of {playerContext.PlayerName} in {playerContext.DrivingVehicle}"); - vehicles.SetOnPilotMode(playerContext.DrivingVehicle, playerContext.PlayerId, true); - if (playerManager.TryFind(playerContext.PlayerId, out RemotePlayer remotePlayer)) + vehicles.SetOnPilotMode(playerContext.DrivingVehicle, playerContext.SessionId, true); + if (playerManager.TryFind(playerContext.SessionId, out RemotePlayer remotePlayer)) { // As remote players are still driving, they aren't updating their IsUnderwater state so UnderwaterStateTracker.Update // isn't going to send a packet. Therefore we need to set this by hand diff --git a/NitroxClient/GameLogic/InitialSync/PdaInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PdaInitialSyncProcessor.cs index 0d3499d371..f5f20d8f53 100644 --- a/NitroxClient/GameLogic/InitialSync/PdaInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PdaInitialSyncProcessor.cs @@ -4,8 +4,6 @@ using System.Linq; using NitroxClient.Communication; using NitroxClient.GameLogic.InitialSync.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; diff --git a/NitroxClient/GameLogic/InitialSync/PlayerPositionInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PlayerPositionInitialSyncProcessor.cs index 254f82d94c..017a2783f8 100644 --- a/NitroxClient/GameLogic/InitialSync/PlayerPositionInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PlayerPositionInitialSyncProcessor.cs @@ -3,8 +3,6 @@ using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; using UnityEngine; using UWE; diff --git a/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs index 4c49bcf9f5..aa676c97d2 100644 --- a/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/PlayerPreferencesInitialSyncProcessor.cs @@ -6,7 +6,6 @@ using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; diff --git a/NitroxClient/GameLogic/InitialSync/QuickSlotInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/QuickSlotInitialSyncProcessor.cs index 3a86b9354d..f9829c4165 100644 --- a/NitroxClient/GameLogic/InitialSync/QuickSlotInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/QuickSlotInitialSyncProcessor.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.InitialSync.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.GameLogic.InitialSync; diff --git a/NitroxClient/GameLogic/InitialSync/RemotePlayerInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/RemotePlayerInitialSyncProcessor.cs index 37bfbd2ffc..a7be684372 100644 --- a/NitroxClient/GameLogic/InitialSync/RemotePlayerInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/RemotePlayerInitialSyncProcessor.cs @@ -1,7 +1,5 @@ using System.Collections; using NitroxClient.GameLogic.InitialSync.Abstract; -using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.MultiplayerSession; using Nitrox.Model.Subnautica.Packets; diff --git a/NitroxClient/GameLogic/InitialSync/SimulationOwnershipInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/SimulationOwnershipInitialSyncProcessor.cs index 992376e99d..9e53e44528 100644 --- a/NitroxClient/GameLogic/InitialSync/SimulationOwnershipInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/SimulationOwnershipInitialSyncProcessor.cs @@ -1,7 +1,6 @@ using System.Collections; using NitroxClient.GameLogic.InitialSync.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.GameLogic.InitialSync; diff --git a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs index 03282d6dd0..e2bb013519 100644 --- a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs @@ -3,7 +3,6 @@ using System.Linq; using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; using Story; diff --git a/NitroxClient/GameLogic/Interior.cs b/NitroxClient/GameLogic/Interior.cs index 919efa19cc..d18714e452 100644 --- a/NitroxClient/GameLogic/Interior.cs +++ b/NitroxClient/GameLogic/Interior.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.GameLogic diff --git a/NitroxClient/GameLogic/Items.cs b/NitroxClient/GameLogic/Items.cs index a2a0727d91..20f1736a66 100644 --- a/NitroxClient/GameLogic/Items.cs +++ b/NitroxClient/GameLogic/Items.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; @@ -18,7 +19,7 @@ public class Items { private readonly IPacketSender packetSender; private readonly Entities entities; - public static GameObject PickingUpObject { get; private set; } + public static GameObject? PickingUpObject { get; private set; } private readonly EntityMetadataManager entityMetadataManager; /// @@ -54,7 +55,7 @@ public void PickedUp(GameObject gameObject, TechType techType, NitroxId containe { PickingUpObject = gameObject; - // Try catch to avoid blocking PickingUpObject with a non null value outside of the current context + // Try catch to avoid blocking PickingUpObject with a non-null value outside the current context try { InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, containerId); @@ -214,7 +215,7 @@ public static IEnumerable GetPrefabChildren(GameObject gameObject, Nitro if (metadata.HasValue) { TechTag techTag = prefab.gameObject.GetComponent(); - TechType techType = (techTag) ? techTag.type : TechType.None; + TechType techType = techTag ? techTag.type : TechType.None; yield return new PrefabChildEntity(id, prefab.classId, techType.ToDto(), indexInGroup, metadata.Value, parentId); @@ -268,7 +269,8 @@ private void RemoveAnyRemoteControl(GameObject gameObject) } /// Parent of the GameObject to check - public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPark) + /// The waterpark, if known for the parent + public static bool TryGetParentWaterPark(Transform parent, [NotNullWhen(true)] out WaterPark? waterPark) { // NB: When dropped in a WaterPark, items are placed under WaterPark/items_root/ // So we need to search two steps higher to find the WaterPark @@ -281,11 +283,10 @@ public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPa return false; } - /// - private static bool TryGetParentWaterParkId(Transform parent, out NitroxId waterParkId) + private static bool TryGetParentWaterParkId(Transform parent, out NitroxId? waterParkId) { - if (TryGetParentWaterPark(parent, out WaterPark waterPark) && waterPark.TryGetNitroxId(out waterParkId)) + if (TryGetParentWaterPark(parent, out WaterPark? waterPark) && waterPark.TryGetNitroxId(out waterParkId)) { return true; } diff --git a/NitroxClient/GameLogic/LocalPlayer.cs b/NitroxClient/GameLogic/LocalPlayer.cs index 327f52a0c1..caf235f376 100644 --- a/NitroxClient/GameLogic/LocalPlayer.cs +++ b/NitroxClient/GameLogic/LocalPlayer.cs @@ -1,4 +1,5 @@ using System; +using Nitrox.Model.Core; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; @@ -6,9 +7,6 @@ using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.GameLogic; using Nitrox.Model.GameLogic.PlayerAnimation; -using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.MultiplayerSession; using Nitrox.Model.Subnautica.Packets; @@ -37,7 +35,7 @@ public class LocalPlayer : ILocalNitroxPlayer /// /// Gets the player id. The session is lost on disconnect so this can return null. /// - public ushort? PlayerId => multiplayerSession?.Reservation?.PlayerId; + public SessionId? SessionId => multiplayerSession.Reservation?.SessionId; public PlayerSettings PlayerSettings => multiplayerSession.PlayerSettings; public Perms Permissions { get; set; } @@ -59,27 +57,27 @@ public LocalPlayer(IMultiplayerSession multiplayerSession, IPacketSender packetS public void BroadcastLocation(Vector3 location, Vector3 velocity, Quaternion bodyRotation, Quaternion aimingRotation) { - if (!PlayerId.HasValue) + if (!SessionId.HasValue) { return; } - PlayerMovement playerMovement = new(PlayerId.Value, location.ToDto(), velocity.ToDto(), bodyRotation.ToDto(), aimingRotation.ToDto()); + PlayerMovement playerMovement = new(SessionId.Value, location.ToDto(), velocity.ToDto(), bodyRotation.ToDto(), aimingRotation.ToDto()); packetSender.Send(playerMovement); } public void AnimationChange(AnimChangeType type, AnimChangeState state) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new AnimationChangeEvent(PlayerId.Value, new(type, state))); + packetSender.Send(new AnimationChangeEvent(SessionId.Value, new(type, state))); } } public void InPrecursorChange(bool inPrecursor) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { packetSender.Send(new UpdateInPrecursor(inPrecursor)); } @@ -87,7 +85,7 @@ public void InPrecursorChange(bool inPrecursor) public void DisplaySurfaceWaterChange(bool displaySurfaceWater) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { packetSender.Send(new UpdateDisplaySurfaceWater(displaySurfaceWater)); } @@ -95,43 +93,43 @@ public void DisplaySurfaceWaterChange(bool displaySurfaceWater) public void BroadcastStats(float oxygen, float maxOxygen, float health, float food, float water, float infectionAmount) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new PlayerStats(PlayerId.Value, oxygen, maxOxygen, health, food, water, infectionAmount)); + packetSender.Send(new PlayerStats(SessionId.Value, oxygen, maxOxygen, health, food, water, infectionAmount)); } } public void BroadcastDeath(Vector3 deathPosition) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new PlayerDeathEvent(PlayerId.Value, deathPosition.ToDto())); + packetSender.Send(new PlayerDeathEvent(SessionId.Value, deathPosition.ToDto())); } } public void BroadcastSubrootChange(Optional subrootId) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new SubRootChanged(PlayerId.Value, subrootId)); + packetSender.Send(new SubRootChanged(SessionId.Value, subrootId)); } } public void BroadcastEscapePodChange(Optional escapePodId) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new EscapePodChanged(PlayerId.Value, escapePodId)); + packetSender.Send(new EscapePodChanged(SessionId.Value, escapePodId)); } } public void BroadcastWeld(NitroxId id, float healthAdded) => packetSender.Send(new WeldAction(id, healthAdded)); - public void BroadcastHeldItemChanged(NitroxId itemId, PlayerHeldItemChanged.ChangeType techType, NitroxTechType isFirstTime) + public void BroadcastHeldItemChanged(NitroxId itemId, PlayerHeldItemChanged.ChangeType techType, NitroxTechType? isFirstTime) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new PlayerHeldItemChanged(PlayerId.Value, itemId, techType, isFirstTime)); + packetSender.Send(new PlayerHeldItemChanged(SessionId.Value, itemId, techType, isFirstTime)); } } @@ -139,9 +137,9 @@ public void BroadcastHeldItemChanged(NitroxId itemId, PlayerHeldItemChanged.Chan public void BroadcastBenchChanged(NitroxId bench, BenchChanged.BenchChangeState changeState) { - if (PlayerId.HasValue) + if (SessionId.HasValue) { - packetSender.Send(new BenchChanged(PlayerId.Value, bench, changeState)); + packetSender.Send(new BenchChanged(SessionId.Value, bench, changeState)); } } diff --git a/NitroxClient/GameLogic/MedkitFabricator.cs b/NitroxClient/GameLogic/MedkitFabricator.cs index 88b83b1fc0..2269bfc4fc 100644 --- a/NitroxClient/GameLogic/MedkitFabricator.cs +++ b/NitroxClient/GameLogic/MedkitFabricator.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.GameLogic diff --git a/NitroxClient/GameLogic/MobileVehicleBay.cs b/NitroxClient/GameLogic/MobileVehicleBay.cs index b2ef71a9cb..a75a0e768b 100644 --- a/NitroxClient/GameLogic/MobileVehicleBay.cs +++ b/NitroxClient/GameLogic/MobileVehicleBay.cs @@ -1,7 +1,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/NitroxConsole.cs b/NitroxClient/GameLogic/NitroxConsole.cs index 87ef67be4a..e4863f02cd 100644 --- a/NitroxClient/GameLogic/NitroxConsole.cs +++ b/NitroxClient/GameLogic/NitroxConsole.cs @@ -2,7 +2,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.Helper; using Nitrox.Model.Subnautica.Packets; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs index 9928c52182..8e0a797b2e 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using Nitrox.Model.Core; using NitroxClient.Communication.Abstract; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; @@ -15,7 +15,7 @@ public class PlayerCinematics private IntroCinematicMode lastModeToSend = IntroCinematicMode.NONE; - public ushort? IntroCinematicPartnerId = null; + public SessionId? IntroCinematicPartnerId = null; /// /// Some cinematics should not be played. Example the intro as it's completely handled by a dedicated system. @@ -28,27 +28,27 @@ public PlayerCinematics(IPacketSender packetSender, LocalPlayer localPlayer) this.localPlayer = localPlayer; } - public void StartCinematicMode(ushort playerId, NitroxId controllerID, int controllerNameHash, string key) + public void StartCinematicMode(SessionId sessionId, NitroxId controllerID, int controllerNameHash, string key) { if (!blacklistedKeys.Contains(key)) { - packetSender.Send(new PlayerCinematicControllerCall(playerId, controllerID, controllerNameHash, key, true)); + packetSender.Send(new PlayerCinematicControllerCall(sessionId, controllerID, controllerNameHash, key, true)); } } - public void EndCinematicMode(ushort playerId, NitroxId controllerID, int controllerNameHash, string key) + public void EndCinematicMode(SessionId sessionId, NitroxId controllerID, int controllerNameHash, string key) { if (!blacklistedKeys.Contains(key)) { - packetSender.Send(new PlayerCinematicControllerCall(playerId, controllerID, controllerNameHash, key, false)); + packetSender.Send(new PlayerCinematicControllerCall(sessionId, controllerID, controllerNameHash, key, false)); } } public void SetLocalIntroCinematicMode(IntroCinematicMode introCinematicMode) { - if (!localPlayer.PlayerId.HasValue) + if (!localPlayer.SessionId.HasValue) { - Log.Error($"PlayerId was null while setting IntroCinematicMode to {introCinematicMode}"); + Log.Error($"{nameof(SessionId)} was null while setting IntroCinematicMode to {introCinematicMode}"); return; } @@ -62,7 +62,7 @@ public void SetLocalIntroCinematicMode(IntroCinematicMode introCinematicMode) // This method can be called before client is joined. To prevent sending as an unauthenticated packet we delay it. if (Multiplayer.Joined) { - packetSender.Send(new SetIntroCinematicMode(localPlayer.PlayerId.Value, introCinematicMode)); + packetSender.Send(new SetIntroCinematicMode(localPlayer.SessionId.Value, introCinematicMode)); return; } diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/Abstract/INitroxPlayer.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/Abstract/INitroxPlayer.cs index be495df4e7..16c7668020 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/Abstract/INitroxPlayer.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/Abstract/INitroxPlayer.cs @@ -1,5 +1,4 @@ -using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Subnautica.MultiplayerSession; +using Nitrox.Model.Subnautica.MultiplayerSession; using UnityEngine; namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/DiveSuitColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/DiveSuitColorSwapManager.cs index d346e23983..cf3f5f1213 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/DiveSuitColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/DiveSuitColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/FinColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/FinColorSwapManager.cs index 4264494214..771d6edfc6 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/FinColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/FinColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationHelmetColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationHelmetColorSwapManager.cs index 9f8ad21feb..b57d8fba5d 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationHelmetColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationHelmetColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitColorSwapManager.cs index 176a30c63a..34eac40ff4 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitVestColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitVestColorSwapManager.cs index a02345fb48..7a4ceb1ea1 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitVestColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationSuitVestColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationTankColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationTankColorSwapManager.cs index 9636ac3c1b..640a156f0c 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationTankColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RadiationTankColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RebreatherColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RebreatherColorSwapManager.cs index 9ceefab040..727c4be504 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RebreatherColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/RebreatherColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ReinforcedSuitColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ReinforcedSuitColorSwapManager.cs index e50bdcc225..99ab632f71 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ReinforcedSuitColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ReinforcedSuitColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ScubaTankColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ScubaTankColorSwapManager.cs index cd84875412..aa936861b0 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ScubaTankColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/ScubaTankColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/StillSuitColorSwapManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/StillSuitColorSwapManager.cs index 3815715868..91800361f0 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/StillSuitColorSwapManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/ColorSwap/StillSuitColorSwapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/PlayerModelManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/PlayerModelManager.cs index 17b93539c3..07d93312a8 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerModel/PlayerModelManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerModel/PlayerModelManager.cs @@ -6,7 +6,6 @@ using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment; using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; using Object = UnityEngine.Object; diff --git a/NitroxClient/GameLogic/PlayerLogic/PlayerPreferences/PlayerPreferenceManager.cs b/NitroxClient/GameLogic/PlayerLogic/PlayerPreferences/PlayerPreferenceManager.cs index 1e16870ccc..49ffe18c91 100644 --- a/NitroxClient/GameLogic/PlayerLogic/PlayerPreferences/PlayerPreferenceManager.cs +++ b/NitroxClient/GameLogic/PlayerLogic/PlayerPreferences/PlayerPreferenceManager.cs @@ -1,6 +1,5 @@ using Nitrox.Model.Helper; using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Subnautica.DataStructures; using UnityEngine; namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences diff --git a/NitroxClient/GameLogic/PlayerManager.cs b/NitroxClient/GameLogic/PlayerManager.cs index 6feb95f25b..8ccb3ee2d5 100644 --- a/NitroxClient/GameLogic/PlayerManager.cs +++ b/NitroxClient/GameLogic/PlayerManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Nitrox.Model.Core; using NitroxClient.GameLogic.HUD; using NitroxClient.GameLogic.PlayerLogic.PlayerModel; using NitroxClient.MonoBehaviours.Discord; @@ -16,7 +17,7 @@ public class PlayerManager private readonly PlayerModelManager playerModelManager; private readonly PlayerVitalsManager playerVitalsManager; private readonly FMODWhitelist fmodWhitelist; - private readonly Dictionary playersById = new(); + private readonly Dictionary sessionsById = new(); public OnCreateDelegate OnCreate; public OnRemoveDelegate OnRemove; @@ -28,17 +29,17 @@ public PlayerManager(PlayerModelManager playerModelManager, PlayerVitalsManager this.fmodWhitelist = fmodWhitelist; } - public Optional Find(ushort playerId) + public Optional Find(SessionId sessionId) { - playersById.TryGetValue(playerId, out RemotePlayer player); + sessionsById.TryGetValue(sessionId, out RemotePlayer player); return Optional.OfNullable(player); } - public bool TryFind(ushort playerId, out RemotePlayer remotePlayer) => playersById.TryGetValue(playerId, out remotePlayer); + public bool TryFind(SessionId sessionId, out RemotePlayer remotePlayer) => sessionsById.TryGetValue(sessionId, out remotePlayer); public Optional Find(NitroxId playerNitroxId) { - RemotePlayer remotePlayer = playersById.Select(idToPlayer => idToPlayer.Value) + RemotePlayer remotePlayer = sessionsById.Select(idToPlayer => idToPlayer.Value) .FirstOrDefault(player => player.PlayerContext.PlayerNitroxId == playerNitroxId); return Optional.OfNullable(remotePlayer); @@ -46,7 +47,7 @@ public Optional Find(NitroxId playerNitroxId) public IEnumerable GetAll() { - return playersById.Values; + return sessionsById.Values; } public HashSet GetAllPlayerObjects() @@ -65,32 +66,37 @@ public HashSet GetAllPlayerObjects() public RemotePlayer Create(PlayerContext playerContext) { Validate.NotNull(playerContext); - Validate.IsFalse(playersById.ContainsKey(playerContext.PlayerId)); + + // Can happen that player is already known if both join queue & initial sync happen. + if (sessionsById.TryGetValue(playerContext.SessionId, out RemotePlayer alreadyAddedPlayer)) + { + return alreadyAddedPlayer; + } RemotePlayer remotePlayer = new(playerContext, playerModelManager, playerVitalsManager, fmodWhitelist); - playersById.Add(remotePlayer.PlayerId, remotePlayer); - OnCreate(remotePlayer.PlayerId, remotePlayer); + sessionsById.Add(remotePlayer.SessionId, remotePlayer); + OnCreate(remotePlayer.SessionId, remotePlayer); DiscordClient.UpdatePartySize(GetTotalPlayerCount()); return remotePlayer; } - public void RemovePlayer(ushort playerId) + public void RemovePlayer(SessionId sessionId) { - if (playersById.TryGetValue(playerId, out RemotePlayer player)) + if (sessionsById.TryGetValue(sessionId, out RemotePlayer player)) { player.Destroy(); - playersById.Remove(playerId); - OnRemove(playerId, player); + sessionsById.Remove(sessionId); + OnRemove(sessionId, player); DiscordClient.UpdatePartySize(GetTotalPlayerCount()); } } /// Remote players + You => X + 1 - public int GetTotalPlayerCount() => playersById.Count + 1; + public int GetTotalPlayerCount() => sessionsById.Count + 1; - public delegate void OnCreateDelegate(ushort playerId, RemotePlayer remotePlayer); - public delegate void OnRemoveDelegate(ushort playerId, RemotePlayer remotePlayer); + public delegate void OnCreateDelegate(SessionId sessionId, RemotePlayer remotePlayer); + public delegate void OnRemoveDelegate(SessionId sessionId, RemotePlayer remotePlayer); } diff --git a/NitroxClient/GameLogic/RemotePlayer.cs b/NitroxClient/GameLogic/RemotePlayer.cs index 71a596ae11..7814757560 100644 --- a/NitroxClient/GameLogic/RemotePlayer.cs +++ b/NitroxClient/GameLogic/RemotePlayer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using Nitrox.Model.Core; using Nitrox.Model.DataStructures.GameLogic; using NitroxClient.GameLogic.HUD; using NitroxClient.GameLogic.PlayerLogic; @@ -11,9 +12,6 @@ using NitroxClient.MonoBehaviours.Vehicles; using Nitrox.Model.GameLogic.FMOD; using Nitrox.Model.GameLogic.PlayerAnimation; -using Nitrox.Model.MultiplayerSession; -using Nitrox.Model.Server; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.MultiplayerSession; using UnityEngine; @@ -36,7 +34,7 @@ public class RemotePlayer : INitroxPlayer private readonly FMODWhitelist fmodWhitelist; public PlayerContext PlayerContext { get; } - public GameObject Body { get; private set; } + public GameObject? Body { get; private set; } public GameObject PlayerModel { get; private set; } public Rigidbody RigidBody { get; private set; } public CapsuleCollider Collider { get; private set; } @@ -46,13 +44,13 @@ public class RemotePlayer : INitroxPlayer public Transform ItemAttachPoint { get; private set; } public RemotePlayerVitals vitals { get; private set; } - public ushort PlayerId => PlayerContext.PlayerId; + public SessionId SessionId => PlayerContext.SessionId; public string PlayerName => PlayerContext.PlayerName; public PlayerSettings PlayerSettings => PlayerContext.PlayerSettings; public Vehicle Vehicle { get; private set; } public SubRoot SubRoot { get; private set; } - public EscapePod EscapePod { get; private set; } + public EscapePod? EscapePod { get; private set; } public PilotingChair PilotingChair { get; private set; } public InfectedMixin InfectedMixin { get; private set; } public LiveMixin LiveMixin { get; private set; } @@ -272,7 +270,7 @@ public void SetSubRoot(SubRoot newSubRoot, bool force = false) } } - public void SetEscapePod(EscapePod newEscapePod) + public void SetEscapePod(EscapePod? newEscapePod) { if (EscapePod != newEscapePod) { diff --git a/NitroxClient/GameLogic/Rockets.cs b/NitroxClient/GameLogic/Rockets.cs index a335153b5b..fe7b436871 100644 --- a/NitroxClient/GameLogic/Rockets.cs +++ b/NitroxClient/GameLogic/Rockets.cs @@ -2,7 +2,6 @@ using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/SeamothModulesEvent.cs b/NitroxClient/GameLogic/SeamothModulesEvent.cs index 2e4f4e36ae..5c25579bdf 100644 --- a/NitroxClient/GameLogic/SeamothModulesEvent.cs +++ b/NitroxClient/GameLogic/SeamothModulesEvent.cs @@ -1,7 +1,5 @@ using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/SimulationOwnership.cs b/NitroxClient/GameLogic/SimulationOwnership.cs index 845ac78a57..b3b2f76989 100644 --- a/NitroxClient/GameLogic/SimulationOwnership.cs +++ b/NitroxClient/GameLogic/SimulationOwnership.cs @@ -3,7 +3,6 @@ using NitroxClient.GameLogic.Simulation; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; @@ -44,7 +43,7 @@ public bool HasExclusiveLock(NitroxId id) public void RequestSimulationLock(NitroxId id, SimulationLockType lockType) { - SimulationOwnershipRequest ownershipRequest = new SimulationOwnershipRequest(multiplayerSession.Reservation.PlayerId, id, lockType); + SimulationOwnershipRequest ownershipRequest = new SimulationOwnershipRequest(multiplayerSession.Reservation.SessionId, id, lockType); packetSender.Send(ownershipRequest); } @@ -83,7 +82,7 @@ public void StopSimulatingEntity(NitroxId id) public void TreatSimulatedEntity(SimulatedEntity simulatedEntity) { - bool isLocalPlayerNewOwner = multiplayerSession.Reservation.PlayerId == simulatedEntity.PlayerId; + bool isLocalPlayerNewOwner = multiplayerSession.Reservation.SessionId == simulatedEntity.SessionId; if (TreatVehicleEntity(simulatedEntity.Id, isLocalPlayerNewOwner, simulatedEntity.LockType) || newerSimulationById.ContainsKey(simulatedEntity.Id)) diff --git a/NitroxClient/GameLogic/Spawning/Bases/BaseLeakEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/Bases/BaseLeakEntitySpawner.cs index 18497cae9f..bb04b850c9 100644 --- a/NitroxClient/GameLogic/Spawning/Bases/BaseLeakEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/Bases/BaseLeakEntitySpawner.cs @@ -2,7 +2,6 @@ using Nitrox.Model.DataStructures; using NitroxClient.GameLogic.Spawning.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs index 3555504da2..dca3fe894c 100644 --- a/NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs @@ -8,7 +8,6 @@ using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Bases; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; diff --git a/NitroxClient/GameLogic/Spawning/Bases/InteriorPieceEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/Bases/InteriorPieceEntitySpawner.cs index a410df6b7b..8064fa8c5d 100644 --- a/NitroxClient/GameLogic/Spawning/Bases/InteriorPieceEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/Bases/InteriorPieceEntitySpawner.cs @@ -6,7 +6,6 @@ using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Bases; diff --git a/NitroxClient/GameLogic/Spawning/EscapePodEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/EscapePodEntitySpawner.cs index 76055a6ba2..3d795521d9 100644 --- a/NitroxClient/GameLogic/Spawning/EscapePodEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/EscapePodEntitySpawner.cs @@ -6,8 +6,6 @@ using NitroxClient.GameLogic.Spawning.Metadata.Processor; using NitroxClient.MonoBehaviours; using NitroxClient.MonoBehaviours.CinematicController; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; diff --git a/NitroxClient/GameLogic/Spawning/InstalledBatteryEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/InstalledBatteryEntitySpawner.cs index d9a674ddd2..02ae906842 100644 --- a/NitroxClient/GameLogic/Spawning/InstalledBatteryEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/InstalledBatteryEntitySpawner.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Nitrox.Model.DataStructures; using NitroxClient.Communication; @@ -15,7 +16,7 @@ public class InstalledBatteryEntitySpawner : SyncEntitySpawner> result) { - if (!CanSpawn(entity, out EnergyMixin energyMixin, out string errorLog)) + if (!CanSpawn(entity, out EnergyMixin? energyMixin, out string errorLog)) { Log.Error(errorLog); result.Set(Optional.Empty); @@ -53,7 +54,7 @@ protected override bool SpawnSync(InstalledBatteryEntity entity, TaskResult false; - private bool CanSpawn(InstalledBatteryEntity entity, out EnergyMixin energyMixin, out string errorLog) + private bool CanSpawn(InstalledBatteryEntity entity, [NotNullWhen(true)] out EnergyMixin? energyMixin, [NotNullWhen(false)] out string? errorLog) { if (!NitroxEntity.TryGetObjectFrom(entity.ParentId, out GameObject parentObject)) { @@ -63,9 +64,9 @@ private bool CanSpawn(InstalledBatteryEntity entity, out EnergyMixin energyMixin } energyMixin = parentObject.GetAllComponentsInChildren() - .ElementAt(entity.ComponentIndex); + .ElementAtOrDefault(entity.ComponentIndex); - if (!energyMixin) + if (energyMixin == null) { errorLog = $"Unable to find EnergyMixin on parent to install battery {entity}"; return false; diff --git a/NitroxClient/GameLogic/Spawning/InstalledModuleEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/InstalledModuleEntitySpawner.cs index c81ab4678e..38075a056a 100644 --- a/NitroxClient/GameLogic/Spawning/InstalledModuleEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/InstalledModuleEntitySpawner.cs @@ -4,7 +4,6 @@ using NitroxClient.GameLogic.Spawning.Abstract; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/InventoryEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/InventoryEntitySpawner.cs index 6a3d11a151..9bdcb73989 100644 --- a/NitroxClient/GameLogic/Spawning/InventoryEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/InventoryEntitySpawner.cs @@ -19,10 +19,10 @@ protected override IEnumerator SpawnAsync(InventoryEntity entity, TaskResult> result) { GameObject parent = NitroxEntity.RequireObjectFrom(entity.ParentId); - StorageContainer container = parent.GetAllComponentsInChildren() - .ElementAt(entity.ComponentIndex); + StorageContainer? container = parent.GetAllComponentsInChildren() + .ElementAtOrDefault(entity.ComponentIndex); - if (container) + if (container != null) { NitroxEntity.SetNewId(container.gameObject, entity.Id); result.Set(Optional.OfNullable(container.gameObject)); diff --git a/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs index 43eb5512d6..e2950005d2 100644 --- a/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/InventoryItemEntitySpawner.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Diagnostics.CodeAnalysis; using Nitrox.Model.DataStructures; using NitroxClient.Communication; using NitroxClient.GameLogic.Helper; @@ -7,8 +8,6 @@ using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; @@ -23,7 +22,7 @@ public class InventoryItemEntitySpawner(EntityMetadataManager entityMetadataMana protected override IEnumerator SpawnAsync(InventoryItemEntity entity, TaskResult> result) { - if (!CanSpawn(entity, out GameObject parentObject, out ItemsContainer container, out string errorLog)) + if (!CanSpawn(entity, out GameObject? parentObject, out ItemsContainer? container, out string? errorLog)) { Log.Error(errorLog); result.Set(Optional.Empty); @@ -45,7 +44,7 @@ protected override bool SpawnSync(InventoryItemEntity entity, TaskResult false; - private bool CanSpawn(InventoryItemEntity entity, out GameObject parentObject, out ItemsContainer container, out string errorLog) + private bool CanSpawn(InventoryItemEntity entity, [NotNullWhen(true)] out GameObject? parentObject, [NotNullWhen(true)] out ItemsContainer? container, [NotNullWhen(false)] out string? errorLog) { Optional owner = NitroxEntity.GetObjectFrom(entity.ParentId); if (!owner.HasValue) diff --git a/NitroxClient/GameLogic/Spawning/Metadata/BeaconMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/BeaconMetadataProcessor.cs index bc124430e0..53d71302d2 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/BeaconMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/BeaconMetadataProcessor.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SeaTreaderMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SeaTreaderMetadataExtractor.cs index d082bfeffe..9ea502f784 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SeaTreaderMetadataExtractor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SeaTreaderMetadataExtractor.cs @@ -1,5 +1,4 @@ using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SubNameInputMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SubNameInputMetadataExtractor.cs index c15dd334e6..a348a94b3b 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SubNameInputMetadataExtractor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/SubNameInputMetadataExtractor.cs @@ -1,7 +1,6 @@ using System.Linq; using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/ConstructorMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/ConstructorMetadataProcessor.cs index 69bd3f9e88..54ee919103 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/ConstructorMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/ConstructorMetadataProcessor.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CrafterMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CrafterMetadataProcessor.cs index 32c3685ea4..020cca9874 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CrafterMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CrafterMetadataProcessor.cs @@ -1,5 +1,4 @@ using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsLightingMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsLightingMetadataProcessor.cs index 02324b8e34..29ac77df5d 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsLightingMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsLightingMetadataProcessor.cs @@ -1,7 +1,6 @@ using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs index 07315b0082..ab8c855855 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/CyclopsMetadataProcessor.cs @@ -1,7 +1,6 @@ using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/ExosuitMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/ExosuitMetadataProcessor.cs index b57cb5dc7c..6133289d8a 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/ExosuitMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/ExosuitMetadataProcessor.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs index 8391f2afb2..02c8a98c2f 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/FruitPlantMetadataProcessor.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlayerMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlayerMetadataProcessor.cs index e152c7bbaa..569ac7bb32 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlayerMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PlayerMetadataProcessor.cs @@ -10,7 +10,7 @@ namespace NitroxClient.GameLogic.Spawning.Metadata.Processor; public class PlayerMetadataProcessor : EntityMetadataProcessor { - private NitroxId localPlayerId = null; + private NitroxId? localPlayerId; public override void ProcessMetadata(GameObject gameObject, PlayerMetadata metadata) { if (!gameObject.TryGetIdOrWarn(out NitroxId id)) diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/RadiationMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/RadiationMetadataProcessor.cs index 306983770e..f4bd378e07 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/RadiationMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/RadiationMetadataProcessor.cs @@ -1,6 +1,5 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/RocketMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/RocketMetadataProcessor.cs index f29276ac46..7ce4838bd5 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/RocketMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/RocketMetadataProcessor.cs @@ -2,7 +2,6 @@ using System.Linq; using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeaTreaderMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeaTreaderMetadataProcessor.cs index a8e3c0a3bc..c275800f69 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeaTreaderMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeaTreaderMetadataProcessor.cs @@ -1,6 +1,5 @@ using System; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeamothMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeamothMetadataProcessor.cs index 112d912aa5..028fceb375 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeamothMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SeamothMetadataProcessor.cs @@ -1,7 +1,6 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.FMOD; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/StayAtLeashPositionMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/StayAtLeashPositionMetadataProcessor.cs index 1983db5923..da19d94adb 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/StayAtLeashPositionMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/StayAtLeashPositionMetadataProcessor.cs @@ -1,5 +1,4 @@ using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SubNameInputMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SubNameInputMetadataProcessor.cs index 681b9d972a..fce6374e55 100644 --- a/NitroxClient/GameLogic/Spawning/Metadata/Processor/SubNameInputMetadataProcessor.cs +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/SubNameInputMetadataProcessor.cs @@ -2,8 +2,6 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/PrefabChildEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/PrefabChildEntitySpawner.cs index 26b90d38c6..abbc2c8d5d 100644 --- a/NitroxClient/GameLogic/Spawning/PrefabChildEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/PrefabChildEntitySpawner.cs @@ -20,11 +20,11 @@ protected override IEnumerator SpawnAsync(PrefabChildEntity entity, TaskResult> result) { GameObject parent = NitroxEntity.RequireObjectFrom(entity.ParentId); - PrefabIdentifier prefab = parent.GetAllComponentsInChildren() + PrefabIdentifier? prefab = parent.GetAllComponentsInChildren() .Where(prefab => prefab.classId == entity.ClassId) - .ElementAt(entity.ComponentIndex); + .ElementAtOrDefault(entity.ComponentIndex); - if (prefab) + if (prefab != null) { NitroxEntity.SetNewId(prefab.gameObject, entity.Id); result.Set(Optional.OfNullable(prefab.gameObject)); diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/CrashEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/CrashEntitySpawner.cs index f4ec9485b6..bf7716c205 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/CrashEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/CrashEntitySpawner.cs @@ -1,7 +1,6 @@ using System.Collections; using Nitrox.Model.DataStructures; using NitroxClient.GameLogic.Spawning.Metadata.Processor; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/CreatureRespawnEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/CreatureRespawnEntitySpawner.cs index 9a2bd5701a..0a18ecbe42 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/CreatureRespawnEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/CreatureRespawnEntitySpawner.cs @@ -2,7 +2,6 @@ using NitroxClient.GameLogic.Simulation; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/DefaultWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/DefaultWorldEntitySpawner.cs index 6289053d9a..bf54441218 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/DefaultWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/DefaultWorldEntitySpawner.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Collections.Generic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; using UWE; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/GeyserWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/GeyserWorldEntitySpawner.cs index 68e4f6fd5d..c641b3290d 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/GeyserWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/GeyserWorldEntitySpawner.cs @@ -1,7 +1,6 @@ using System.Collections; using Nitrox.Model.DataStructures; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; using UWE; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/GlobalRootEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/GlobalRootEntitySpawner.cs index 576e411022..8615ff0dba 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/GlobalRootEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/GlobalRootEntitySpawner.cs @@ -3,8 +3,6 @@ using NitroxClient.Communication; using NitroxClient.GameLogic.Spawning.Abstract; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PlacedWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PlacedWorldEntitySpawner.cs index 256af95d0f..72a86520b1 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PlacedWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PlacedWorldEntitySpawner.cs @@ -1,20 +1,14 @@ using System.Collections; using Nitrox.Model.DataStructures; using NitroxClient.GameLogic.Spawning.Abstract; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; namespace NitroxClient.GameLogic.Spawning.WorldEntities; -public class PlacedWorldEntitySpawner : SyncEntitySpawner +internal sealed class PlacedWorldEntitySpawner(WorldEntitySpawner worldEntitySpawner) : SyncEntitySpawner { - private readonly WorldEntitySpawner worldEntitySpawner; - - public PlacedWorldEntitySpawner(WorldEntitySpawner worldEntitySpawner) - { - this.worldEntitySpawner = worldEntitySpawner; - } + private readonly WorldEntitySpawner worldEntitySpawner = worldEntitySpawner; protected override IEnumerator SpawnAsync(PlacedWorldEntity entity, TaskResult> result) { diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs index ed616b3662..81fe5e5c02 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using NitroxClient.GameLogic.Spawning.Metadata; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs index 9285c1e000..025fefeefc 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PrefabPlaceholderEntitySpawner.cs @@ -1,6 +1,5 @@ using System.Collections; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackChildEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackChildEntitySpawner.cs index 88d8c5ae5a..f3cce86616 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackChildEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackChildEntitySpawner.cs @@ -1,7 +1,6 @@ using System.Collections; using Nitrox.Model.DataStructures; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackEntitySpawner.cs index 53d36f94de..f1d01f2295 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/ReefbackEntitySpawner.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using Nitrox.Model.DataStructures; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs index bb5848e55e..44ec0179b6 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/SerializedWorldEntitySpawner.cs @@ -5,8 +5,6 @@ using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Helper; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; using UWE; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleEntitySpawner.cs index 1346f56bf8..7b02036d93 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/VehicleEntitySpawner.cs @@ -5,7 +5,6 @@ using NitroxClient.MonoBehaviours.CinematicController; using NitroxClient.Unity.Helper; using Nitrox.Model.Helper; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using UnityEngine; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs index 3b7498a4e4..b9db639949 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using NitroxClient.GameLogic.Spawning.Metadata; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; namespace NitroxClient.GameLogic.Spawning.WorldEntities; @@ -19,7 +18,7 @@ public class WorldEntitySpawnerResolver private readonly Dictionary customSpawnersByTechType = new(); - public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, Entities entities, SimulationOwnership simulationOwnership) + public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, Entities entities, SimulationOwnership simulationOwnership) { customSpawnersByTechType[TechType.Crash] = new CrashEntitySpawner(); customSpawnersByTechType[TechType.Creepvine] = new CreepvineEntitySpawner(defaultEntitySpawner); diff --git a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs index ce9dcc99f6..20f9f54773 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntitySpawner.cs @@ -7,36 +7,31 @@ using NitroxClient.GameLogic.Spawning.Metadata; using NitroxClient.GameLogic.Spawning.WorldEntities; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Helper; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; namespace NitroxClient.GameLogic.Spawning; -public class WorldEntitySpawner : SyncEntitySpawner +internal sealed class WorldEntitySpawner(EntityMetadataManager entityMetadataManager, Entities entities, SimulationOwnership simulationOwnership) : SyncEntitySpawner { - private readonly WorldEntitySpawnerResolver worldEntitySpawnResolver; - private readonly Dictionary batchCellsById; - - public WorldEntitySpawner(EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, Entities entities, SimulationOwnership simulationOwnership) + private readonly WorldEntitySpawnerResolver worldEntitySpawnResolver = new(entityMetadataManager, entities, simulationOwnership); + private readonly Lazy> batchCellsById = new(() => { - worldEntitySpawnResolver = new WorldEntitySpawnerResolver(entityMetadataManager, playerManager, localPlayer, entities, simulationOwnership); - if (NitroxEnvironment.IsNormal) { - batchCellsById = (Dictionary)LargeWorldStreamer.main.cellManager.batch2cells; + return (Dictionary)LargeWorldStreamer.main.cellManager.batch2cells; } - } + return []; + }); - protected override IEnumerator SpawnAsync(WorldEntity entity, TaskResult> result) + protected override IEnumerator? SpawnAsync(WorldEntity entity, TaskResult> result) { bool foundParentCell = TryFindAwakeParentCell(entity, out EntityCell parentCell); if (foundParentCell) { parentCell.EnsureRoot(); } - Optional parent = (entity.ParentId != null) ? NitroxEntity.GetObjectFrom(entity.ParentId) : Optional.Empty; + Optional parent = entity.ParentId != null ? NitroxEntity.GetObjectFrom(entity.ParentId) : Optional.Empty; // No place to spawn the entity if (!foundParentCell && !parent.HasValue) @@ -86,7 +81,7 @@ public bool TryFindAwakeParentCell(WorldEntity entity, out EntityCell parentCell Int3 batchId = entity.AbsoluteEntityCell.BatchId.ToUnity(); Int3 cellId = entity.AbsoluteEntityCell.CellId.ToUnity(); - if (batchCellsById.TryGetValue(batchId, out BatchCells batchCells)) + if (batchCellsById.Value.TryGetValue(batchId, out BatchCells batchCells)) { parentCell = batchCells.Get(cellId, entity.Level); // in both states, the cell is awake @@ -105,7 +100,7 @@ public EntityCell EnsureCell(WorldEntity entity) Int3 batchId = entity.AbsoluteEntityCell.BatchId.ToUnity(); Int3 cellId = entity.AbsoluteEntityCell.CellId.ToUnity(); - if (!batchCellsById.TryGetValue(batchId, out BatchCells batchCells)) + if (!batchCellsById.Value.TryGetValue(batchId, out BatchCells batchCells)) { batchCells = LargeWorldStreamer.main.cellManager.InitializeBatchCells(batchId); } diff --git a/NitroxClient/GameLogic/Terrain.cs b/NitroxClient/GameLogic/Terrain.cs index 9c1de29ee3..d68cc6847a 100644 --- a/NitroxClient/GameLogic/Terrain.cs +++ b/NitroxClient/GameLogic/Terrain.cs @@ -1,8 +1,6 @@ using System.Collections; using System.Collections.Generic; using NitroxClient.Communication.Abstract; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic; using Nitrox.Model.Subnautica.Packets; using UnityEngine; @@ -77,7 +75,7 @@ public void UpdateVisibility() { if (cellsPendingSync) { - CellVisibilityChanged cellsChanged = new(multiplayerSession.Reservation.PlayerId, addedCells, removedCells); + CellVisibilityChanged cellsChanged = new(multiplayerSession.Reservation.SessionId, addedCells, removedCells); packetSender.Send(cellsChanged); addedCells.Clear(); diff --git a/NitroxClient/GameLogic/TimeManager.cs b/NitroxClient/GameLogic/TimeManager.cs index 3d94707863..d830fb8783 100644 --- a/NitroxClient/GameLogic/TimeManager.cs +++ b/NitroxClient/GameLogic/TimeManager.cs @@ -1,7 +1,6 @@ using System; using NitroxClient.MonoBehaviours; using Nitrox.Model.Networking; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxClient/GameLogic/Vehicles.cs b/NitroxClient/GameLogic/Vehicles.cs index 31b8e15425..119e3b9f9b 100644 --- a/NitroxClient/GameLogic/Vehicles.cs +++ b/NitroxClient/GameLogic/Vehicles.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using Nitrox.Model.Core; using NitroxClient.Communication; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.Helper; @@ -8,8 +9,6 @@ using NitroxClient.MonoBehaviours; using NitroxClient.Unity.Helper; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; using Nitrox.Model.Subnautica.Packets; @@ -110,14 +109,14 @@ public void BroadcastOnPilotModeChanged(GameObject gameObject, bool isPiloting) { if (gameObject.TryGetIdOrWarn(out NitroxId vehicleId)) { - VehicleOnPilotModeChanged packet = new(vehicleId, multiplayerSession.Reservation.PlayerId, isPiloting); + VehicleOnPilotModeChanged packet = new(vehicleId, multiplayerSession.Reservation.SessionId, isPiloting); packetSender.Send(packet); } } - public void SetOnPilotMode(GameObject gameObject, ushort playerId, bool isPiloting) + public void SetOnPilotMode(GameObject gameObject, SessionId sessionId, bool isPiloting) { - if (playerManager.TryFind(playerId, out RemotePlayer remotePlayer)) + if (playerManager.TryFind(sessionId, out RemotePlayer remotePlayer)) { if (gameObject.TryGetComponent(out Vehicle vehicle)) { @@ -137,11 +136,11 @@ public void SetOnPilotMode(GameObject gameObject, ushort playerId, bool isPiloti } } - public void SetOnPilotMode(NitroxId vehicleId, ushort playerId, bool isPiloting) + public void SetOnPilotMode(NitroxId vehicleId, SessionId sessionId, bool isPiloting) { if (NitroxEntity.TryGetObjectFrom(vehicleId, out GameObject vehicleObject)) { - SetOnPilotMode(vehicleObject, playerId, isPiloting); + SetOnPilotMode(vehicleObject, sessionId, isPiloting); } } diff --git a/NitroxClient/GlobalUsings.cs b/NitroxClient/GlobalUsings.cs index abcac8af1c..6e08d8f01b 100644 --- a/NitroxClient/GlobalUsings.cs +++ b/NitroxClient/GlobalUsings.cs @@ -3,3 +3,4 @@ global using Nitrox.Model.Extensions; global using Nitrox.Model.Logger; global using Nitrox.Model.Subnautica.Extensions; +global using Task = System.Threading.Tasks.Task; diff --git a/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicController.cs b/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicController.cs index 6d03d5ea3f..82641ab4ea 100644 --- a/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicController.cs +++ b/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Nitrox.Model.Core; using NitroxClient.GameLogic; using UnityEngine; @@ -6,7 +7,7 @@ namespace NitroxClient.MonoBehaviours.CinematicController; public class MultiplayerCinematicController : MonoBehaviour { - private readonly Dictionary controllerByPlayerId = new(); + private readonly Dictionary controllerBySessionId = new(); /// /// MCCs with the same Animator to reset state if needed. @@ -34,14 +35,14 @@ public void CallCinematicModeEnd(RemotePlayer player) public void CallAllCinematicModeEnd() { - foreach (RemotePlayerCinematicController remoteController in controllerByPlayerId.Values) + foreach (RemotePlayerCinematicController remoteController in controllerBySessionId.Values) { remoteController.EndCinematicMode(true); } foreach (MultiplayerCinematicController controller in multiplayerControllerSameAnimator) { - foreach (RemotePlayerCinematicController remoteController in controller.controllerByPlayerId.Values) + foreach (RemotePlayerCinematicController remoteController in controller.controllerBySessionId.Values) { remoteController.EndCinematicMode(true); } @@ -50,7 +51,7 @@ public void CallAllCinematicModeEnd() private RemotePlayerCinematicController GetController(RemotePlayer player) { - if (controllerByPlayerId.TryGetValue(player.PlayerId, out RemotePlayerCinematicController controller)) + if (controllerBySessionId.TryGetValue(player.SessionId, out RemotePlayerCinematicController controller)) { return controller; } @@ -58,16 +59,16 @@ private RemotePlayerCinematicController GetController(RemotePlayer player) player.PlayerDisconnectEvent.AddHandler(gameObject, OnPlayerDisconnect); controller = CreateNewControllerForPlayer(); - controllerByPlayerId.Add(player.PlayerId, controller); + controllerBySessionId.Add(player.SessionId, controller); return controller; } public void OnPlayerDisconnect(RemotePlayer player) { - if (controllerByPlayerId.TryGetValue(player.PlayerId, out RemotePlayerCinematicController controller)) + if (controllerBySessionId.TryGetValue(player.SessionId, out RemotePlayerCinematicController controller)) { Destroy(controller); - controllerByPlayerId.Remove(player.PlayerId); + controllerBySessionId.Remove(player.SessionId); } } diff --git a/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicReference.cs b/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicReference.cs index 505d5ab455..5a17dce18d 100644 --- a/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicReference.cs +++ b/NitroxClient/MonoBehaviours/CinematicController/MultiplayerCinematicReference.cs @@ -8,7 +8,7 @@ namespace NitroxClient.MonoBehaviours.CinematicController; public class MultiplayerCinematicReference : MonoBehaviour { - private readonly Dictionary> controllerByKey = new(); + private readonly Dictionary> controllerByKey = []; private bool isEscapePod; @@ -52,7 +52,7 @@ public void CallCinematicModeEnd(string key, int identifier, RemotePlayer player controller.CallCinematicModeEnd(player); } - public static int GetCinematicControllerIdentifier(GameObject controller, GameObject reference) => controller.gameObject.GetHierarchyPath(reference).GetHashCode(); + private static int GetCinematicControllerIdentifier(GameObject controller, GameObject reference) => controller.gameObject.GetHierarchyPath(reference).GetHashCode(); public void AddController(PlayerCinematicController playerController) { @@ -73,7 +73,10 @@ public void AddController(PlayerCinematicController playerController) MultiplayerCinematicController controller = MultiplayerCinematicController.Initialize(playerController); controller.AddOtherControllers(allControllers); - allControllers.ForEach(x => x.AddOtherControllers(new[] { controller })); + foreach (MultiplayerCinematicController? x in allControllers) + { + x.AddOtherControllers([controller]); + } controllers.Add(identifier, controller); } diff --git a/NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs b/NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs index 926c143afa..02896d72a7 100644 --- a/NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs +++ b/NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs @@ -14,11 +14,9 @@ public class NitroxCyclops : MonoBehaviour public VirtualCyclops Virtual; private CyclopsMotor cyclopsMotor; private SubRoot subRoot; - private SubControl subControl; private Rigidbody rigidbody; private WorldForces worldForces; private Stabilizer stabilizer; - private CharacterController controller; private CyclopsNoiseManager cyclopsNoiseManager; public readonly Dictionary Pawns = []; @@ -29,11 +27,9 @@ public void Start() { cyclopsMotor = Player.mainObject.GetComponent(); subRoot = GetComponent(); - subControl = GetComponent(); rigidbody = GetComponent(); worldForces = GetComponent(); stabilizer = GetComponent(); - controller = cyclopsMotor.controller; cyclopsNoiseManager = GetComponent(); UWE.Utils.SetIsKinematicAndUpdateInterpolation(rigidbody, false, true); diff --git a/NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs b/NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs index c34ba0451b..89f3f37d36 100644 --- a/NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs +++ b/NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs @@ -2,7 +2,11 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; +using Nitrox.Model.Packets; +using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic.ChatUI; +using NitroxClient.Unity.Helper; using UnityEngine; using UnityEngine.UI; @@ -13,23 +17,76 @@ public class PlayerChat : uGUI_InputGroup private const int LINE_CHAR_LIMIT = 255; private const int MESSAGES_LIMIT = 64; private const float TOGGLED_TRANSPARENCY = 0.4f; - public const float CHAT_VISIBILITY_TIME_LENGTH = 6f; private static readonly Queue entries = []; private Image[] backgroundImages; private CanvasGroup canvasGroup; - private InputField inputField; + private Coroutine? fadeCoroutine; + private InputField inputField = null!; + private Text backgroundText = null!; private GameObject logEntryPrefab; + private string lastInputText = ""; private bool transparent; - private Coroutine fadeCoroutine; public static bool IsReady { get; private set; } public string InputText { get => inputField.text; - set => inputField.text = value; + set + { + inputField.text = value; + inputField.caretPosition = value.Length; + } + } + + public string AutoCompleteText + { + get => backgroundText.text; + set + { + if (value.Length <= inputField.text.Length) + { + value = ""; + } + backgroundText.text = value; + } + } + + public override void Update() + { + base.Update(); + if (!focused) + { + return; + } + if (string.IsNullOrWhiteSpace(InputText)) + { + AutoCompleteText = ""; + return; + } + bool hasInputChanged = InputText != lastInputText; + lastInputText = InputText; + + // Handle command auto complete. + if (InputText[0] == PlayerChatManager.SERVER_COMMAND_PREFIX) + { + // Auto complete command names. + if (hasInputChanged && Regex.IsMatch(InputText, @"^/\w+$")) + { + string commandName = InputText.Substring(1); + this.Resolve().Send(new TextAutoComplete(commandName, TextAutoComplete.AutoCompleteContext.COMMAND_NAME)); + } + if (!string.IsNullOrWhiteSpace(AutoCompleteText)) + { + if (UnityEngine.Input.GetKeyDown(KeyCode.Tab) || (UnityEngine.Input.GetKeyDown(KeyCode.RightArrow) && inputField.caretPosition == InputText.Length)) + { + InputText = AutoCompleteText; + AutoCompleteText = ""; + } + } + } } public IEnumerator SetupChatComponents() @@ -44,23 +101,33 @@ public IEnumerator SetupChatComponents() GetComponentsInChildren -public sealed partial class SubRoot_OnTakeDamage_Patch : NitroxPatch, IDynamicPatch +internal sealed partial class SubRoot_OnTakeDamage_Patch : NitroxPatch, IDynamicPatch { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SubRoot t) => t.OnTakeDamage(default)); diff --git a/NitroxPatcher/Patches/Dynamic/Survival_Eat_Patch.cs b/NitroxPatcher/Patches/Dynamic/Survival_Eat_Patch.cs index 325b8f7db4..d7bcd15821 100644 --- a/NitroxPatcher/Patches/Dynamic/Survival_Eat_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Survival_Eat_Patch.cs @@ -1,7 +1,6 @@ using System.Reflection; using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxPatcher/Patches/Dynamic/Survival_Use_Patch.cs b/NitroxPatcher/Patches/Dynamic/Survival_Use_Patch.cs index 6e07909641..aed2207b8b 100644 --- a/NitroxPatcher/Patches/Dynamic/Survival_Use_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Survival_Use_Patch.cs @@ -1,7 +1,6 @@ using System.Reflection; using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxPatcher/Patches/Dynamic/TimeCapsule_Open_Patch.cs b/NitroxPatcher/Patches/Dynamic/TimeCapsule_Open_Patch.cs index 405d25b156..f02b88945d 100644 --- a/NitroxPatcher/Patches/Dynamic/TimeCapsule_Open_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/TimeCapsule_Open_Patch.cs @@ -1,7 +1,6 @@ using System.Reflection; using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxPatcher.Patches.Dynamic; diff --git a/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs b/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs index 88e247d2bf..4cdd18ae23 100644 --- a/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Trashcan_Update_Patch.cs @@ -5,7 +5,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxPatcher/Patches/Dynamic/Utils_PlayFMODAsset_Patch.cs b/NitroxPatcher/Patches/Dynamic/Utils_PlayFMODAsset_Patch.cs index ad401f0c7a..3c4c28398d 100644 --- a/NitroxPatcher/Patches/Dynamic/Utils_PlayFMODAsset_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Utils_PlayFMODAsset_Patch.cs @@ -1,6 +1,5 @@ using System.Reflection; using NitroxClient.GameLogic.FMOD; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.GameLogic.FMOD; namespace NitroxPatcher.Patches.Dynamic; diff --git a/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnTriggerEnter.cs b/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnTriggerEnter.cs index 1393c42837..109fe8ea2c 100644 --- a/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnTriggerEnter.cs +++ b/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnTriggerEnter.cs @@ -2,7 +2,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; using UnityEngine; @@ -34,7 +33,7 @@ public static void Postfix(VehicleDockingBay __instance, ref Vehicle __state) Resolve().HasAnyLockType(vehicleId)) { Vehicles.EngagePlayerMovementSuppressor(interpolatingVehicle); - Resolve().Send(new VehicleDocking(vehicleId, dockId, Resolve().Reservation.PlayerId)); + Resolve().Send(new VehicleDocking(vehicleId, dockId, Resolve().Reservation.SessionId)); } } } diff --git a/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnUndockingComplete_Patch.cs b/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnUndockingComplete_Patch.cs index 651156390e..797e81cc5f 100644 --- a/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnUndockingComplete_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/VehicleDockingBay_OnUndockingComplete_Patch.cs @@ -1,7 +1,6 @@ using System.Reflection; using NitroxClient.Communication.Abstract; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxPatcher.Patches.Dynamic; @@ -18,6 +17,6 @@ public static void Prefix(VehicleDockingBay __instance) return; } - Resolve().Send(new VehicleUndocking(vehicleId, dockId, Resolve().Reservation.PlayerId, false)); + Resolve().Send(new VehicleUndocking(vehicleId, dockId, Resolve().Reservation.SessionId, false)); } } diff --git a/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs b/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs index 2557c3419f..332fb448b4 100644 --- a/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Vehicle_TorpedoShot_Patch.cs @@ -6,8 +6,6 @@ using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; using Nitrox.Model.DataStructures.Unity; -using Nitrox.Model.Packets; -using Nitrox.Model.Subnautica.DataStructures; using Nitrox.Model.Subnautica.Packets; using UnityEngine; diff --git a/NitroxPatcher/Patches/Dynamic/WaterParkCreature_ManagedUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/WaterParkCreature_ManagedUpdate_Patch.cs index d142e5480b..2d7c4918cd 100644 --- a/NitroxPatcher/Patches/Dynamic/WaterParkCreature_ManagedUpdate_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/WaterParkCreature_ManagedUpdate_Patch.cs @@ -5,7 +5,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxPatcher.Patches.Dynamic; diff --git a/NitroxPatcher/Patches/Dynamic/WaterParkItem_ManagedUpdate_Patch.cs b/NitroxPatcher/Patches/Dynamic/WaterParkItem_ManagedUpdate_Patch.cs index 55a498f0f5..d9033c3413 100644 --- a/NitroxPatcher/Patches/Dynamic/WaterParkItem_ManagedUpdate_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/WaterParkItem_ManagedUpdate_Patch.cs @@ -2,7 +2,6 @@ using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; using Nitrox.Model.DataStructures; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxPatcher.Patches.Dynamic; diff --git a/NitroxPatcher/Patches/Dynamic/uGUI_ColorPicker_Awake_Patch.cs b/NitroxPatcher/Patches/Dynamic/uGUI_ColorPicker_Awake_Patch.cs index c7f8f80211..fec08020f9 100644 --- a/NitroxPatcher/Patches/Dynamic/uGUI_ColorPicker_Awake_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/uGUI_ColorPicker_Awake_Patch.cs @@ -1,5 +1,4 @@ using System.Reflection; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxPatcher.Patches.Dynamic; diff --git a/NitroxPatcher/Patches/Dynamic/uGUI_SceneIntro_IntroSequence_Patch.cs b/NitroxPatcher/Patches/Dynamic/uGUI_SceneIntro_IntroSequence_Patch.cs index 8c4fc12da9..09d585535e 100644 --- a/NitroxPatcher/Patches/Dynamic/uGUI_SceneIntro_IntroSequence_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/uGUI_SceneIntro_IntroSequence_Patch.cs @@ -139,7 +139,7 @@ private static bool IsRemoteCinematicReady(uGUI_SceneIntro uGuiSceneIntro) return false; } - ushort? opPartnerId = Resolve().IntroCinematicPartnerId; + SessionId? opPartnerId = Resolve().IntroCinematicPartnerId; if (Resolve().IntroCinematicMode == IntroCinematicMode.START && opPartnerId.HasValue && Resolve().TryFind(opPartnerId.Value, out RemotePlayer newPartner)) @@ -198,7 +198,7 @@ private static void EndRemoteCinematic() Resolve().SetLocalIntroCinematicMode(IntroCinematicMode.COMPLETED); } - private static bool IsPartnerValid() => partner != null && Resolve().Find(partner.PlayerId).HasValue; + private static bool IsPartnerValid() => partner != null && Resolve().Find(partner.SessionId).HasValue; public static void SkipLocalCinematic(uGUI_SceneIntro uGuiSceneIntro, bool wasNewPlayer) { diff --git a/NitroxPatcher/Patches/Persistent/uGUI_MainMenu_Start_Patch.cs b/NitroxPatcher/Patches/Persistent/uGUI_MainMenu_Start_Patch.cs index 95338a1389..b444685e36 100644 --- a/NitroxPatcher/Patches/Persistent/uGUI_MainMenu_Start_Patch.cs +++ b/NitroxPatcher/Patches/Persistent/uGUI_MainMenu_Start_Patch.cs @@ -88,7 +88,7 @@ private static void SessionConnectionStateChangedHandler(IMultiplayerSessionConn } NitroxColor playerColor = new(1,1,1); - byte[] nameHash = playerName.AsMd5Hash(); + byte[] nameHash = playerName.ToMd5Hash(); if (nameHash.Length >= 8) { float hue = BitConverter.ToUInt64([nameHash[0], nameHash[1], nameHash[2], nameHash[3], nameHash[4], nameHash[5], nameHash[6], nameHash[7]], 0) / (float)ulong.MaxValue; From d9be49c846aab4e791a87526cf3c3bc334fc8082 Mon Sep 17 00:00:00 2001 From: MrBub <106004257+misterbubb@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:17:23 -0500 Subject: [PATCH 11/59] Fix Sea Emperor hatching sequence not completing in multiplayer (#2653) --- ...rEggAnimation_OnHatchAnimationEnd_Patch.cs | 43 +++++++++++ .../Dynamic/IncubatorEgg_HatchNow_Patch.cs | 72 ++++++++++++------- .../Dynamic/SeaEmperorBaby_Start_Patch.cs | 54 ++++++++++++++ 3 files changed, 142 insertions(+), 27 deletions(-) create mode 100644 NitroxPatcher/Patches/Dynamic/IncubatorEggAnimation_OnHatchAnimationEnd_Patch.cs create mode 100644 NitroxPatcher/Patches/Dynamic/SeaEmperorBaby_Start_Patch.cs diff --git a/NitroxPatcher/Patches/Dynamic/IncubatorEggAnimation_OnHatchAnimationEnd_Patch.cs b/NitroxPatcher/Patches/Dynamic/IncubatorEggAnimation_OnHatchAnimationEnd_Patch.cs new file mode 100644 index 0000000000..76f65be071 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/IncubatorEggAnimation_OnHatchAnimationEnd_Patch.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using UnityEngine; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Handles cleanup of temporary babies created for animation on non-simulating players. +/// Only the real networked baby (from simulating player) should call SwimToMother(). +/// Temporary babies are destroyed after the animation completes. +/// +public sealed partial class IncubatorEggAnimation_OnHatchAnimationEnd_Patch : NitroxPatch, IDynamicPatch +{ + internal const string TEMPORARY_BABY_MARKER = "_NitroxTemporary"; + + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((IncubatorEggAnimation t) => t.OnHatchAnimationEnd()); + + public static bool Prefix(IncubatorEggAnimation __instance) + { + // Safety check - ensure we have a baby reference + if (!__instance.baby) + { + return true; // Let original method handle this case + } + + // Check if this is a temporary baby (created for animation only) + if (__instance.baby.name.Contains(TEMPORARY_BABY_MARKER)) + { + // For temporary babies, we only want to end the cinematic mode and cleanup + // Don't call SwimToMother() as that should only happen for the real networked baby + __instance.baby.cinematicController.SetCinematicMode(false); + + // Set animationActive to false (field is publicized via BepInEx.AssemblyPublicizer) + __instance.animationActive = false; + + // Destroy the temporary baby - the real networked one will be spawned via server broadcast + Object.Destroy(__instance.baby.gameObject); + return false; // Skip the original method + } + + // For real networked babies, let the original method run (which calls SwimToMother) + return true; + } +} \ No newline at end of file diff --git a/NitroxPatcher/Patches/Dynamic/IncubatorEgg_HatchNow_Patch.cs b/NitroxPatcher/Patches/Dynamic/IncubatorEgg_HatchNow_Patch.cs index 20b31f4b5d..261695ff43 100644 --- a/NitroxPatcher/Patches/Dynamic/IncubatorEgg_HatchNow_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/IncubatorEgg_HatchNow_Patch.cs @@ -1,52 +1,70 @@ using System.Reflection; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; -using Nitrox.Model.Core; using Nitrox.Model.DataStructures; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using UnityEngine; namespace NitroxPatcher.Patches.Dynamic; +/// +/// Handles the Sea Emperor baby hatching sequence in multiplayer. +/// All players see the full hatching animation for better visual consistency. +/// The simulating player spawns the real networked baby that will call SwimToMother(). +/// Non-simulating players create temporary babies just for the animation sequence. +/// public sealed partial class IncubatorEgg_HatchNow_Patch : NitroxPatch, IDynamicPatch { private static readonly MethodInfo TARGET_METHOD = Reflect.Method((IncubatorEgg t) => t.HatchNow()); public static bool Prefix(IncubatorEgg __instance) { - StartHatchingVisuals(__instance); + // Play hatching visual/sound effects for all players + __instance.fxControl.Play(); + Utils.PlayFMODAsset(__instance.hatchSound, __instance.transform, 30f); - SpawnBabiesIfSimulating(__instance); - - return false; - } - - private static void StartHatchingVisuals(IncubatorEgg egg) - { - egg.fxControl.Play(); - Utils.PlayFMODAsset(egg.hatchSound, egg.transform, 30f); + NitroxEntity serverKnownParent = __instance.GetComponentInParent(); + if (!serverKnownParent) + { + Log.Error("Could not find a server known parent for incubator egg"); + return false; + } - SafeAnimator.SetBool(egg.animationController.eggAnimator, egg.animParameter, true); - } + bool isSimulating = Resolve().HasAnyLockType(serverKnownParent.Id); - private static void SpawnBabiesIfSimulating(IncubatorEgg egg) - { - SimulationOwnership simulationOwnership = NitroxServiceLocator.LocateService(); + // Create baby GameObject for animation (networked for simulating player, temporary for others) + GameObject baby = Object.Instantiate(__instance.seaEmperorBabyPrefab); + baby.transform.SetParent(__instance.attachPoint); + baby.transform.localPosition = Vector3.zero; + baby.transform.localRotation = Quaternion.identity; - NitroxEntity serverKnownParent = egg.GetComponentInParent(); - Validate.NotNull(serverKnownParent, "Could not find a server known parent for incubator egg"); - - // Only spawn the babies if we are simulating the main incubator platform. - if (simulationOwnership.HasAnyLockType(serverKnownParent.Id)) + if (isSimulating) { - GameObject baby = UnityEngine.Object.Instantiate(egg.seaEmperorBabyPrefab); - baby.transform.position = egg.attachPoint.transform.position; - baby.transform.localRotation = Quaternion.identity; - + // Simulating player: make this baby networked and broadcast it NitroxId babyId = NitroxEntity.GenerateNewId(baby); - - WorldEntity entity = new(baby.transform.position.ToDto(), baby.transform.rotation.ToDto(), baby.transform.localScale.ToDto(), TechType.SeaEmperorBaby.ToDto(), 3, "09883a6c-9e78-4bbf-9561-9fa6e49ce766", false, babyId, null); + WorldEntity entity = new( + baby.transform.position.ToDto(), + baby.transform.rotation.ToDto(), + baby.transform.localScale.ToDto(), + TechType.SeaEmperorBaby.ToDto(), + 3, + "09883a6c-9e78-4bbf-9561-9fa6e49ce766", + false, + babyId, + null); Resolve().BroadcastEntitySpawnedByClient(entity); } + else + { + // Non-simulating player: mark baby as temporary (will be cleaned up after animation) + baby.name += IncubatorEggAnimation_OnHatchAnimationEnd_Patch.TEMPORARY_BABY_MARKER; + } + + // All players run the full animation sequence + __instance.babyGO = baby; + __instance.animationController.StartHatchAnimation(__instance.babyIdentifier, __instance.animParameter, baby); + __instance.Invoke("PlayFxOnBaby", 2f); + + return false; } } diff --git a/NitroxPatcher/Patches/Dynamic/SeaEmperorBaby_Start_Patch.cs b/NitroxPatcher/Patches/Dynamic/SeaEmperorBaby_Start_Patch.cs new file mode 100644 index 0000000000..63c9fef89e --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/SeaEmperorBaby_Start_Patch.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Reflection; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// When a Sea Emperor baby is spawned on a remote client (via server broadcast), +/// it needs to swim to mother. The simulating player handles this via the egg animation +/// callback, but remote players receive the baby as a standalone entity. +/// +/// This patch ensures the baby swims to mother if: +/// 1. SeaEmperor.main exists (we're in the prison aquarium) +/// 2. The baby has no parent (not attached to an egg - means it's a remote spawn) +/// 3. The baby doesn't already have a swim target (not already swimming) +/// 4. The baby is not a temporary animation baby +/// +public sealed partial class SeaEmperorBaby_Start_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SeaEmperorBaby t) => t.Start()); + + public static void Postfix(SeaEmperorBaby __instance) + { + // Only trigger swim to mother for remotely spawned babies + if (!SeaEmperor.main) + { + return; + } + + // Skip temporary babies created for animation + if (__instance.name.Contains(IncubatorEggAnimation_OnHatchAnimationEnd_Patch.TEMPORARY_BABY_MARKER)) + { + return; + } + + // If the baby has a parent, it was spawned by the local hatching sequence + // The animation system will handle calling SwimToMother() via OnHatchAnimationEnd() + if (__instance.transform.parent != null) + { + return; + } + + // Check if this baby already has a swim target (already being handled) + if (__instance.swimToTarget != null && __instance.swimToTarget.target != null) + { + return; + } + + // This is a remotely spawned baby - make it swim to mother + // Assign a baby ID based on how many babies are already registered + int babyId = SeaEmperor.main.GetBabies().Count(); + __instance.SetId(babyId); + __instance.SwimToMother(); + } +} From 2ee1972fbd5b4096473b3a93450acc792a56852f Mon Sep 17 00:00:00 2001 From: Meas <1107063+Measurity@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:05:08 +0100 Subject: [PATCH 12/59] regression: server sleep (aka no players connected) not causing a save (#2669) --- .../Models/AppEvents/ISessionCleaner.cs | 2 +- .../Models/Commands/Core/CommandRegistry.cs | 4 ++++ .../Models/GameLogic/SleepManager.cs | 4 ++-- .../Services/HibernateService.cs | 13 +++++++++++-- .../Services/SaveService.cs | 18 ++++++++---------- .../ConnectionState/Disconnected.cs | 2 -- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs b/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs index 84a035417e..de7c08b9ca 100644 --- a/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs +++ b/Nitrox.Server.Subnautica/Models/AppEvents/ISessionCleaner.cs @@ -6,7 +6,7 @@ namespace Nitrox.Server.Subnautica.Models.AppEvents; internal interface ISessionCleaner : IEvent { - public record Args(SessionManager.Session Session, int NewPlayerTotal); + public record Args(SessionManager.Session Session, int NewSessionTotal); public class Trigger(Func[]> lazyHandlersProvider) : AsyncTrigger(lazyHandlersProvider); } diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs index da23fca2a5..8451a51cb7 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/CommandRegistry.cs @@ -197,6 +197,10 @@ public async ValueTask> TryConvertToType(string value, Type { continue; } + if (!handlers[0].AcceptedOrigin.HasFlag(CommandOrigin.PLAYER)) + { + continue; + } bool canViewCommand = false; foreach (CommandHandlerEntry h in handlers) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs index 8e2f8787dd..f8920798b5 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/SleepManager.cs @@ -91,11 +91,11 @@ public async Task OnEventAsync(ISessionCleaner.Args args) } // Send to all players except the disconnecting one - SleepStatusUpdate packet = new(sessionIdsInBed.Count, args.NewPlayerTotal); + SleepStatusUpdate packet = new(sessionIdsInBed.Count, args.NewSessionTotal); await packetSender.SendPacketToOthersAsync(packet, args.Session.Id); // Check if remaining players are now all sleeping (disconnected player was the only one awake) - if (args.NewPlayerTotal > 0 && sessionIdsInBed.Count >= args.NewPlayerTotal) + if (args.NewSessionTotal > 0 && sessionIdsInBed.Count >= args.NewSessionTotal) { StartSleep(); } diff --git a/Nitrox.Server.Subnautica/Services/HibernateService.cs b/Nitrox.Server.Subnautica/Services/HibernateService.cs index 8eaa5fc4a5..d321b7c2be 100644 --- a/Nitrox.Server.Subnautica/Services/HibernateService.cs +++ b/Nitrox.Server.Subnautica/Services/HibernateService.cs @@ -1,12 +1,13 @@ using Nitrox.Server.Subnautica.Models.AppEvents; +using Nitrox.Server.Subnautica.Models.AppEvents.Core; namespace Nitrox.Server.Subnautica.Services; -internal sealed class HibernateService(IHibernate.SleepTrigger sleepTrigger, IHibernate.WakeTrigger wakeTrigger, ILogger logger) : IHostedLifecycleService +internal sealed class HibernateService(IHibernate.SleepTrigger sleepTrigger, IHibernate.WakeTrigger wakeTrigger, ILogger logger) : IHostedLifecycleService, ISessionCleaner { + private readonly ILogger logger = logger; private readonly IHibernate.SleepTrigger sleepTrigger = sleepTrigger; private readonly IHibernate.WakeTrigger wakeTrigger = wakeTrigger; - private readonly ILogger logger = logger; public bool IsSleeping { @@ -53,4 +54,12 @@ public async Task WakeAsync() public async Task StoppingAsync(CancellationToken cancellationToken) => await SleepAsync(); public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + async Task IEvent.OnEventAsync(ISessionCleaner.Args args) + { + if (args.NewSessionTotal < 1) + { + await SleepAsync(); + } + } } diff --git a/Nitrox.Server.Subnautica/Services/SaveService.cs b/Nitrox.Server.Subnautica/Services/SaveService.cs index dc6c563022..5d7d5a7ab4 100644 --- a/Nitrox.Server.Subnautica/Services/SaveService.cs +++ b/Nitrox.Server.Subnautica/Services/SaveService.cs @@ -4,18 +4,20 @@ using Nitrox.Model.Constants; using Nitrox.Model.Platforms.OS.Shared; using Nitrox.Server.Subnautica.Models.AppEvents; +using Nitrox.Server.Subnautica.Models.AppEvents.Core; using Nitrox.Server.Subnautica.Models.Serialization.World; using Nitrox.Server.Subnautica.Services.Core; namespace Nitrox.Server.Subnautica.Services; -internal sealed class SaveService(Func worldServiceProvider, ISaveState.Trigger saveStateTrigger, IOptions options, IOptions startOptions, ILogger logger) : QueuingBackgroundService, IHostedLifecycleService +internal sealed class SaveService(Func worldServiceProvider, ISaveState.Trigger saveStateTrigger, IOptions options, IOptions startOptions, ILogger logger) + : QueuingBackgroundService, IHibernate { - private readonly Func worldServiceProvider = worldServiceProvider; + private readonly ILogger logger = logger; + private readonly IOptions options = options; private readonly ISaveState.Trigger saveStateTrigger = saveStateTrigger; private readonly IOptions startOptions = startOptions; - private readonly IOptions options = options; - private readonly ILogger logger = logger; + private readonly Func worldServiceProvider = worldServiceProvider; protected override async Task ExecuteQueuedActionAsync(ServiceAction action, CancellationToken stoppingToken) { @@ -117,13 +119,9 @@ private void BackUp(string saveDir) } } - public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public async Task StartedAsync(CancellationToken cancellationToken) => await QueueActionAsync(ServiceAction.SAVE, cancellationToken); - - public async Task StoppingAsync(CancellationToken cancellationToken) => await QueueActionAsync(ServiceAction.SAVE, cancellationToken); + async Task IEvent.OnEventAsync(IHibernate.SleepArgs args) => await QueueActionAsync(ServiceAction.SAVE); - public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + Task IEvent.OnEventAsync(IHibernate.WakeArgs args) => Task.CompletedTask; internal enum ServiceAction { diff --git a/NitroxClient/Communication/MultiplayerSession/ConnectionState/Disconnected.cs b/NitroxClient/Communication/MultiplayerSession/ConnectionState/Disconnected.cs index 7eed569ceb..2230591f11 100644 --- a/NitroxClient/Communication/MultiplayerSession/ConnectionState/Disconnected.cs +++ b/NitroxClient/Communication/MultiplayerSession/ConnectionState/Disconnected.cs @@ -1,9 +1,7 @@ using System; -using System.Threading.Tasks; using NitroxClient.Communication.Abstract; using NitroxClient.Communication.Exceptions; using Nitrox.Model.Helper; -using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; namespace NitroxClient.Communication.MultiplayerSession.ConnectionState From 9b90ae3d9fc510d48cc62cebe07e90ec07e739b4 Mon Sep 17 00:00:00 2001 From: dartasen Date: Mon, 16 Feb 2026 23:33:59 +0100 Subject: [PATCH 13/59] Upgrade LiteNetLib --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1450e135cd..843302198d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 601babfd6f95193980ad8456233888f24b952307 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:58:05 +0100 Subject: [PATCH 14/59] Fixed NRE at BatchEntitySpawner.parsedBatches --- .../Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs index 9031fbbafa..b1557aabc8 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs @@ -39,7 +39,7 @@ internal sealed class BatchEntitySpawner( private readonly Lock parsedBatchesLock = new(); private readonly Lock emptyBatchesLock = new(); - private HashSet parsedBatches; + private HashSet parsedBatches = []; public List SerializableParsedBatches { From 9b4c591b89b490f804e0e0b10e89e59df659576d Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:11:06 +0100 Subject: [PATCH 15/59] Fixed server output selection not copying text on ctrl+c --- Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs | 4 ++-- Nitrox.Launcher/Views/EmbeddedServerView.axaml | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs index 392021bfa4..8a3c07f1a3 100644 --- a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs @@ -48,7 +48,7 @@ public EmbeddedServerViewModel(ServerEntry serverEntry) } }); } - + [RelayCommand] private void Back() => ChangeViewToPrevious(); @@ -84,7 +84,7 @@ private async Task SendServerAsync(TextBox textBox) } ClearInput(textBox); } - + [RelayCommand] private async Task StopServerAsync() { diff --git a/Nitrox.Launcher/Views/EmbeddedServerView.axaml b/Nitrox.Launcher/Views/EmbeddedServerView.axaml index 5670d0fcbc..a87d13e89c 100644 --- a/Nitrox.Launcher/Views/EmbeddedServerView.axaml +++ b/Nitrox.Launcher/Views/EmbeddedServerView.axaml @@ -55,11 +55,13 @@ VerticalAlignment="Center" /> - @@ -92,7 +94,7 @@ - + Date: Thu, 19 Feb 2026 17:06:35 +0100 Subject: [PATCH 16/59] Fixed launcher player count in UI not updating on server stop --- Nitrox.Launcher/Models/Design/ServerEntry.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Nitrox.Launcher/Models/Design/ServerEntry.cs b/Nitrox.Launcher/Models/Design/ServerEntry.cs index 449e7fd739..f729badbed 100644 --- a/Nitrox.Launcher/Models/Design/ServerEntry.cs +++ b/Nitrox.Launcher/Models/Design/ServerEntry.cs @@ -372,6 +372,7 @@ await Dispatcher.UIThread.InvokeAsync(async () => } } CommandQueue = Channel.CreateUnbounded(); + Players = 0; IsOnline = false; Output.Clear(); }); From 094db8ebc04d3dc496236ed15c8b1d88725c0cc8 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:46:24 +0100 Subject: [PATCH 17/59] Fixed url-tagged text not aligning with normal text in RichTextBlock --- Nitrox.Launcher/Models/Controls/RichTextBlock.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Nitrox.Launcher/Models/Controls/RichTextBlock.cs b/Nitrox.Launcher/Models/Controls/RichTextBlock.cs index 5f74844806..bd6ca67948 100644 --- a/Nitrox.Launcher/Models/Controls/RichTextBlock.cs +++ b/Nitrox.Launcher/Models/Controls/RichTextBlock.cs @@ -126,7 +126,10 @@ public static void ParseTextAndAddInlines(ReadOnlySpan text, InlineCollect private static Run CreateRunWithTags(string text, Dictionary> tags) { - Run run = new(text); + Run run = new(text) + { + BaselineAlignment = BaselineAlignment.Center // Fixes normal text not aligning with URL-tagged text + }; KeyValuePair>? lastColorTag = null; foreach (KeyValuePair> pair in tags) { From a3342d03d0f25ebef35a2c18df9c2288d9acb63f Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:55:37 +0100 Subject: [PATCH 18/59] Fixed auto scroll always scrolling to the bottom --- .../ViewModels/EmbeddedServerViewModel.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs index 8a3c07f1a3..2021f0bd1c 100644 --- a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs @@ -31,6 +31,8 @@ internal partial class EmbeddedServerViewModel : RoutableViewModelBase [ObservableProperty] private bool shouldAutoScroll = true; + private int previousContentLength; + public AvaloniaList ServerOutput => ServerEntry.Output; public EmbeddedServerViewModel(ServerEntry serverEntry) @@ -132,21 +134,29 @@ private void CommandHistoryGoForward(TextBox textBox) [RelayCommand] private void OutputSizeChanged(SizeChangedEventArgs args) { - if (ShouldAutoScroll && args.HeightChanged && args.Source is Visual visual) + if (!ShouldAutoScroll || args.NewSize == args.PreviousSize || args.Source is not Visual visual) { - ScrollViewer scrollViewer = visual.FindAncestorOfType(); - if (scrollViewer is not null) - { - // TODO: ScrollToEnd for virtualized lists is not working well, see: https://github.com/AvaloniaUI/Avalonia/issues/14365 - wait for fix to clean up this code. - // Workaround: Run ScrollToEnd twice on the next two frames. - Dispatcher.UIThread.InvokeAsync(() => - { - scrollViewer.ScrollToEnd(); - // Run it again next frame - Dispatcher.UIThread.InvokeAsync(() => scrollViewer.ScrollToEnd()); - }); - } + return; + } + if (previousContentLength == ServerOutput.Count) + { + return; } + previousContentLength = ServerOutput.Count; + ScrollViewer scrollViewer = visual.FindAncestorOfType(); + if (scrollViewer is null) + { + return; + } + + // TODO: ScrollToEnd for virtualized lists is not working well, see: https://github.com/AvaloniaUI/Avalonia/issues/14365 - wait for fix to clean up this code. + // Workaround: Run ScrollToEnd twice on the next two frames. + Dispatcher.UIThread.InvokeAsync(() => + { + scrollViewer.ScrollToEnd(); + // Run it again next frame + Dispatcher.UIThread.InvokeAsync(() => scrollViewer.ScrollToEnd()); + }); } private void SetCaretToEnd(TextBox textBox) From d767d9f0277ac6278494cb2af80fe12b7af4982c Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:01:38 +0100 Subject: [PATCH 19/59] Fixed string handler command being called if no args are passed --- Nitrox.Server.Subnautica/Services/CommandService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nitrox.Server.Subnautica/Services/CommandService.cs b/Nitrox.Server.Subnautica/Services/CommandService.cs index 87664739a2..0b3678fb2c 100644 --- a/Nitrox.Server.Subnautica/Services/CommandService.cs +++ b/Nitrox.Server.Subnautica/Services/CommandService.cs @@ -120,7 +120,7 @@ public bool ExecuteCommand(ReadOnlySpan inputText, ICommandContext context } // No catch-all handler, return help page... - if (handler == null) + if (handler == null || commandArgs.IsEmpty) { logger.ZLogInformation($"Command {commandName.ToString():@CommandName} does not support the provided arguments. See below for more information."); ExecuteCommand($"help {commandName}", context, out commandTask); From 0b2713cdad07fb385b206649c210bfda8653ff99 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:47:27 +0100 Subject: [PATCH 20/59] Code readability improvements --- Nitrox.Model/Helper/Reflect.cs | 2 +- .../Core/HostToServerCommandContext.cs | 2 +- .../Models/Communication/LiteNetLibServer.cs | 9 ++-- .../Services/PortForwardService.cs | 2 +- .../Platforms/OS/Windows/RegistryTest.cs | 2 +- .../Model/Platforms/OSTestMethodAttribute.cs | 36 +++++++++------- .../PlaceholderGroupWorldEntitySpawner.cs | 2 +- .../WorldEntitySpawnerResolver.cs | 41 ++++++------------- 8 files changed, 44 insertions(+), 52 deletions(-) diff --git a/Nitrox.Model/Helper/Reflect.cs b/Nitrox.Model/Helper/Reflect.cs index 7ca0375eca..800eff7888 100644 --- a/Nitrox.Model/Helper/Reflect.cs +++ b/Nitrox.Model/Helper/Reflect.cs @@ -77,7 +77,7 @@ public static PropertyInfo Property(Expression> expression) return (PropertyInfo)GetMemberInfo(expression); } - private static MemberInfo GetMemberInfo(LambdaExpression expression, Type implementingType = null) + private static MemberInfo GetMemberInfo(LambdaExpression expression, Type? implementingType = null) { Expression currentExpression = expression.Body; while (true) diff --git a/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs index d0719f4d3d..feca5be245 100644 --- a/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs +++ b/Nitrox.Server.Subnautica/Models/Commands/Core/HostToServerCommandContext.cs @@ -12,7 +12,7 @@ internal sealed record HostToServerCommandContext : ICommandContext public ILogger Logger { get; set; } = NullLogger.Instance; public CommandOrigin Origin { get; init; } = CommandOrigin.SERVER; public string OriginName => "SERVER"; - public SessionId OriginId { get; init; } = 0; + public SessionId OriginId { get; init; } = SessionId.SERVER_ID; public Perms Permissions { get; init; } = Perms.HOST; public HostToServerCommandContext(IPacketSender packetSender) diff --git a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs index c55ebadb52..30399b4bc2 100644 --- a/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs +++ b/Nitrox.Server.Subnautica/Models/Communication/LiteNetLibServer.cs @@ -12,6 +12,7 @@ using Nitrox.Model.Packets.Core; using Nitrox.Server.Subnautica.Models.Administration; using Nitrox.Server.Subnautica.Models.AppEvents; +using Nitrox.Server.Subnautica.Models.AppEvents.Core; using Nitrox.Server.Subnautica.Models.GameLogic; using Nitrox.Server.Subnautica.Models.Helper; using Nitrox.Server.Subnautica.Models.Packets.Core; @@ -45,8 +46,10 @@ public LiteNetLibServer(PlayerManager playerManager, SessionManager sessionManag this.options = options; this.logger = logger; listener = new EventBasedNetListener(); - server = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) { - UseNativeSockets = true, IPv6Enabled = true }; + server = new NetManager(listener, NitroxEnvironment.IsReleaseMode ? new Crc32cLayer() : null) + { + UseNativeSockets = true, IPv6Enabled = true + }; } public Task StartAsync(CancellationToken cancellationToken) @@ -181,7 +184,7 @@ public async Task KickPlayer(SessionId sessionId, string reason = "") return true; } - public async Task OnEventAsync(ISessionCleaner.Args args) + async Task IEvent.OnEventAsync(ISessionCleaner.Args args) { Disconnect disconnect = new(args.Session.Id); await SendPacketToAllAsync(disconnect); diff --git a/Nitrox.Server.Subnautica/Services/PortForwardService.cs b/Nitrox.Server.Subnautica/Services/PortForwardService.cs index 30a94fb774..5d4283ac1e 100644 --- a/Nitrox.Server.Subnautica/Services/PortForwardService.cs +++ b/Nitrox.Server.Subnautica/Services/PortForwardService.cs @@ -6,7 +6,7 @@ namespace Nitrox.Server.Subnautica.Services; /// -/// Opens ports on network attached routers via UPnP. +/// Uses to automatically open server listening port on network router via UPnP. /// /// /// By port forwarding, incoming connections will be forwarded to the host machine running the game server.

diff --git a/Nitrox.Test/Model/Platforms/OS/Windows/RegistryTest.cs b/Nitrox.Test/Model/Platforms/OS/Windows/RegistryTest.cs index beddc89c31..94bdbf8858 100644 --- a/Nitrox.Test/Model/Platforms/OS/Windows/RegistryTest.cs +++ b/Nitrox.Test/Model/Platforms/OS/Windows/RegistryTest.cs @@ -8,7 +8,7 @@ namespace Nitrox.Model.Platforms.OS.Windows; [SupportedOSPlatform("windows")] public class RegistryTest { - [OSTestMethod("windows")] + [OSTestMethod(OperatingSystems.Windows)] public async Task WaitsForRegistryKeyToExist() { const string PATH_TO_KEY = @"SOFTWARE\Nitrox\test"; diff --git a/Nitrox.Test/Model/Platforms/OSTestMethodAttribute.cs b/Nitrox.Test/Model/Platforms/OSTestMethodAttribute.cs index dda82583e9..eedd369677 100644 --- a/Nitrox.Test/Model/Platforms/OSTestMethodAttribute.cs +++ b/Nitrox.Test/Model/Platforms/OSTestMethodAttribute.cs @@ -1,32 +1,38 @@ namespace Nitrox.Test.Model.Platforms; +/// +/// Test method attribute, that will only run the test on the specified platform. +/// +/// case-insensitive platform, i.e: linux, windows, osx [AttributeUsage(AttributeTargets.Method)] -public class OSTestMethodAttribute : TestMethodAttribute +internal sealed class OSTestMethodAttribute(OperatingSystems platform) : TestMethodAttribute { - public string Platform { get;} - - /// - /// Test method attribute, that will only run the test on the specified platform. - /// - /// case insensitive platform, i.e: linux, windows, osx - public OSTestMethodAttribute(string platform) - { - Platform = platform; - } + private readonly OperatingSystems platform = platform; public override TestResult[] Execute(ITestMethod testMethod) { - if (!OperatingSystem.IsOSPlatform(Platform)) + if (!OperatingSystem.IsOSPlatform(GetPlatformString())) { - return [ - new TestResult() + return + [ + new TestResult { Outcome = UnitTestOutcome.Inconclusive, - TestContextMessages = $"This test can only be run on {Platform}" + TestContextMessages = $"This test can only be run on {GetPlatformString()}" } ]; } return base.Execute(testMethod); } + + private string GetPlatformString() => + platform switch + { + OperatingSystems.Windows => "Windows", + OperatingSystems.Linux => "Linux", + OperatingSystems.OSX => "OSX", + OperatingSystems.FreeBSD => "FreeBSD", + _ => throw new InvalidOperationException($"Unknown platform {(int)platform}") + }; } diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs index 81fe5e5c02..fa4965be2b 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/PlaceholderGroupWorldEntitySpawner.cs @@ -12,7 +12,7 @@ namespace NitroxClient.GameLogic.Spawning.WorldEntities; /// This spawner can't hold a SpawnSync function because it is also responsible for spawning its children /// so the function will still use sync spawning when possible and fall back to async when required. ///
-public class PlaceholderGroupWorldEntitySpawner : IWorldEntitySpawner +internal sealed class PlaceholderGroupWorldEntitySpawner : IWorldEntitySpawner { private readonly Entities entities; private readonly WorldEntitySpawnerResolver spawnerResolver; diff --git a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs index b9db639949..9db0b09ba4 100644 --- a/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs +++ b/NitroxClient/GameLogic/Spawning/WorldEntities/WorldEntitySpawnerResolver.cs @@ -4,7 +4,7 @@ namespace NitroxClient.GameLogic.Spawning.WorldEntities; -public class WorldEntitySpawnerResolver +internal sealed class WorldEntitySpawnerResolver { private readonly DefaultWorldEntitySpawner defaultEntitySpawner = new(); @@ -32,33 +32,16 @@ public WorldEntitySpawnerResolver(EntityMetadataManager entityMetadataManager, E creatureRespawnEntitySpawner = new CreatureRespawnEntitySpawner(simulationOwnership); } - public IWorldEntitySpawner ResolveEntitySpawner(WorldEntity entity) - { - switch (entity) - { - case PrefabPlaceholderEntity: - return prefabPlaceholderEntitySpawner; - case PlaceholderGroupWorldEntity: - return placeholderGroupWorldEntitySpawner; - case SerializedWorldEntity: - return serializedWorldEntitySpawner; - case GeyserWorldEntity: - return geyserWorldEntitySpawner; - case ReefbackEntity: - return reefbackEntitySpawner; - case ReefbackChildEntity: - return reefbackChildEntitySpawner; - case CreatureRespawnEntity: - return creatureRespawnEntitySpawner; - } - - TechType techType = entity.TechType.ToUnity(); - - if (customSpawnersByTechType.TryGetValue(techType, out IWorldEntitySpawner value)) + public IWorldEntitySpawner ResolveEntitySpawner(WorldEntity entity) => + entity switch { - return value; - } - - return defaultEntitySpawner; - } + PrefabPlaceholderEntity => prefabPlaceholderEntitySpawner, + PlaceholderGroupWorldEntity => placeholderGroupWorldEntitySpawner, + SerializedWorldEntity => serializedWorldEntitySpawner, + GeyserWorldEntity => geyserWorldEntitySpawner, + ReefbackEntity => reefbackEntitySpawner, + ReefbackChildEntity => reefbackChildEntitySpawner, + CreatureRespawnEntity => creatureRespawnEntitySpawner, + _ => customSpawnersByTechType.GetValueOrDefault(entity.TechType.ToUnity(), defaultEntitySpawner) + }; } From 2e5febf70d98828abfe171f894ab0f053a465da0 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:54:04 +0100 Subject: [PATCH 21/59] Fixed prefab cache version not being checked Previously this caused user to require deleting the cache file in order to start the server successfully. --- .../Parsers/PrefabPlaceholderGroupsResource.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs index f3c97eb8bc..47f7e18fbc 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs @@ -149,12 +149,8 @@ private async Task CreateOrLoadPrefabCacheAsync(string nitroxCachePath) { logger.ZLogWarning($"An error occurred while deserializing the prefab cache. Re-creating it: {ex.Message:@Error}"); } - if (cache.HasValue) + if (cache is { Version: CACHE_VERSION }) { - if (cache.Value.Version != CACHE_VERSION) - { - logger.ZLogInformation($"Found outdated cache ({cache.Value.Version}, expected {CACHE_VERSION})"); - } prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths; randomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId; groupsByClassId = cache.Value.GroupsByClassId; @@ -164,6 +160,10 @@ private async Task CreateOrLoadPrefabCacheAsync(string nitroxCachePath) // Fallback solution else { + if (cache.HasValue) + { + logger.ZLogInformation($"Found outdated cache (is v{cache.Value.Version}, expected v{CACHE_VERSION})"); + } logger.ZLogInformation($"Building cache, this may take a while..."); // Get all prefab-classIds linked to the (partial) bundle path string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db"); From 9fbf9e8478ce26138740ef50a6d32866f646aa29 Mon Sep 17 00:00:00 2001 From: MrBub <106004257+misterbubb@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:25:17 -0500 Subject: [PATCH 22/59] Sync PrecursorDisableGunTerminal firstUse state (#2647) --- .../Entities/Metadata/EntityMetadata.cs | 1 + .../PrecursorDisableGunTerminalMetadata.cs | 29 ++++++++++++++++++ .../Server/Serialization/WorldServiceTest.cs | 3 ++ ...rsorDisableGunTerminalMetadataExtractor.cs | 12 ++++++++ ...rsorDisableGunTerminalMetadataProcessor.cs | 26 ++++++++++++++++ ...Terminal_OnPlayerCinematicModeEnd_Patch.cs | 30 +++++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/PrecursorDisableGunTerminalMetadata.cs create mode 100644 NitroxClient/GameLogic/Spawning/Metadata/Extractor/PrecursorDisableGunTerminalMetadataExtractor.cs create mode 100644 NitroxClient/GameLogic/Spawning/Metadata/Processor/PrecursorDisableGunTerminalMetadataProcessor.cs create mode 100644 NitroxPatcher/Patches/Dynamic/PrecursorDisableGunTerminal_OnPlayerCinematicModeEnd_Patch.cs diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs index 815bb037ce..63ca7ca3ba 100644 --- a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/EntityMetadata.cs @@ -44,6 +44,7 @@ namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata [ProtoInclude(85, typeof(PrecursorComputerTerminalMetadata))] [ProtoInclude(86, typeof(GenericConsoleMetadata))] [ProtoInclude(87, typeof(BlueprintHandTargetMetadata))] + [ProtoInclude(88, typeof(PrecursorDisableGunTerminalMetadata))] public abstract class EntityMetadata { } diff --git a/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/PrecursorDisableGunTerminalMetadata.cs b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/PrecursorDisableGunTerminalMetadata.cs new file mode 100644 index 0000000000..4b5fdd57ba --- /dev/null +++ b/Nitrox.Model.Subnautica/DataStructures/GameLogic/Entities/Metadata/PrecursorDisableGunTerminalMetadata.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.Serialization; +using BinaryPack.Attributes; + +namespace Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; + +[Serializable] +[DataContract] +public class PrecursorDisableGunTerminalMetadata : EntityMetadata +{ + [DataMember(Order = 1)] + public bool FirstUse { get; } + + [IgnoreConstructor] + protected PrecursorDisableGunTerminalMetadata() + { + // Constructor for serialization. Has to be "protected" for json serialization. + } + + public PrecursorDisableGunTerminalMetadata(bool firstUse) + { + FirstUse = firstUse; + } + + public override string ToString() + { + return $"[PrecursorDisableGunTerminalMetadata FirstUse: {FirstUse}]"; + } +} diff --git a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs index 06c1499f12..7d51598a6d 100644 --- a/Nitrox.Test/Server/Serialization/WorldServiceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldServiceTest.cs @@ -347,6 +347,9 @@ private static void EntityTest(Entity entity, Entity entityAfter) case BlueprintHandTargetMetadata metadata when entityAfter.Metadata is BlueprintHandTargetMetadata metadataAfter: Assert.AreEqual(metadata.Used, metadataAfter.Used); break; + case PrecursorDisableGunTerminalMetadata metadata when entityAfter.Metadata is PrecursorDisableGunTerminalMetadata metadataAfter: + Assert.AreEqual(metadata.FirstUse, metadataAfter.FirstUse); + break; default: Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {entity.Metadata?.GetType().Name} - {entityAfter.Metadata?.GetType().Name}"); break; diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PrecursorDisableGunTerminalMetadataExtractor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PrecursorDisableGunTerminalMetadataExtractor.cs new file mode 100644 index 0000000000..ddf3c52a12 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Extractor/PrecursorDisableGunTerminalMetadataExtractor.cs @@ -0,0 +1,12 @@ +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; +using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor; + +public class PrecursorDisableGunTerminalMetadataExtractor : EntityMetadataExtractor +{ + public override PrecursorDisableGunTerminalMetadata Extract(PrecursorDisableGunTerminal entity) + { + return new(entity.firstUse); + } +} diff --git a/NitroxClient/GameLogic/Spawning/Metadata/Processor/PrecursorDisableGunTerminalMetadataProcessor.cs b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PrecursorDisableGunTerminalMetadataProcessor.cs new file mode 100644 index 0000000000..a2ccda7b08 --- /dev/null +++ b/NitroxClient/GameLogic/Spawning/Metadata/Processor/PrecursorDisableGunTerminalMetadataProcessor.cs @@ -0,0 +1,26 @@ +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; +using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract; +using UnityEngine; + +namespace NitroxClient.GameLogic.Spawning.Metadata.Processor; + +public class PrecursorDisableGunTerminalMetadataProcessor : EntityMetadataProcessor +{ + public override void ProcessMetadata(GameObject gameObject, PrecursorDisableGunTerminalMetadata metadata) + { + // The NitroxEntity is on a parent object, so the terminal component may be on this object or a child + if (!gameObject.TryGetComponent(out PrecursorDisableGunTerminal terminal)) + { + terminal = gameObject.GetComponentInChildren(); + } + + if (!terminal) + { + Log.Warn($"[PrecursorDisableGunTerminalMetadataProcessor] No PrecursorDisableGunTerminal component found on {gameObject.name} or its children"); + return; + } + + Log.Debug($"[PrecursorDisableGunTerminalMetadataProcessor] Applying metadata: firstUse={metadata.FirstUse} to {terminal.gameObject.name}"); + terminal.firstUse = metadata.FirstUse; + } +} diff --git a/NitroxPatcher/Patches/Dynamic/PrecursorDisableGunTerminal_OnPlayerCinematicModeEnd_Patch.cs b/NitroxPatcher/Patches/Dynamic/PrecursorDisableGunTerminal_OnPlayerCinematicModeEnd_Patch.cs new file mode 100644 index 0000000000..0ad9410ad9 --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/PrecursorDisableGunTerminal_OnPlayerCinematicModeEnd_Patch.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using NitroxClient.GameLogic; +using NitroxClient.MonoBehaviours; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities.Metadata; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Syncs the firstUse state of the PrecursorDisableGunTerminal (the terminal at the Precursor Gun facility). +/// This ensures remote players see the correct animation (first use is longer, subsequent uses are shorter). +/// The firstUse flag is set to false in OnPlayerCinematicModeEnd after the cinematic completes. +/// +public sealed partial class PrecursorDisableGunTerminal_OnPlayerCinematicModeEnd_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((PrecursorDisableGunTerminal t) => t.OnPlayerCinematicModeEnd()); + + public static void Postfix(PrecursorDisableGunTerminal __instance) + { + // After the cinematic ends, firstUse is set to false - sync this to other players + // The terminal component is on a child object, so we need to look up the hierarchy for NitroxEntity + if (!__instance.TryGetComponentInParent(out NitroxEntity entity, true)) + { + Log.Warn($"[PrecursorDisableGunTerminal] No NitroxEntity found in hierarchy for {__instance.gameObject.GetFullHierarchyPath()}"); + return; + } + + Log.Debug($"[PrecursorDisableGunTerminal] Broadcasting metadata update: firstUse={__instance.firstUse}, id={entity.Id}"); + Resolve().BroadcastMetadataUpdate(entity.Id, new PrecursorDisableGunTerminalMetadata(__instance.firstUse)); + } +} From f5ab7477ac17c6545cf75c63887a7b0093aa6f97 Mon Sep 17 00:00:00 2001 From: MrBub <106004257+misterbubb@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:29:25 -0500 Subject: [PATCH 23/59] Re-do the "restart" command (#2594) Co-authored-by: misterbubb <106004257+misterbubb@users.noreply.github.com> --- Nitrox.Launcher/Models/Design/ServerEntry.cs | 3 +- Nitrox.Model/Platforms/OS/Shared/ProcessEx.cs | 2 +- Nitrox.Server.Subnautica/AssemblyResolver.cs | 2 +- .../Models/Commands/RestartCommand.cs | 36 +++++++++++++++ Nitrox.Server.Subnautica/Program.cs | 1 + .../Services/RestartService.cs | 44 +++++++++++++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 Nitrox.Server.Subnautica/Models/Commands/RestartCommand.cs create mode 100644 Nitrox.Server.Subnautica/Services/RestartService.cs diff --git a/Nitrox.Launcher/Models/Design/ServerEntry.cs b/Nitrox.Launcher/Models/Design/ServerEntry.cs index f729badbed..d0c4e9dfe6 100644 --- a/Nitrox.Launcher/Models/Design/ServerEntry.cs +++ b/Nitrox.Launcher/Models/Design/ServerEntry.cs @@ -413,7 +413,8 @@ private ServerProcess(string saveDir, CancellationTokenSource cts, bool isEmbedd { "--save", saveName, - $"--game-path \"{NitroxUser.GamePath}\"", + "--game-path", + NitroxUser.GamePath }, WindowStyle = isEmbeddedMode ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal, CreateNoWindow = isEmbeddedMode diff --git a/Nitrox.Model/Platforms/OS/Shared/ProcessEx.cs b/Nitrox.Model/Platforms/OS/Shared/ProcessEx.cs index 629c376e33..3d4dd18bdd 100644 --- a/Nitrox.Model/Platforms/OS/Shared/ProcessEx.cs +++ b/Nitrox.Model/Platforms/OS/Shared/ProcessEx.cs @@ -110,7 +110,7 @@ public static bool ProcessExists(string procName, Func? predica // On Linux, processes are started as child by default. So we wrap as shell command to start detached from current process. if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - List newArgs = ["-c", string.Join(" ", "nohup", $"'{startInfo.FileName}'", string.Join(" ", startInfo.ArgumentList), ">/dev/null 2>&1", "&")]; + List newArgs = ["-c", string.Join(" ", "nohup", $"'{startInfo.FileName}'", string.Join(" ", startInfo.ArgumentList.Select(a => $"'{a}'")), ">/dev/null 2>&1", "&")]; startInfo.FileName = "/bin/sh"; startInfo.ArgumentList.Clear(); foreach (string arg in newArgs) diff --git a/Nitrox.Server.Subnautica/AssemblyResolver.cs b/Nitrox.Server.Subnautica/AssemblyResolver.cs index d764dc27a8..ad33007cb2 100644 --- a/Nitrox.Server.Subnautica/AssemblyResolver.cs +++ b/Nitrox.Server.Subnautica/AssemblyResolver.cs @@ -93,7 +93,7 @@ internal static class AssemblyResolver return null; } // Try find game managed libraries - return Path.Combine(NitroxUser.GamePath, GameInfo.Subnautica.DataFolder, "Managed", dllName); + return Path.Combine(GamePath, GameInfo.Subnautica.DataFolder, "Managed", dllName); } private static string GetExecutableDirectory() diff --git a/Nitrox.Server.Subnautica/Models/Commands/RestartCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/RestartCommand.cs new file mode 100644 index 0000000000..8b12421872 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/RestartCommand.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.Diagnostics; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Services; + +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresOrigin(CommandOrigin.SERVER)] +internal sealed class RestartCommand(IOptions options, RestartService restartService, ILogger logger, IHostApplicationLifetime lifetime) : ICommandHandler +{ + private readonly IOptions options = options; + private readonly IHostApplicationLifetime lifetime = lifetime; + private readonly ILogger logger = logger; + private readonly RestartService restartService = restartService; + + [Description("Restarts the server")] + public Task Execute(ICommandContext context) + { + if (Debugger.IsAttached) + { + logger.ZLogError($"Server can not be restarted while a debugger is attached."); + return Task.CompletedTask; + } + if (options.Value.IsEmbedded) + { + logger.ZLogError($"Use launcher to stop and start the server."); + return Task.CompletedTask; + } + + logger.ZLogInformation($"Server will restart on close. Stopping server..."); + restartService.RestartOnStop = true; + lifetime.StopApplication(); + + return Task.CompletedTask; + } +} diff --git a/Nitrox.Server.Subnautica/Program.cs b/Nitrox.Server.Subnautica/Program.cs index 10011993b7..7da5f3758d 100644 --- a/Nitrox.Server.Subnautica/Program.cs +++ b/Nitrox.Server.Subnautica/Program.cs @@ -112,6 +112,7 @@ private static async Task StartServerAsync(string[] args) .AddHostedSingletonService() .AddHostedSingletonService() .AddHostedSingletonService() + .AddHostedSingletonService() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Nitrox.Server.Subnautica/Services/RestartService.cs b/Nitrox.Server.Subnautica/Services/RestartService.cs new file mode 100644 index 0000000000..41f3b8c926 --- /dev/null +++ b/Nitrox.Server.Subnautica/Services/RestartService.cs @@ -0,0 +1,44 @@ +using Nitrox.Model.Core; +using Nitrox.Model.Platforms.OS.Shared; +using Nitrox.Server.Subnautica.Models.Packets.Core; + +namespace Nitrox.Server.Subnautica.Services; + +internal sealed class RestartService(IPacketSender packetSender, ILogger logger) : IHostedLifecycleService +{ + private readonly ILogger logger = logger; + private readonly IPacketSender packetSender = packetSender; + + public bool RestartOnStop + { + get => Interlocked.CompareExchange(ref field, false, false); + set => Interlocked.CompareExchange(ref field, value, !value); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + if (RestartOnStop) + { + await packetSender.SendPacketToAllAsync(new ChatMessage(SessionId.SERVER_ID, "Server is restarting...")); + } + } + + public Task StoppedAsync(CancellationToken cancellationToken) + { + if (!RestartOnStop) + { + return Task.CompletedTask; + } + logger.ZLogInformation($"Server is restarting..."); + ProcessEx.StartSelfCopyArgs(); + return Task.CompletedTask; + } +} From 2789685673abf277e6edf9775a17643e2a6c64c6 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:08:25 +0100 Subject: [PATCH 24/59] Fix logs not getting redacted in file due to CaptureScope. Added a TODO to find altnerative way to group logs so that other logs aren't in-between. --- .../Services/StatusService.cs | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/Nitrox.Server.Subnautica/Services/StatusService.cs b/Nitrox.Server.Subnautica/Services/StatusService.cs index f01238c6aa..288a97b868 100644 --- a/Nitrox.Server.Subnautica/Services/StatusService.cs +++ b/Nitrox.Server.Subnautica/Services/StatusService.cs @@ -56,36 +56,33 @@ public async Task StartedAsync(CancellationToken cancellationToken) async Task LogIps() { - // Capture and log so that logs are written in one go. This prevents different log lines being inserted in-between. - string logMessage; - using (CaptureScope captureScope = logger.BeginCaptureScope()) + // Note: Do not use capture scope here because no redaction happens in captured logs. + /* TODO: Find a way to group logs and output in one go so that unrelated logs aren't in-between. + * Need to implement this by buffering ZLoggerEntry in a queue and writing once signaled (e.g. log scope gets disposed) + */ + using (logger.BeginPlainScope()) { - using (logger.BeginPlainScope()) + logger.ZLogInformation($"Use IP to connect:"); + using (logger.BeginPrefixScope("\t")) { - logger.ZLogInformation($"Use IP to connect:"); - using (logger.BeginPrefixScope("\t")) + logger.ZLogInformation($"{IPAddress.Loopback} - You (Local)"); + foreach ((IPAddress address, NetHelper.MachineIpOrigin origin, string? networkName) in await NetHelper.GetAllKnownIpsAsync()) { - logger.ZLogInformation($"{IPAddress.Loopback} - You (Local)"); - foreach ((IPAddress address, NetHelper.MachineIpOrigin origin, string? networkName) in await NetHelper.GetAllKnownIpsAsync()) + switch (origin) { - switch (origin) - { - case NetHelper.MachineIpOrigin.LAN: - logger.LogLanIp(address); - break; - case NetHelper.MachineIpOrigin.VPN: - logger.LogVpnIp(networkName!, address); - break; - case NetHelper.MachineIpOrigin.WAN: - logger.LogWanIp(address); - break; - } + case NetHelper.MachineIpOrigin.LAN: + logger.LogLanIp(address); + break; + case NetHelper.MachineIpOrigin.VPN: + logger.LogVpnIp(networkName!, address); + break; + case NetHelper.MachineIpOrigin.WAN: + logger.LogWanIp(address); + break; } } } - logMessage = $"{string.Join("", captureScope.Logs).Trim(Environment.NewLine)}{Environment.NewLine}"; } - logger.ZLogInformation($"{logMessage}"); } } From cbb4688c532d05d3735a2bd4c62d7c4d01aa398b Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:10:51 +0100 Subject: [PATCH 25/59] Fix entities not showing if game path set via command line args --- .../Extensions/ServiceCollectionExtensions.cs | 8 ++++---- .../Models/Serialization/BatchCellsParser.cs | 8 +------- Nitrox.Server.Subnautica/Program.cs | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs index d5cf231979..2ff960ad7a 100644 --- a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs @@ -106,7 +106,7 @@ public IServiceCollection AddFallback() where TInterface public IServiceCollection AddHostedSingletonService() where T : class, IHostedService => services.AddSingleton().AddHostedService(provider => provider.GetRequiredService()); - public IServiceCollection AddNitroxOptions() + public IServiceCollection AddNitroxOptions(ServerStartOptions startOptions) { services.AddOptionsWithValidateOnStart() .BindConfiguration("") @@ -114,15 +114,15 @@ public IServiceCollection AddNitroxOptions() { if (string.IsNullOrWhiteSpace(options.GamePath)) { - options.GamePath = NitroxUser.GamePath; + options.GamePath = startOptions.GamePath; } if (string.IsNullOrWhiteSpace(options.NitroxAssetsPath)) { - options.NitroxAssetsPath = NitroxUser.AssetsPath; + options.NitroxAssetsPath = startOptions.NitroxAssetsPath; } if (string.IsNullOrWhiteSpace(options.NitroxAppDataPath)) { - options.NitroxAppDataPath = NitroxUser.AppDataPath; + options.NitroxAppDataPath = startOptions.NitroxAppDataPath; } }); services.AddOptionsWithValidateOnStart() diff --git a/Nitrox.Server.Subnautica/Models/Serialization/BatchCellsParser.cs b/Nitrox.Server.Subnautica/Models/Serialization/BatchCellsParser.cs index 5bee96df28..bcfb46adb3 100644 --- a/Nitrox.Server.Subnautica/Models/Serialization/BatchCellsParser.cs +++ b/Nitrox.Server.Subnautica/Models/Serialization/BatchCellsParser.cs @@ -40,7 +40,7 @@ public BatchCellsParser(EntitySpawnPointFactory entitySpawnPointFactory, Subnaut public List ParseBatchData(NitroxInt3 batchId) { - List spawnPoints = new List(); + List spawnPoints = []; ParseFile(batchId, "CellsCache", "baked-", "", spawnPoints); @@ -49,12 +49,6 @@ public List ParseBatchData(NitroxInt3 batchId) public void ParseFile(NitroxInt3 batchId, string pathPrefix, string prefix, string suffix, List spawnPoints) { - string subnauticaPath = NitroxUser.GamePath; - if (string.IsNullOrEmpty(subnauticaPath)) - { - return; - } - string path = options.Value.GetSubnauticaBuild18Path(); string fileName = Path.Combine(path, pathPrefix, $"{prefix}batch-cells-{batchId.X}-{batchId.Y}-{batchId.Z}{suffix}.bin"); diff --git a/Nitrox.Server.Subnautica/Program.cs b/Nitrox.Server.Subnautica/Program.cs index 7da5f3758d..394f3d726b 100644 --- a/Nitrox.Server.Subnautica/Program.cs +++ b/Nitrox.Server.Subnautica/Program.cs @@ -91,7 +91,7 @@ private static async Task StartServerAsync(string[] args) options.ServicesStartConcurrently = true; options.ServicesStopConcurrently = true; }) - .AddNitroxOptions() + .AddNitroxOptions(startOptions) // Add initialization services - diagnoses the server environment on startup. .AddHostedSingletonService() .AddHostedSingletonService() From c770a5749cca5bb4fa7fb70bf946ace49ebb1096 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:22:57 +0100 Subject: [PATCH 26/59] Add BoxCollider to debugger (#2676) --- .../Debuggers/Drawer/DrawerManager.cs | 9 ++- .../Drawer/Unity/BoxColliderDrawer.cs | 76 +++++++++++++++++++ .../Debuggers/Drawer/Unity/MaterialDrawer.cs | 11 ++- .../Debuggers/Drawer/Unity/RigidbodyDrawer.cs | 10 ++- .../Debuggers/Drawer/UnityUI/MaskDrawer.cs | 5 +- .../MonoBehaviours/NitroxDebugManager.cs | 2 +- 6 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 NitroxClient/Debuggers/Drawer/Unity/BoxColliderDrawer.cs diff --git a/NitroxClient/Debuggers/Drawer/DrawerManager.cs b/NitroxClient/Debuggers/Drawer/DrawerManager.cs index b4c3c8a619..2d8c13f753 100644 --- a/NitroxClient/Debuggers/Drawer/DrawerManager.cs +++ b/NitroxClient/Debuggers/Drawer/DrawerManager.cs @@ -21,8 +21,8 @@ namespace NitroxClient.Debuggers.Drawer; ///
public class DrawerManager { - private readonly Dictionary> drawers = new(); - private readonly Dictionary> editorDrawers = new(); + private readonly Dictionary> drawers = []; + private readonly Dictionary> editorDrawers = []; public DrawerManager(SceneDebugger sceneDebugger) { @@ -37,6 +37,7 @@ public DrawerManager(SceneDebugger sceneDebugger) MaterialDrawer materialDrawer = new(); ImageDrawer imageDrawer = new(colorDrawer, materialDrawer, rectDrawer); NitroxEntityDrawer nitroxEntityDrawer = new(); + RigidbodyDrawer rigidbodyDrawer = new(vectorDrawer); AddDrawer(nitroxEntityDrawer); AddDrawer(nitroxEntityDrawer); @@ -68,13 +69,14 @@ public DrawerManager(SceneDebugger sceneDebugger) AddDrawer(new(colorDrawer, materialDrawer)); AddDrawer(new(sceneDebugger, selectableDrawer, unityEventDrawer)); AddDrawer(); - AddDrawer(new(vectorDrawer)); + AddDrawer(rigidbodyDrawer); AddDrawer(new(sceneDebugger, vectorDrawer)); AddDrawer(unityEventDrawer); AddDrawer>(unityEventDrawer); AddDrawer(new(vectorDrawer, sceneDebugger)); AddDrawer(); AddDrawer(new(vectorDrawer)); + AddDrawer(new(vectorDrawer, rigidbodyDrawer)); AddEditor(vectorDrawer); AddEditor(vectorDrawer); @@ -86,6 +88,7 @@ public DrawerManager(SceneDebugger sceneDebugger) AddEditor(colorDrawer); AddEditor(colorDrawer); AddEditor(materialDrawer); + AddEditor(materialDrawer); AddEditor(rectDrawer); AddEditor(rectDrawer); } diff --git a/NitroxClient/Debuggers/Drawer/Unity/BoxColliderDrawer.cs b/NitroxClient/Debuggers/Drawer/Unity/BoxColliderDrawer.cs new file mode 100644 index 0000000000..3009559692 --- /dev/null +++ b/NitroxClient/Debuggers/Drawer/Unity/BoxColliderDrawer.cs @@ -0,0 +1,76 @@ +using Nitrox.Model.Helper; +using UnityEngine; +using UWE; + +namespace NitroxClient.Debuggers.Drawer.Unity; + +public sealed class BoxColliderDrawer : IDrawer +{ + private readonly RigidbodyDrawer rigidbodyDrawer; + private readonly VectorDrawer vectorDrawer; + private const float VECTOR_MAX_WIDTH = 405; + + public BoxColliderDrawer(VectorDrawer vectorDrawer, RigidbodyDrawer rigidbodyDrawer) + { + Validate.NotNull(vectorDrawer); + Validate.NotNull(rigidbodyDrawer); + + this.vectorDrawer = vectorDrawer; + this.rigidbodyDrawer = rigidbodyDrawer; + } + + public void Draw(BoxCollider target) + { + using (new GUILayout.VerticalScope()) + { + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Center", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + target.center = vectorDrawer.Draw(target.center, new VectorDrawer.DrawOptions(VECTOR_MAX_WIDTH)); + } + + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Size", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + target.size = vectorDrawer.Draw(target.size, new VectorDrawer.DrawOptions(VECTOR_MAX_WIDTH)); + } + } + + GUILayout.Space(10); + + using (new GUILayout.VerticalScope()) + { + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Enabled", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + target.enabled = NitroxGUILayout.BoolField(target.enabled); + } + + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Is Trigger", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + target.isTrigger = NitroxGUILayout.BoolField(target.isTrigger); + } + + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Contact Offset", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + target.contactOffset = NitroxGUILayout.FloatField(target.contactOffset); + } + } + + GUILayout.Space(10); + + using (new GUILayout.HorizontalScope()) + { + GUILayout.Label("Attached Rigid Body", NitroxGUILayout.DrawerLabel, GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); + NitroxGUILayout.Separator(); + rigidbodyDrawer.Draw(target.attachedRigidbody); + } + } +} diff --git a/NitroxClient/Debuggers/Drawer/Unity/MaterialDrawer.cs b/NitroxClient/Debuggers/Drawer/Unity/MaterialDrawer.cs index 9125119f8c..1d3ca7831f 100644 --- a/NitroxClient/Debuggers/Drawer/Unity/MaterialDrawer.cs +++ b/NitroxClient/Debuggers/Drawer/Unity/MaterialDrawer.cs @@ -1,8 +1,8 @@ -using UnityEngine; +using UnityEngine; namespace NitroxClient.Debuggers.Drawer.Unity; -public class MaterialDrawer : IEditorDrawer +public sealed class MaterialDrawer : IEditorDrawer, IEditorDrawer { public Material Draw(Material material) { @@ -10,4 +10,11 @@ public Material Draw(Material material) GUILayout.Box(material.name, GUILayout.Width(150), GUILayout.Height(20)); return material; } + + public PhysicMaterial Draw(PhysicMaterial target) + { + // TODO: Implement Material picker + GUILayout.Box(target.name, GUILayout.Width(150), GUILayout.Height(20)); + return target; + } } diff --git a/NitroxClient/Debuggers/Drawer/Unity/RigidbodyDrawer.cs b/NitroxClient/Debuggers/Drawer/Unity/RigidbodyDrawer.cs index d5c91d956d..84e8d24312 100644 --- a/NitroxClient/Debuggers/Drawer/Unity/RigidbodyDrawer.cs +++ b/NitroxClient/Debuggers/Drawer/Unity/RigidbodyDrawer.cs @@ -3,7 +3,7 @@ namespace NitroxClient.Debuggers.Drawer.Unity; -public class RigidbodyDrawer : IDrawer +public sealed class RigidbodyDrawer : IDrawer { private readonly VectorDrawer vectorDrawer; private const float LABEL_WIDTH = 120; @@ -16,8 +16,14 @@ public RigidbodyDrawer(VectorDrawer vectorDrawer) this.vectorDrawer = vectorDrawer; } - public void Draw(Rigidbody rb) + public void Draw(Rigidbody? rb) { + if (!rb) + { + GUILayout.TextField("Rigidbody is null", NitroxGUILayout.DrawerLabel); + return; + } + using (new GUILayout.HorizontalScope()) { GUILayout.Label("Mass", NitroxGUILayout.DrawerLabel, GUILayout.Width(LABEL_WIDTH)); diff --git a/NitroxClient/Debuggers/Drawer/UnityUI/MaskDrawer.cs b/NitroxClient/Debuggers/Drawer/UnityUI/MaskDrawer.cs index e17360a28c..6164b76144 100644 --- a/NitroxClient/Debuggers/Drawer/UnityUI/MaskDrawer.cs +++ b/NitroxClient/Debuggers/Drawer/UnityUI/MaskDrawer.cs @@ -1,13 +1,10 @@ -using System; using UnityEngine; using UnityEngine.UI; namespace NitroxClient.Debuggers.Drawer.UnityUI; -public class MaskDrawer : IDrawer, IDrawer +public sealed class MaskDrawer : IDrawer, IDrawer { - public Type[] ApplicableTypes { get; } = { typeof(Mask), typeof(RectMask2D) }; - public void Draw(Mask mask) { using (new GUILayout.HorizontalScope()) diff --git a/NitroxClient/MonoBehaviours/NitroxDebugManager.cs b/NitroxClient/MonoBehaviours/NitroxDebugManager.cs index 8eb0a24e61..b8fa653508 100644 --- a/NitroxClient/MonoBehaviours/NitroxDebugManager.cs +++ b/NitroxClient/MonoBehaviours/NitroxDebugManager.cs @@ -17,7 +17,7 @@ public class NitroxDebugManager : MonoBehaviour private readonly HashSet prevActiveDebuggers = []; private List debuggers; - private bool showDebuggerList; + private bool showDebuggerList = true; private bool isDebugging; private Rect windowRect; From 911c10325da3766386c3f8608cb2b117efa27c17 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:22:06 -0700 Subject: [PATCH 27/59] Add basic VS Code config (#2678) --- .vscode/extensions.json | 8 ++++++++ .vscode/launch.json | 32 ++++++++++++++++++++++++++++++++ .vscode/settings.json | 4 ++++ 3 files changed, 44 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..8cb56582a5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "avaloniateam.vscode-avalonia", + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + "ms-dotnettools.vscode-dotnet-runtime" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..b56419025f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Nitrox.Launcher", + "type": "dotnet", + "request": "launch", + "preLaunchTask": "dotnet: build", + "projectPath": "${workspaceFolder}/Nitrox.Launcher/Nitrox.Launcher.csproj" + }, + { + "name": "Nitrox.Server.Subnautica", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "dotnet: build", + "program": "", + "args": [], + "cwd": "${workspaceFolder}/Nitrox.Server.Subnautica/", + "stopAtEntry": false, + "console": "externalTerminal", + "windows": { + "program": "${workspaceFolder}/Nitrox.Server.Subnautica/bin/Debug/net10.0/Nitrox.Server.Subnautica.exe" + }, + "linux": { + "program": "${workspaceFolder}/Nitrox.Server.Subnautica/bin/Debug/net10.0/Nitrox.Server.Subnautica" + }, + "osx": { + "program": "${workspaceFolder}/Nitrox.Server.Subnautica/bin/Debug/net10.0/Nitrox.Server.Subnautica" + } + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..ca098724f7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "files.autoSave": "afterDelay", + "editor.bracketPairColorization.enabled": true +} \ No newline at end of file From d92f28d9308168d451de82c038e44951abf68c2a Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:48:33 -0700 Subject: [PATCH 28/59] Improved debuggers (#2677) --- NitroxClient/ClientAutoFacRegistrar.cs | 4 +- NitroxClient/Debuggers/AbstractDebugger.cs | 260 ++++++++++++ NitroxClient/Debuggers/BaseDebugger.cs | 270 ------------- NitroxClient/Debuggers/EntityDebugger.cs | 161 ++++---- NitroxClient/Debuggers/NetworkDebugger.cs | 373 +++++++++--------- NitroxClient/Debuggers/SceneDebugger.cs | 16 +- NitroxClient/Debuggers/SceneExtraDebugger.cs | 70 +--- NitroxClient/Debuggers/SoundDebugger.cs | 14 +- .../MonoBehaviours/NitroxDebugManager.cs | 51 ++- 9 files changed, 595 insertions(+), 624 deletions(-) create mode 100644 NitroxClient/Debuggers/AbstractDebugger.cs delete mode 100644 NitroxClient/Debuggers/BaseDebugger.cs diff --git a/NitroxClient/ClientAutoFacRegistrar.cs b/NitroxClient/ClientAutoFacRegistrar.cs index aad4b11bd0..26baecd377 100644 --- a/NitroxClient/ClientAutoFacRegistrar.cs +++ b/NitroxClient/ClientAutoFacRegistrar.cs @@ -55,8 +55,8 @@ private void RegisterCoreDependencies(ContainerBuilder containerBuilder) { #if DEBUG containerBuilder.RegisterAssemblyTypes(currentAssembly) - .AssignableTo() - .As() + .AssignableTo() + .As() .AsImplementedInterfaces() .AsSelf() .SingleInstance(); diff --git a/NitroxClient/Debuggers/AbstractDebugger.cs b/NitroxClient/Debuggers/AbstractDebugger.cs new file mode 100644 index 0000000000..af3b8b02d7 --- /dev/null +++ b/NitroxClient/Debuggers/AbstractDebugger.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Nitrox.Model.DataStructures; +using Nitrox.Model.Helper; +using NitroxClient.Unity.Helper; +using UnityEngine; + +namespace NitroxClient.Debuggers; + +[ExcludeFromCodeCoverage] +public abstract class AbstractDebugger +{ + public virtual bool Enabled { get; set; } + + public int WindowId { get; } + + public string? DebuggerName { get; } + + public KeyCode Hotkey { get; } + + public bool HotkeyAltRequired { get; } + + public bool HotkeyControlRequired { get; } + + public bool HotkeyShiftRequired { get; } + + public string HotkeyString { get; } + + protected GUISkinCreationOptions SkinCreationOptions { get; } + + protected float MaxHeight { get; } + + protected DebuggerTab? ActiveTab { get; set; } + + private readonly Dictionary tabs = []; + private Rect windowRect; + + protected AbstractDebugger(int windowId, float desiredWidth, string? debuggerName = null, KeyCode hotkey = KeyCode.None, bool control = false, bool shift = false, bool alt = false, GUISkinCreationOptions skinOptions = GUISkinCreationOptions.DEFAULT, float maxHeight = 1000f) + { + WindowId = windowId; + MaxHeight = maxHeight; + + if (desiredWidth < 200) + { + desiredWidth = 200; + } + + windowRect = new Rect(Screen.width / 2 - (desiredWidth / 2), Screen.height * 0.1f, desiredWidth, Math.Min(Screen.height * 0.8f, maxHeight)); // Default position in center of screen. + + Hotkey = hotkey; + HotkeyAltRequired = alt; + HotkeyShiftRequired = shift; + HotkeyControlRequired = control; + HotkeyString = Hotkey == KeyCode.None ? "None" : $"{(HotkeyControlRequired ? "CTRL+" : "")}{(HotkeyAltRequired ? "ALT+" : "")}{(HotkeyShiftRequired ? "SHIFT+" : "")}{Hotkey}"; + SkinCreationOptions = skinOptions; + + if (string.IsNullOrEmpty(debuggerName)) + { + string name = GetType().Name; + DebuggerName = name.Substring(0, name.IndexOf("Debugger", StringComparison.Ordinal)); + } + else + { + DebuggerName = debuggerName; + } + } + + protected DebuggerTab AddTab(string name, Action render) + { + DebuggerTab tab = new(name, render); + tabs.Add(tab.Name, tab); + return tab; + } + + protected Optional GetTab([NotNull] string? name) + { + Validate.NotNull(name); + + tabs.TryGetValue(name, out DebuggerTab tab); + return Optional.OfNullable(tab); + } + + /// + /// Call this inside a method. + /// + public virtual void OnGUI() + { + if (!Enabled) + { + return; + } + + GUISkin skin = GetSkin(); + GUISkinUtils.RenderWithSkin(skin, () => + { + windowRect = GUILayout.Window(WindowId, windowRect, RenderInternal, $"[DEBUGGER] {DebuggerName}", GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); + }); + } + + /// + /// Optionally adjust the skin that is used during render. + /// + /// + /// Set on or in constructor before using this method. + /// + /// Skin that is being used during . + protected virtual void OnSetSkin(GUISkin skin) + { + } + + /// + /// Optionally use a custom render solution for the debugger by overriding this method. + /// + protected virtual void Render() + { + ActiveTab?.Render(); + } + + /// + /// Gets (a copy of) a skin specified by . + /// + /// A reference to an existing or copied skin. + private GUISkin GetSkin() + { + GUISkin skin = GUI.skin; + string skinName = GetSkinName(); + switch (SkinCreationOptions) + { + case GUISkinCreationOptions.DEFAULT: + skin = GUISkinUtils.RegisterDerivedOnce("debuggers.default", s => + { + SetBaseStyle(s); + OnSetSkinImpl(s); + }); + break; + + case GUISkinCreationOptions.UNITYCOPY: + skin = GUISkinUtils.RegisterDerivedOnce(skinName, OnSetSkinImpl); + break; + + case GUISkinCreationOptions.DERIVEDCOPY: + GUISkin baseSkin = GUISkinUtils.RegisterDerivedOnce("debuggers.default", SetBaseStyle); + skin = GUISkinUtils.RegisterDerivedOnce(skinName, OnSetSkinImpl, baseSkin); + break; + } + + return skin; + } + + private string GetSkinName() + { + string name = GetType().Name; + return $"debuggers.{name.Substring(0, name.IndexOf("Debugger")).ToLowerInvariant()}"; + } + + private void OnSetSkinImpl(GUISkin skin) + { + if (SkinCreationOptions == GUISkinCreationOptions.DEFAULT) + { + Enabled = false; + throw new NotSupportedException($"Cannot change {nameof(GUISkin)} for {GetType().FullName} when accessing the default skin. Change {nameof(SkinCreationOptions)} to something else than {nameof(GUISkinCreationOptions.DEFAULT)}."); + } + + OnSetSkin(skin); + } + + private void RenderInternal(int windowId) + { + using (new GUILayout.HorizontalScope("Box")) + { + if (tabs.Count == 1) + { + GUILayout.Label(tabs.First().Key, "tabActive"); + } + else + { + foreach (DebuggerTab tab in tabs.Values) + { + if (GUILayout.Button(tab.Name, ActiveTab == tab ? "tabActive" : "tab")) + { + ActiveTab = tab; + } + } + } + } + + Render(); + GUI.DragWindow(); + } + + private void SetBaseStyle(GUISkin skin) + { + skin.label.alignment = TextAnchor.MiddleLeft; + skin.label.margin = new RectOffset(); + skin.label.padding = new RectOffset(); + + skin.SetCustomStyle("header", skin.label, s => + { + s.margin.top = 10; + s.margin.bottom = 10; + s.alignment = TextAnchor.MiddleCenter; + s.fontSize = 16; + s.fontStyle = FontStyle.Bold; + }); + + skin.SetCustomStyle("tab", skin.button, s => + { + s.fontSize = 16; + s.margin = new RectOffset(5, 5, 5, 5); + }); + + skin.SetCustomStyle("tabActive", skin.button, s => + { + s.fontStyle = FontStyle.Bold; + s.fontSize = 16; + }); + } + + public virtual void ResetWindowPosition() + { + // Reset position of debuggers because SN sometimes throws the windows from planet 4546B + windowRect = new Rect(Screen.width / 2f - (windowRect.width / 2), Screen.height / 2f - (windowRect.height / 2), windowRect.width, Math.Min(Screen.height * 0.8f, MaxHeight)); + } + + public enum GUISkinCreationOptions + { + /// + /// Uses the NitroxDebug skin. + /// + DEFAULT, + + /// + /// Creates a copy of the default Unity IMGUI skin and sets the copied skin as render skin. + /// + UNITYCOPY, + + /// + /// Creates a copy based on the NitroxDebug skin and sets the copied skin as render skin. + /// + DERIVEDCOPY + } + + public sealed class DebuggerTab + { + public string Name { get; } + + public Action Render { get; } + + public DebuggerTab(string name, Action render) + { + Validate.NotNull(name, $"Expected a name for the {nameof(DebuggerTab)}"); + Validate.NotNull(render, $"Expected an action for the {nameof(DebuggerTab)}"); + + Name = name; + Render = render; + } + } +} diff --git a/NitroxClient/Debuggers/BaseDebugger.cs b/NitroxClient/Debuggers/BaseDebugger.cs deleted file mode 100644 index f86435d9e7..0000000000 --- a/NitroxClient/Debuggers/BaseDebugger.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Nitrox.Model.DataStructures; -using NitroxClient.Unity.Helper; -using Nitrox.Model.Helper; -using UnityEngine; - -namespace NitroxClient.Debuggers -{ - [ExcludeFromCodeCoverage] - public abstract class BaseDebugger - { - public readonly string DebuggerName; - public readonly KeyCode Hotkey; - public readonly bool HotkeyAltRequired; - public readonly bool HotkeyControlRequired; - public readonly bool HotkeyShiftRequired; - public readonly string HotkeyString; - public readonly GUISkinCreationOptions SkinCreationOptions; - - /// - /// Currently active tab. This is the index used with . - /// - public DebuggerTab ActiveTab; - - public bool CanDragWindow = true; - - public virtual bool Enabled { get; set; } - - public Rect WindowRect; - - /// - /// Optional rendered tabs of the current debugger. - /// - /// - /// gives the index of the currently selected tab. - /// - private readonly Dictionary tabs = new Dictionary(); - - private float maxHeight; - - protected BaseDebugger(float desiredWidth, string debuggerName = null, KeyCode hotkey = KeyCode.None, bool control = false, bool shift = false, bool alt = false, GUISkinCreationOptions skinOptions = GUISkinCreationOptions.DEFAULT, float maxHeight = 1000f) - { - this.maxHeight = maxHeight; - - if (desiredWidth < 200) - { - desiredWidth = 200; - } - - WindowRect = new Rect(Screen.width / 2 - (desiredWidth / 2), Screen.height * 0.1f, desiredWidth, Math.Min(Screen.height * 0.8f, maxHeight)); // Default position in center of screen. - - Hotkey = hotkey; - HotkeyAltRequired = alt; - HotkeyShiftRequired = shift; - HotkeyControlRequired = control; - HotkeyString = Hotkey == KeyCode.None ? "None" : $"{(HotkeyControlRequired ? "CTRL+" : "")}{(HotkeyAltRequired ? "ALT+" : "")}{(HotkeyShiftRequired ? "SHIFT+" : "")}{Hotkey}"; - SkinCreationOptions = skinOptions; - - if (string.IsNullOrEmpty(debuggerName)) - { - string name = GetType().Name; - DebuggerName = name.Substring(0, name.IndexOf("Debugger", StringComparison.Ordinal)); - } - else - { - DebuggerName = debuggerName; - } - } - - public enum GUISkinCreationOptions - { - /// - /// Uses the NitroxDebug skin. - /// - DEFAULT, - - /// - /// Creates a copy of the default Unity IMGUI skin and sets the copied skin as render skin. - /// - UNITYCOPY, - - /// - /// Creates a copy based on the NitroxDebug skin and sets the copied skin as render skin. - /// - DERIVEDCOPY - } - - protected DebuggerTab AddTab(string name, Action render) - { - DebuggerTab tab = new(name, render); - tabs.Add(tab.Name, tab); - return tab; - } - - protected Optional GetTab(string name) - { - Validate.NotNull(name); - - tabs.TryGetValue(name, out DebuggerTab tab); - return Optional.OfNullable(tab); - } - - public virtual void Update() - { - // Defaults to a no-op but can be overriden - } - - /// - /// Call this inside a method. - /// - public virtual void OnGUI() - { - if (!Enabled) - { - return; - } - - GUISkin skin = GetSkin(); - GUISkinUtils.RenderWithSkin(skin, () => - { - WindowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Keyboard), WindowRect, RenderInternal, $"[DEBUGGER] {DebuggerName}", GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); - }); - } - - /// - /// Optionally adjust the skin that is used during render. - /// - /// - /// Set on or in constructor before using this method. - /// - /// Skin that is being used during . - protected virtual void OnSetSkin(GUISkin skin) - { - } - - /// - /// Optionally use a custom render solution for the debugger by overriding this method. - /// - protected virtual void Render() - { - ActiveTab?.Render(); - } - - /// - /// Gets (a copy of) a skin specified by . - /// - /// A reference to an existing or copied skin. - private GUISkin GetSkin() - { - GUISkin skin = GUI.skin; - string skinName = GetSkinName(); - switch (SkinCreationOptions) - { - case GUISkinCreationOptions.DEFAULT: - skin = GUISkinUtils.RegisterDerivedOnce("debuggers.default", s => - { - SetBaseStyle(s); - OnSetSkinImpl(s); - }); - break; - - case GUISkinCreationOptions.UNITYCOPY: - skin = GUISkinUtils.RegisterDerivedOnce(skinName, OnSetSkinImpl); - break; - - case GUISkinCreationOptions.DERIVEDCOPY: - GUISkin baseSkin = GUISkinUtils.RegisterDerivedOnce("debuggers.default", SetBaseStyle); - skin = GUISkinUtils.RegisterDerivedOnce(skinName, OnSetSkinImpl, baseSkin); - break; - } - - return skin; - } - - private string GetSkinName() - { - string name = GetType().Name; - return $"debuggers.{name.Substring(0, name.IndexOf("Debugger")).ToLowerInvariant()}"; - } - - private void OnSetSkinImpl(GUISkin skin) - { - if (SkinCreationOptions == GUISkinCreationOptions.DEFAULT) - { - Enabled = false; - throw new NotSupportedException($"Cannot change {nameof(GUISkin)} for {GetType().FullName} when accessing the default skin. Change {nameof(SkinCreationOptions)} to something else than {nameof(GUISkinCreationOptions.DEFAULT)}."); - } - - OnSetSkin(skin); - } - - private void RenderInternal(int windowId) - { - using (new GUILayout.HorizontalScope("Box")) - { - if (tabs.Count == 1) - { - GUILayout.Label(tabs.First().Key, "tabActive"); - } - else - { - foreach (DebuggerTab tab in tabs.Values) - { - if (GUILayout.Button(tab.Name, ActiveTab == tab ? "tabActive" : "tab")) - { - ActiveTab = tab; - } - } - } - } - - Render(); - if (CanDragWindow) - { - GUI.DragWindow(); - } - } - - private void SetBaseStyle(GUISkin skin) - { - skin.label.alignment = TextAnchor.MiddleLeft; - skin.label.margin = new RectOffset(); - skin.label.padding = new RectOffset(); - - skin.SetCustomStyle("header", skin.label, s => - { - s.margin.top = 10; - s.margin.bottom = 10; - s.alignment = TextAnchor.MiddleCenter; - s.fontSize = 16; - s.fontStyle = FontStyle.Bold; - }); - - skin.SetCustomStyle("tab", skin.button, s => - { - s.fontSize = 16; - s.margin = new RectOffset(5, 5, 5, 5); - }); - - skin.SetCustomStyle("tabActive", skin.button, s => - { - s.fontStyle = FontStyle.Bold; - s.fontSize = 16; - }); - } - - public virtual void ResetWindowPosition() - { - WindowRect = new Rect(Screen.width / 2f - (WindowRect.width / 2), Screen.height / 2f - (WindowRect.height / 2), WindowRect.width, Math.Min(Screen.height * 0.8f, maxHeight)); //Reset position of debuggers because SN sometimes throws the windows from planet 4546B - } - - public class DebuggerTab - { - public DebuggerTab(string name, Action render) - { - Validate.NotNull(name, $"Expected a name for the {nameof(DebuggerTab)}"); - Validate.NotNull(render, $"Expected an action for the {nameof(DebuggerTab)}"); - - Name = name; - Render = render; - } - - public string Name { get; } - public Action Render { get; } - } - } -} diff --git a/NitroxClient/Debuggers/EntityDebugger.cs b/NitroxClient/Debuggers/EntityDebugger.cs index afa3b7c91f..9f54e65cdc 100644 --- a/NitroxClient/Debuggers/EntityDebugger.cs +++ b/NitroxClient/Debuggers/EntityDebugger.cs @@ -1,120 +1,121 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; +using NitroxClient.MonoBehaviours; using UnityEngine; -namespace NitroxClient.Debuggers +namespace NitroxClient.Debuggers; + +[ExcludeFromCodeCoverage] +public class EntityDebugger : AbstractDebugger { - [ExcludeFromCodeCoverage] - public class EntityDebugger : BaseDebugger + private const int WINDOW_ID = 421; + private const float TEXT_X_OFFSET = 20f; + + private static Color labelBgColor = new(0.2f, 0.2f, 0.2f, 0.5f); + private static Color labelFgColor = Color.white; + private static Texture2D? lineTex; + + private static readonly List rects = []; + + public EntityDebugger() : base(WINDOW_ID, 200, null, KeyCode.E, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) { - private static Color labelBgColor = new Color(0.2f, 0.2f, 0.2f, 0.5f); - private static Color labelFgColor = Color.white; - private static float textXOffset = 20f; - private static Texture2D lineTex; + ActiveTab = AddTab("EntityDebugger", RenderEntityDebugger); + } - private static List rects; + private static void RenderEntityDebugger() + { - public EntityDebugger() : base(200, null, KeyCode.E, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) - { - ActiveTab = AddTab("EntityDebugger", RenderEntityDebugger); - rects = new List(); - } + } - private void RenderEntityDebugger() + public override void OnGUI() + { + if (!Enabled) { + return; } - public override void OnGUI() + rects.Clear(); + foreach (KeyValuePair gameObjectPairs in NitroxEntity.GetGameObjects()) { - if (!Enabled) + NitroxId id = gameObjectPairs.Key; + GameObject gameObject = gameObjectPairs.Value; + if (gameObject == null || gameObject == Player.mainObject) { - return; + continue; } - rects.Clear(); - foreach (KeyValuePair gameObjectPairs in NitroxEntity.GetGameObjects()) + Vector3 screenPos = Player.main.viewModelCamera.WorldToScreenPoint(gameObject.transform.position); + if (screenPos.z > 0 && screenPos.z < 20 && + screenPos.x >= 0 && screenPos.x < Screen.width && + screenPos.y >= 0 && screenPos.y < Screen.height) { - NitroxId id = gameObjectPairs.Key; - GameObject gameObject = gameObjectPairs.Value; - if (gameObject == null || gameObject == Player.mainObject) - { - continue; - } - - Vector3 screenPos = Player.main.viewModelCamera.WorldToScreenPoint(gameObject.transform.position); - if (screenPos.z > 0 && screenPos.z < 20 && - screenPos.x >= 0 && screenPos.x < Screen.width && - screenPos.y >= 0 && screenPos.y < Screen.height) + GUIStyle style = GUI.skin.label; + GUIContent textContent = new($"ID {id} NAME {gameObject.name}"); + Vector2 size = style.CalcSize(textContent); + size += new Vector2(10f, 0f); //for box edges + + Vector2 pointLocation = new(screenPos.x, Screen.height - screenPos.y); + Rect drawSize = new(screenPos.x + TEXT_X_OFFSET, Screen.height - screenPos.y, size.x, size.y); + while (true) { - GUIStyle style = GUI.skin.label; - GUIContent textContent = new($"ID {id} NAME {gameObject.name}"); - Vector2 size = style.CalcSize(textContent); - size += new Vector2(10f, 0f); //for box edges - - Vector2 pointLocation = new Vector2(screenPos.x, Screen.height - screenPos.y); - Rect drawSize = new Rect(screenPos.x + textXOffset, Screen.height - screenPos.y, size.x, size.y); - while (true) + bool finished = true; + foreach (Rect rect in rects) { - bool finished = true; - foreach (Rect rect in rects) - { - if (rect.Overlaps(drawSize)) - { - drawSize.x = rect.x; - drawSize.y = rect.y + rect.height; - finished = false; - break; - } - } - if (finished) + if (rect.Overlaps(drawSize)) { + drawSize.x = rect.x; + drawSize.y = rect.y + rect.height; + finished = false; break; } } + if (finished) + { + break; + } + } - DrawLine(pointLocation, new Vector2(drawSize.x, drawSize.y + size.y / 2), labelFgColor, 2f); + DrawLine(pointLocation, new Vector2(drawSize.x, drawSize.y + size.y / 2), labelFgColor, 2f); - rects.Add(drawSize); + rects.Add(drawSize); - Color oldBgColor = GUI.backgroundColor; - GUI.backgroundColor = labelBgColor; - GUI.color = labelFgColor; - GUI.Box(drawSize, textContent); + Color oldBgColor = GUI.backgroundColor; + GUI.backgroundColor = labelBgColor; + GUI.color = labelFgColor; + GUI.Box(drawSize, textContent); - GUI.backgroundColor = oldBgColor; - } + GUI.backgroundColor = oldBgColor; } } + } - private static void DrawLine(Vector2 pointA, Vector2 pointB, Color color, float width) - { - Matrix4x4 matrix = GUI.matrix; + private static void DrawLine(Vector2 pointA, Vector2 pointB, Color color, float width) + { + Matrix4x4 matrix = GUI.matrix; - if (!lineTex) - { - lineTex = new Texture2D(1, 1); - } + if (!lineTex) + { + lineTex = new Texture2D(1, 1); + } - Color savedColor = GUI.color; - GUI.color = color; + Color savedColor = GUI.color; + GUI.color = color; - float angle = Vector3.Angle(pointB - pointA, Vector2.right); + float angle = Vector3.Angle(pointB - pointA, Vector2.right); - if (pointA.y > pointB.y) - { - angle = -angle; - } + if (pointA.y > pointB.y) + { + angle = -angle; + } - GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f)); + GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f)); - GUIUtility.RotateAroundPivot(angle, pointA); + GUIUtility.RotateAroundPivot(angle, pointA); - GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), lineTex); + GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), lineTex); - GUI.matrix = matrix; - GUI.color = savedColor; - } + GUI.matrix = matrix; + GUI.color = savedColor; } } diff --git a/NitroxClient/Debuggers/NetworkDebugger.cs b/NitroxClient/Debuggers/NetworkDebugger.cs index b0afebea3c..a38bf52316 100644 --- a/NitroxClient/Debuggers/NetworkDebugger.cs +++ b/NitroxClient/Debuggers/NetworkDebugger.cs @@ -2,258 +2,259 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using NitroxClient.Unity.Helper; using Nitrox.Model.Packets; using Nitrox.Model.Subnautica.Packets; +using NitroxClient.Unity.Helper; using UnityEngine; -namespace NitroxClient.Debuggers +namespace NitroxClient.Debuggers; + +[ExcludeFromCodeCoverage] +public class NetworkDebugger : AbstractDebugger, INetworkDebugger { - [ExcludeFromCodeCoverage] - public class NetworkDebugger : BaseDebugger, INetworkDebugger - { - private const int PACKET_STORED_COUNT = 100; - private readonly Dictionary countByType = new Dictionary(); + private const int WINDOW_ID = 422; + private const int PACKET_STORED_COUNT = 100; - private readonly List filter = new() - { - nameof(PlayerMovement), nameof(EntityTransformUpdates), nameof(PlayerStats), nameof(SpawnEntities), nameof(VehicleMovements), nameof(PlayerCinematicControllerCall), - nameof(FMODAssetPacket), nameof(FMODEventInstancePacket), nameof(FMODCustomEmitterPacket), nameof(FMODStudioEmitterPacket), nameof(FMODCustomLoopingEmitterPacket), - nameof(SimulationOwnershipChange), nameof(CellVisibilityChanged), nameof(PlayerInCyclopsMovement), nameof(CreatureActionChanged), nameof(FootstepPacket), nameof(GrapplingHookMovement) - }; - private readonly List packets = new List(PACKET_STORED_COUNT); + private readonly Dictionary countByType = []; - // vs blacklist - private bool isWhitelist; - private Vector2 scrollPosition; + private readonly List filter = + [ + nameof(PlayerMovement), nameof(EntityTransformUpdates), nameof(PlayerStats), nameof(SpawnEntities), nameof(VehicleMovements), nameof(PlayerCinematicControllerCall), + nameof(FMODAssetPacket), nameof(FMODEventInstancePacket), nameof(FMODCustomEmitterPacket), nameof(FMODStudioEmitterPacket), nameof(FMODCustomLoopingEmitterPacket), + nameof(SimulationOwnershipChange), nameof(CellVisibilityChanged), nameof(PlayerInCyclopsMovement), nameof(CreatureActionChanged), nameof(FootstepPacket), nameof(GrapplingHookMovement) + ]; + private readonly List packets = new(PACKET_STORED_COUNT); - private int receivedCount; - private int sentCount; + // vs blacklist + private bool isWhitelist; + private Vector2 scrollPosition; - private uint receivedBytes; - private uint sentBytes; + private int receivedCount; + private int sentCount; - public NetworkDebugger() : base(600, null, KeyCode.N, true, false, false, GUISkinCreationOptions.DERIVEDCOPY, 330) - { - ActiveTab = AddTab("All", RenderTabPackets); - AddTab("Sent", RenderTabSentPackets); - AddTab("Received", RenderTabReceivedPackets); - AddTab("Type Count", RenderTabTypeCount); - AddTab("Filter", RenderTabFilter); - } + private uint receivedBytes; + private uint sentBytes; - public void PacketSent(Packet packet, int byteSize) - { - AddPacket(packet, true); - sentCount++; - sentBytes += (uint)byteSize; - } + public NetworkDebugger() : base(WINDOW_ID, 600, null, KeyCode.N, true, false, false, GUISkinCreationOptions.DERIVEDCOPY, 330) + { + ActiveTab = AddTab("All", RenderTabPackets); + AddTab("Sent", RenderTabSentPackets); + AddTab("Received", RenderTabReceivedPackets); + AddTab("Type Count", RenderTabTypeCount); + AddTab("Filter", RenderTabFilter); + } - public void PacketReceived(Packet packet, int byteSize) - { - AddPacket(packet, false); - receivedCount++; - receivedBytes += (uint)byteSize; - } + public void PacketSent(Packet packet, int byteSize) + { + AddPacket(packet, true); + sentCount++; + sentBytes += (uint)byteSize; + } - protected override void OnSetSkin(GUISkin skin) - { - base.OnSetSkin(skin); - - skin.SetCustomStyle("packet-type-down", - skin.label, - s => - { - s.normal = new GUIStyleState { textColor = Color.green }; - s.fontStyle = FontStyle.Bold; - s.alignment = TextAnchor.MiddleLeft; - }); - - skin.SetCustomStyle("packet-type-up", - skin.label, - s => - { - s.normal = new GUIStyleState { textColor = Color.red }; - s.fontStyle = FontStyle.Bold; - s.alignment = TextAnchor.MiddleLeft; - }); - } + public void PacketReceived(Packet packet, int byteSize) + { + AddPacket(packet, false); + receivedCount++; + receivedBytes += (uint)byteSize; + } + + protected override void OnSetSkin(GUISkin skin) + { + base.OnSetSkin(skin); + + skin.SetCustomStyle("packet-type-down", + skin.label, + s => + { + s.normal = new GUIStyleState { textColor = Color.green }; + s.fontStyle = FontStyle.Bold; + s.alignment = TextAnchor.MiddleLeft; + }); + + skin.SetCustomStyle("packet-type-up", + skin.label, + s => + { + s.normal = new GUIStyleState { textColor = Color.red }; + s.fontStyle = FontStyle.Bold; + s.alignment = TextAnchor.MiddleLeft; + }); + } - private void RenderTabPackets() + private void RenderTabPackets() + { + using (new GUILayout.VerticalScope("Box")) { - using (new GUILayout.VerticalScope("Box")) - { - RenderPacketTotals(); + RenderPacketTotals(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); - RenderPacketList(ToRender.BOTH); - GUILayout.EndScrollView(); - } + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); + RenderPacketList(ToRender.BOTH); + GUILayout.EndScrollView(); } + } - private void RenderTabSentPackets() + private void RenderTabSentPackets() + { + using (new GUILayout.VerticalScope("Box")) { - using (new GUILayout.VerticalScope("Box")) - { - RenderPacketTotals(); + RenderPacketTotals(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); - RenderPacketList(ToRender.SENT); - GUILayout.EndScrollView(); - } + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); + RenderPacketList(ToRender.SENT); + GUILayout.EndScrollView(); } + } - private void RenderTabReceivedPackets() + private void RenderTabReceivedPackets() + { + using (new GUILayout.VerticalScope("Box")) { - using (new GUILayout.VerticalScope("Box")) - { - RenderPacketTotals(); + RenderPacketTotals(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); - RenderPacketList(ToRender.RECEIVED); - GUILayout.EndScrollView(); - } + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); + RenderPacketList(ToRender.RECEIVED); + GUILayout.EndScrollView(); } + } - private void RenderTabTypeCount() + private void RenderTabTypeCount() + { + using (new GUILayout.VerticalScope("Box")) { - using (new GUILayout.VerticalScope("Box")) - { - RenderPacketTotals(); + RenderPacketTotals(); - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); - foreach (KeyValuePair kv in countByType.OrderBy(e => -e.Value)) // descending - { - GUILayout.Label($"{kv.Key.Name}: {kv.Value}"); - } - GUILayout.EndScrollView(); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); + foreach (KeyValuePair kv in countByType.OrderBy(e => -e.Value)) // descending + { + GUILayout.Label($"{kv.Key.Name}: {kv.Value}"); } + GUILayout.EndScrollView(); } + } - private void RenderTabFilter() + private void RenderTabFilter() + { + using (new GUILayout.VerticalScope("Box")) { - using (new GUILayout.VerticalScope("Box")) + RenderPacketTotals(); + using (new GUILayout.HorizontalScope()) { - RenderPacketTotals(); - using (new GUILayout.HorizontalScope()) + isWhitelist = GUILayout.Toggle(isWhitelist, "Is Whitelist"); + if (GUILayout.Button("Clear")) { - isWhitelist = GUILayout.Toggle(isWhitelist, "Is Whitelist"); - if (GUILayout.Button("Clear")) - { - filter.Clear(); - } + filter.Clear(); } + } - scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); - for (int i = 0; i < filter.Count; i++) - { - filter[i] = GUILayout.TextField(filter[i]); - } - string n = GUILayout.TextField(""); - if (n != "") - { - filter.Add(n); - } - GUILayout.EndScrollView(); + scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); + for (int i = 0; i < filter.Count; i++) + { + filter[i] = GUILayout.TextField(filter[i]); } + string n = GUILayout.TextField(""); + if (n != "") + { + filter.Add(n); + } + GUILayout.EndScrollView(); } + } - private void RenderPacketTotals() - { - GUILayout.Label($"Sent: {sentCount} ({sentBytes.AsByteUnitText()}) - Received: {receivedCount} ({receivedBytes.AsByteUnitText()})"); - } + private void RenderPacketTotals() + { + GUILayout.Label($"Sent: {sentCount} ({sentBytes.AsByteUnitText()}) - Received: {receivedCount} ({receivedBytes.AsByteUnitText()})"); + } + + private void RenderPacketList(ToRender toRender) + { + bool isSentList = toRender.HasFlag(ToRender.SENT); + bool isReceiveList = toRender.HasFlag(ToRender.RECEIVED); + PacketPrefixer prefixer = isSentList && isReceiveList ? PacketDirectionPrefixer : PacketNoopPrefixer; - private void RenderPacketList(ToRender toRender) + for (int i = packets.Count - 1; i >= 0; i--) { - bool isSentList = toRender.HasFlag(ToRender.SENT); - bool isReceiveList = toRender.HasFlag(ToRender.RECEIVED); - PacketPrefixer prefixer = isSentList && isReceiveList ? (PacketPrefixer)PacketDirectionPrefixer : PacketNoopPrefixer; + PacketDebugWrapper wrapper = packets[i]; + if (wrapper.IsSent && !isSentList) + { + continue; + } + if (!wrapper.IsSent && !isReceiveList) + { + continue; + } - for (int i = packets.Count - 1; i >= 0; i--) + using (new GUILayout.VerticalScope("Box")) { - PacketDebugWrapper wrapper = packets[i]; - if (wrapper.IsSent && !isSentList) - { - continue; - } - if (!wrapper.IsSent && !isReceiveList) + using (new GUILayout.HorizontalScope()) { - continue; + wrapper.ShowDetails = GUILayout.Toggle(wrapper.ShowDetails, "", GUILayout.Width(20), GUILayout.Height(20)); + GUILayout.Label($"{prefixer(wrapper)}{wrapper.Packet.GetType().FullName}", wrapper.IsSent ? "packet-type-up" : "packet-type-down"); + + packets[i] = wrapper; // Store again because value-type } - using (new GUILayout.VerticalScope("Box")) + if (wrapper.ShowDetails) { - using (new GUILayout.HorizontalScope()) - { - wrapper.ShowDetails = GUILayout.Toggle(wrapper.ShowDetails, "", GUILayout.Width(20), GUILayout.Height(20)); - GUILayout.Label($"{prefixer(wrapper)}{wrapper.Packet.GetType().FullName}", wrapper.IsSent ? "packet-type-up" : "packet-type-down"); - - packets[i] = wrapper; // Store again because value-type - } - - if (wrapper.ShowDetails) - { - GUILayout.Label(wrapper.Packet.ToString()); - } + GUILayout.Label(wrapper.Packet.ToString()); } } } + } - private void AddPacket(Packet packet, bool isSent) + private void AddPacket(Packet packet, bool isSent) + { + Type packetType = packet.GetType(); + if (isWhitelist == filter.Contains(packetType.Name, StringComparer.InvariantCultureIgnoreCase)) { - Type packetType = packet.GetType(); - if (isWhitelist == filter.Contains(packetType.Name, StringComparer.InvariantCultureIgnoreCase)) - { - packets.Add(new PacketDebugWrapper(packet, isSent, false)); - if (packets.Count > PACKET_STORED_COUNT) - { - packets.RemoveAt(0); - } - } - - if (countByType.TryGetValue(packetType, out int count)) - { - countByType[packetType] = count + 1; - } - else + packets.Add(new PacketDebugWrapper(packet, isSent, false)); + if (packets.Count > PACKET_STORED_COUNT) { - countByType.Add(packetType, 1); + packets.RemoveAt(0); } } - private string PacketDirectionPrefixer(PacketDebugWrapper wrapper) => $"{(wrapper.IsSent ? "↑" : "↓")} - "; + if (countByType.TryGetValue(packetType, out int count)) + { + countByType[packetType] = count + 1; + } + else + { + countByType.Add(packetType, 1); + } + } - private string PacketNoopPrefixer(PacketDebugWrapper wraper) => ""; + private string PacketDirectionPrefixer(PacketDebugWrapper wrapper) => $"{(wrapper.IsSent ? "↑" : "↓")} - "; - private delegate string PacketPrefixer(PacketDebugWrapper wrapper); + private string PacketNoopPrefixer(PacketDebugWrapper wraper) => ""; - private struct PacketDebugWrapper - { - public readonly Packet Packet; - public readonly bool IsSent; + private delegate string PacketPrefixer(PacketDebugWrapper wrapper); - public bool ShowDetails { get; set; } + private struct PacketDebugWrapper + { + public readonly Packet Packet; + public readonly bool IsSent; - public PacketDebugWrapper(Packet packet, bool isSent, bool showDetails) - { - IsSent = isSent; - Packet = packet; - ShowDetails = showDetails; - } - } + public bool ShowDetails { get; set; } - [Flags] - private enum ToRender + public PacketDebugWrapper(Packet packet, bool isSent, bool showDetails) { - SENT = 1, - RECEIVED = 2, - BOTH = SENT | RECEIVED + IsSent = isSent; + Packet = packet; + ShowDetails = showDetails; } } - public interface INetworkDebugger + [Flags] + private enum ToRender { - void PacketSent(Packet packet, int size); - void PacketReceived(Packet packet, int size); + SENT = 1, + RECEIVED = 2, + BOTH = SENT | RECEIVED } } + +public interface INetworkDebugger +{ + void PacketSent(Packet packet, int size); + void PacketReceived(Packet packet, int size); +} diff --git a/NitroxClient/Debuggers/SceneDebugger.cs b/NitroxClient/Debuggers/SceneDebugger.cs index d876c91ca1..d8677827f8 100644 --- a/NitroxClient/Debuggers/SceneDebugger.cs +++ b/NitroxClient/Debuggers/SceneDebugger.cs @@ -14,8 +14,10 @@ namespace NitroxClient.Debuggers; [ExcludeFromCodeCoverage] -public class SceneDebugger : BaseDebugger +public class SceneDebugger : AbstractDebugger { + private const int WINDOW_ID = 423; + private readonly DrawerManager drawerManager; public GameObject SelectedObject { get; private set; } private int selectedComponentID; @@ -27,12 +29,12 @@ public class SceneDebugger : BaseDebugger private Vector2 gameObjectScrollPos; private Vector2 hierarchyScrollPos; - private readonly Dictionary componentsVisibilityByID = new(); - private readonly Dictionary cachedFieldsByComponentID = new(); - private readonly Dictionary cachedMethodsByComponentID = new(); - private readonly Dictionary> enumVisibilityByComponentIDAndEnumType = new(); + private readonly Dictionary componentsVisibilityByID = []; + private readonly Dictionary cachedFieldsByComponentID = []; + private readonly Dictionary cachedMethodsByComponentID = []; + private readonly Dictionary> enumVisibilityByComponentIDAndEnumType = []; - public SceneDebugger() : base(650, null, KeyCode.S, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) + public SceneDebugger() : base(WINDOW_ID, 650, null, KeyCode.S, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) { drawerManager = new DrawerManager(this); ActiveTab = AddTab("Scenes", RenderTabScenes); @@ -177,7 +179,7 @@ private void RenderTabHierarchy() { using GUILayout.ScrollViewScope scroll = new(hierarchyScrollPos); hierarchyScrollPos = scroll.scrollPosition; - List showObjects = new(); + List showObjects = []; if (!SelectedObject) { showObjects = selectedScene.GetRootGameObjects().ToList(); diff --git a/NitroxClient/Debuggers/SceneExtraDebugger.cs b/NitroxClient/Debuggers/SceneExtraDebugger.cs index bdca30cef3..9f1a534b0f 100644 --- a/NitroxClient/Debuggers/SceneExtraDebugger.cs +++ b/NitroxClient/Debuggers/SceneExtraDebugger.cs @@ -3,19 +3,21 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; -using NitroxClient.MonoBehaviours; -using NitroxClient.Unity.Helper; using Nitrox.Model.DataStructures; -using Nitrox.Model.Helper; using Nitrox.Model.Subnautica.Helper; +using NitroxClient.MonoBehaviours; +using NitroxClient.Unity.Helper; using UnityEngine; using Mathf = UnityEngine.Mathf; namespace NitroxClient.Debuggers; [ExcludeFromCodeCoverage] -public sealed class SceneExtraDebugger : BaseDebugger +public sealed class SceneExtraDebugger : AbstractDebugger { + private const int WINDOW_ID = 424; + private const int RESULTS_PER_PAGE = 30; + private readonly SceneDebugger sceneDebugger; private const KeyCode RAY_CAST_KEY = KeyCode.F9; @@ -27,7 +29,7 @@ public sealed class SceneExtraDebugger : BaseDebugger private string gameObjectSearchCache = string.Empty; private bool gameObjectSearching; private string gameObjectSearchPatternInvalidMessage = string.Empty; - private List gameObjectResults = new(); + private List gameObjectResults = []; private Vector2 hierarchyScrollPos; @@ -35,22 +37,8 @@ public sealed class SceneExtraDebugger : BaseDebugger private const int PAGE_BUTTON_WIDTH = 100; private int searchPageIndex; - private int resultsPerPage = 30; - - public override bool Enabled - { - get => base.Enabled; - set - { - base.Enabled = value; - if (value) - { - MoveOverlappingSceneDebugger(); - } - } - } - public SceneExtraDebugger(SceneDebugger sceneDebugger) : base(350, "Scene Tools", KeyCode.S, true, false, true, GUISkinCreationOptions.DERIVEDCOPY, 700) + public SceneExtraDebugger(SceneDebugger sceneDebugger) : base(WINDOW_ID, 350, "Scene Tools", KeyCode.S, true, false, true, GUISkinCreationOptions.DERIVEDCOPY, 700) { this.sceneDebugger = sceneDebugger; ActiveTab = AddTab("Tools", RenderTabTools); @@ -58,8 +46,6 @@ public SceneExtraDebugger(SceneDebugger sceneDebugger) : base(350, "Scene Tools" // ReSharper disable once Unity.PreferAddressByIdToGraphicsParams circleTexture = new Lazy(() => Resources.Load("Materials/WorldCursor").GetTexture("_MainTex")); arrowTexture = new Lazy(() => Resources.Load("Sprites/Arrow")); - - ResetWindowPosition(); } public override void OnGUI() @@ -80,9 +66,11 @@ private void RenderTabTools() worldMarkerEnabled = !worldMarkerEnabled; } - if (GUILayout.Button($"Ray Casting: {(rayCastingEnabled ? "Active" : "Inactive")}")) + if (GUILayout.Button($"Ray Casting ({RAY_CAST_KEY}): {(rayCastingEnabled ? "Active" : "Inactive")}")) { - Log.InGame($"Ray casting can be enabled/disabled with: {RAY_CAST_KEY}"); + gameObjectSearching = false; + rayCastingEnabled = !rayCastingEnabled; + gameObjectSearch = rayCastingEnabled ? "Ray casting is running" : string.Empty; } } @@ -99,8 +87,8 @@ private void RenderTabTools() { hierarchyScrollPos = scroll.scrollPosition; - int startIndex = resultsPerPage * searchPageIndex; - int endIndex = startIndex + resultsPerPage; + int startIndex = RESULTS_PER_PAGE * searchPageIndex; + int endIndex = startIndex + RESULTS_PER_PAGE; if (endIndex > gameObjectResults.Count) { @@ -129,7 +117,7 @@ private void RenderTabTools() GUILayout.FlexibleSpace(); // Pagination of search results if necessary - if (gameObjectResults.Count > resultsPerPage) + if (gameObjectResults.Count > RESULTS_PER_PAGE) { using (new GUILayout.HorizontalScope("box")) { @@ -147,7 +135,7 @@ private void RenderTabTools() GUI.enabled = true; // Get the maximum page number based on the size of the results - int maxPage = gameObjectResults.Count / resultsPerPage; + int maxPage = gameObjectResults.Count / RESULTS_PER_PAGE; GUILayout.FlexibleSpace(); GUILayout.Label($"Page {searchPageIndex + 1} of {maxPage + 1}", GUILayout.ExpandHeight(true)); @@ -320,32 +308,6 @@ private void GettingRayCastResults() } } - public override void ResetWindowPosition() - { - base.ResetWindowPosition(); - // Align to the right side of the SceneDebugger - WindowRect.x = sceneDebugger.WindowRect.x + sceneDebugger.WindowRect.width; - WindowRect.y = sceneDebugger.WindowRect.y; - - float exceedWidth = WindowRect.x + WindowRect.width - Screen.width; - if (exceedWidth > 0f) - { - WindowRect.x -= exceedWidth; - } - MoveOverlappingSceneDebugger(); - } - - /// - /// Move the scene debugger if it's overlapping with the extra scene debugger (if they can both hold in the available space) - /// - private void MoveOverlappingSceneDebugger() - { - if (sceneDebugger.WindowRect.width + WindowRect.width < Screen.width && // verify that debuggers can hold at the same time in the screen - sceneDebugger.WindowRect.x + sceneDebugger.WindowRect.width + WindowRect.width > Screen.width) // verify that debuggers are really overlapping - { - sceneDebugger.WindowRect.x = Screen.width - WindowRect.width - sceneDebugger.WindowRect.width; - } - } private void UpdateSelectedObjectMarker(Transform selectedTransform) { diff --git a/NitroxClient/Debuggers/SoundDebugger.cs b/NitroxClient/Debuggers/SoundDebugger.cs index 04b0da6c4e..271d25254a 100644 --- a/NitroxClient/Debuggers/SoundDebugger.cs +++ b/NitroxClient/Debuggers/SoundDebugger.cs @@ -4,19 +4,21 @@ using System.Diagnostics.CodeAnalysis; using FMOD.Studio; using FMODUnity; -using NitroxClient.Unity.Helper; using Nitrox.Model.GameLogic.FMOD; +using NitroxClient.Unity.Helper; using UnityEngine; #pragma warning disable 618 namespace NitroxClient.Debuggers; [ExcludeFromCodeCoverage] -public class SoundDebugger : BaseDebugger +public class SoundDebugger : AbstractDebugger { + private const int WINDOW_ID = 425; + private readonly ReadOnlyDictionary assetList; - private readonly Dictionary assetIs3D = new(); - private readonly Dictionary eventInstancesByPath = new(); + private readonly Dictionary assetIs3D = []; + private readonly Dictionary eventInstancesByPath = []; private Vector2 scrollPosition; private string searchText; private string searchCategory; @@ -26,11 +28,11 @@ public class SoundDebugger : BaseDebugger private bool displayIsGlobal; private bool displayWithRadius; - public SoundDebugger(FMODWhitelist fmodWhitelist) : base(700, null, KeyCode.F, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) + public SoundDebugger(FMODWhitelist fmodWhitelist) : base(WINDOW_ID, 700, null, KeyCode.F, true, false, false, GUISkinCreationOptions.DERIVEDCOPY) { assetList = fmodWhitelist.GetWhitelist(); - foreach (KeyValuePair pair in assetList) + foreach (KeyValuePair pair in assetList) { EventInstance evt = FMODUWE.GetEvent(pair.Key); evt.getDescription(out EventDescription description); diff --git a/NitroxClient/MonoBehaviours/NitroxDebugManager.cs b/NitroxClient/MonoBehaviours/NitroxDebugManager.cs index b8fa653508..0d860acbaa 100644 --- a/NitroxClient/MonoBehaviours/NitroxDebugManager.cs +++ b/NitroxClient/MonoBehaviours/NitroxDebugManager.cs @@ -12,10 +12,12 @@ namespace NitroxClient.MonoBehaviours; [ExcludeFromCodeCoverage] public class NitroxDebugManager : MonoBehaviour { + private const int NITROX_DEBUGGER_WINDOW_ID = 420; + private const KeyCode ENABLE_DEBUGGER_HOTKEY = KeyCode.F7; - private readonly HashSet prevActiveDebuggers = []; - private List debuggers; + private readonly HashSet prevActiveDebuggers = []; + private List debuggers; private bool showDebuggerList = true; private bool isDebugging; @@ -23,7 +25,7 @@ public class NitroxDebugManager : MonoBehaviour private void Awake() { - debuggers = NitroxServiceLocator.LocateServicePreLifetime>().ToList(); + debuggers = NitroxServiceLocator.LocateServicePreLifetime>().ToList(); } public static void ToggleCursor() @@ -39,13 +41,14 @@ public void OnGUI() } // Main window to display all available debuggers. - windowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Keyboard), windowRect, DoWindow, "Nitrox debugging"); + windowRect = GUILayout.Window(NITROX_DEBUGGER_WINDOW_ID, windowRect, DoWindow, "Nitrox Debugging"); // Render debugger windows if they are enabled. - foreach (BaseDebugger debugger in debuggers) + foreach (AbstractDebugger debugger in debuggers) { debugger.OnGUI(); } + } public void Update() @@ -62,12 +65,12 @@ public void Update() ToggleCursor(); } - CheckDebuggerHotkeys(); - - foreach (BaseDebugger debugger in debuggers.Where(debugger => debugger.Enabled)) + if (Input.GetKeyDown(KeyCode.R) && Input.GetKey(KeyCode.LeftControl)) { - debugger.Update(); + ResetDebuggers(); } + + CheckDebuggerHotkeys(); } } @@ -76,17 +79,14 @@ public void ToggleDebugging() isDebugging = !isDebugging; if (isDebugging) { - UWE.Utils.PushLockCursor(false); ShowDebuggers(); + UWE.Utils.alwaysLockCursor = false; + UWE.Utils.lockCursor = false; } else { - UWE.Utils.PopLockCursor(); + UWE.Utils.lockCursor = true; HideDebuggers(); - foreach (BaseDebugger baseDebugger in debuggers) - { - baseDebugger.ResetWindowPosition(); - } } } @@ -101,6 +101,11 @@ private void DoWindow(int windowId) ToggleCursor(); } + if (GUILayout.Button("Reset (CTRL+R)")) + { + ResetDebuggers(); + } + if (GUILayout.Button("Show / Hide", GUILayout.Width(100))) { showDebuggerList = !showDebuggerList; @@ -109,7 +114,7 @@ private void DoWindow(int windowId) } if (showDebuggerList) { - foreach (BaseDebugger debugger in debuggers) + foreach (AbstractDebugger debugger in debuggers) { debugger.Enabled = GUILayout.Toggle(debugger.Enabled, $"{debugger.DebuggerName} debugger ({debugger.HotkeyString})"); } @@ -119,7 +124,7 @@ private void DoWindow(int windowId) private void CheckDebuggerHotkeys() { - foreach (BaseDebugger debugger in debuggers) + foreach (AbstractDebugger debugger in debuggers) { if (Input.GetKeyDown(debugger.Hotkey) && Input.GetKey(KeyCode.LeftControl) == debugger.HotkeyControlRequired && Input.GetKey(KeyCode.LeftShift) == debugger.HotkeyShiftRequired && Input.GetKey(KeyCode.LeftAlt) == debugger.HotkeyAltRequired) { @@ -128,9 +133,17 @@ private void CheckDebuggerHotkeys() } } + private void ResetDebuggers() + { + foreach (AbstractDebugger debugger in debuggers) + { + debugger.ResetWindowPosition(); + } + } + private void HideDebuggers() { - foreach (BaseDebugger debugger in GetComponents()) + foreach (AbstractDebugger debugger in GetComponents()) { if (debugger.Enabled) { @@ -143,7 +156,7 @@ private void HideDebuggers() private void ShowDebuggers() { - foreach (BaseDebugger debugger in prevActiveDebuggers) + foreach (AbstractDebugger debugger in prevActiveDebuggers) { debugger.Enabled = true; } From bdb2890750f5714f65770c8c3b9ab014b42c8156 Mon Sep 17 00:00:00 2001 From: Jacqueb-1337 <61178380+Jacqueb-1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:32:52 -0500 Subject: [PATCH 29/59] fix: server-side entity spawn race conditions (#2682) --- .../Entities/Spawning/BatchEntitySpawner.cs | 37 +++++++------------ .../GameLogic/Entities/WorldEntityManager.cs | 9 ++++- .../EntityDestroyedPacketProcessor.cs | 2 +- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs index b1557aabc8..ab46e1608f 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/Spawning/BatchEntitySpawner.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Nitrox.Model.DataStructures; @@ -37,34 +38,28 @@ internal sealed class BatchEntitySpawner( private readonly XorRandom random = randomFactory.GetUnityLikeRandom(); private readonly SubnauticaUweWorldEntityFactory worldEntityFactory = worldEntityFactory; - private readonly Lock parsedBatchesLock = new(); + private readonly ConcurrentDictionary>>> batchLoadTasks = new(); private readonly Lock emptyBatchesLock = new(); - private HashSet parsedBatches = []; public List SerializableParsedBatches { get { - List parsed; List empty; - lock (parsedBatchesLock) - { - parsed = [.. parsedBatches]; - } - lock (emptyBatchesLock) { empty = [.. emptyBatches]; } - return [.. parsed.Except(empty)]; + return [.. batchLoadTasks.Keys.Except(empty)]; } set { - lock (parsedBatchesLock) + batchLoadTasks.Clear(); + foreach (NitroxInt3 batchId in value) { - parsedBatches = [.. value]; + batchLoadTasks.TryAdd(batchId, new Lazy>>(() => Task.FromResult(new List()))); } } } @@ -73,22 +68,16 @@ public List SerializableParsedBatches public bool IsBatchSpawned(NitroxInt3 batchId) { - lock (parsedBatches) - { - return parsedBatches.Contains(batchId); - } + return batchLoadTasks.ContainsKey(batchId); } - public async Task> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool fullCacheCreation = false) + public Task> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool fullCacheCreation = false) { - lock (parsedBatches) - { - if (!parsedBatches.Add(batchId)) - { - return []; - } - } + return batchLoadTasks.GetOrAdd(batchId, id => new Lazy>>(() => LoadBatchInternalAsync(id, fullCacheCreation))).Value; + } + private async Task> LoadBatchInternalAsync(NitroxInt3 batchId, bool fullCacheCreation) + { DeterministicGenerator deterministicBatchGenerator = new(options.Value.Seed, batchId.ToString()); List spawnPoints = batchCellsParser.ParseBatchData(batchId); List entities = await SpawnEntitiesAsync(spawnPoints, deterministicBatchGenerator); @@ -105,7 +94,7 @@ public async Task> LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, b logger.ZLogInformation($"Spawning {entities.Count} entities from {spawnPoints.Count} spawn points in batch {batchId}"); } - for (int x = 0; x < entities.Count; x++) // Throws on duplicate Entities already but nice to know which ones + for (int x = 0; x < entities.Count; x++) { for (int y = 0; y < entities.Count; y++) { diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs index c9b30d0013..7693c2e09f 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/WorldEntityManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -35,6 +36,7 @@ internal sealed class WorldEntityManager private readonly PlayerManager playerManager; private readonly Lock worldEntitiesLock = new(); + private readonly ConcurrentDictionary>> batchRegistrationTasks = new(); /// /// World entities can disappear if you go out of range. @@ -245,7 +247,12 @@ public async Task LoadAllUnspawnedEntitiesAsync(CancellationToken token) } } - public async Task LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool suppressLogs) + public Task LoadUnspawnedEntitiesAsync(NitroxInt3 batchId, bool suppressLogs) + { + return batchRegistrationTasks.GetOrAdd(batchId, id => new Lazy>(() => LoadAndRegisterBatchInternalAsync(id, suppressLogs))).Value; + } + + private async Task LoadAndRegisterBatchInternalAsync(NitroxInt3 batchId, bool suppressLogs) { List spawnedEntities = await batchEntitySpawner.LoadUnspawnedEntitiesAsync(batchId, suppressLogs); diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs index 9aea3e4c55..318e43a73f 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/EntityDestroyedPacketProcessor.cs @@ -28,7 +28,7 @@ public async Task Process(AuthProcessorContext context, EntityDestroyed packet) bool isOtherPlayer = player != context.Sender; if (isOtherPlayer && player.CanSee(entity)) { - await context.ReplyAsync(packet); + await context.SendAsync(packet, player.SessionId); } } } From 3a400b1e54baa4f4ab8316aaf4f38d1967934f72 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:11:48 +0100 Subject: [PATCH 30/59] Disable custom titlebar on MacOS --- Nitrox.Launcher/Views/Abstract/WindowEx.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Nitrox.Launcher/Views/Abstract/WindowEx.cs b/Nitrox.Launcher/Views/Abstract/WindowEx.cs index 6c8ce6d682..4441df7c78 100644 --- a/Nitrox.Launcher/Views/Abstract/WindowEx.cs +++ b/Nitrox.Launcher/Views/Abstract/WindowEx.cs @@ -12,7 +12,8 @@ protected override void OnInitialized() this.ApplyOsWindowStyling(); // On Linux systems, Avalonia has trouble allowing windows to resize without "decorations". So we enable it in full, but hide the custom titlebar as it'll look bad. - if (OperatingSystem.IsLinux()) + // On macOS, we need the native toolbar as every app is using it + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { SystemDecorations = SystemDecorations.Full; NitroxAttached.SetUseCustomTitleBar(this, false); From 78bef8dd1055b596d272949a064d29193e495e7f Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:28:35 +0100 Subject: [PATCH 31/59] Bump Nitrox.Discovery that supports latest improvements to mac --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 843302198d..44bff2c76b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 749879330c8daccab0c493ad98f52e2660ff724c Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:07:22 +0100 Subject: [PATCH 32/59] Upgrade dependencies --- Directory.Packages.props | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 44bff2c76b..188852d146 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,21 +1,28 @@ + true true + + + 11.3.12 + 10.0.5 + + - - + + - + - + - + @@ -31,12 +38,12 @@ - - - - - - + + + + + + @@ -60,8 +67,9 @@ - + + From b58db69b977d0a440faacc3f9a92a10fed04bb09 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:33:43 +0100 Subject: [PATCH 33/59] Migrate to .slnx (#2684) --- Nitrox.sln | 77 ------------------- Nitrox.slnx | 20 +++++ ...sln.DotSettings => Nitrox.slnx.DotSettings | 0 3 files changed, 20 insertions(+), 77 deletions(-) delete mode 100644 Nitrox.sln create mode 100644 Nitrox.slnx rename Nitrox.sln.DotSettings => Nitrox.slnx.DotSettings (100%) diff --git a/Nitrox.sln b/Nitrox.sln deleted file mode 100644 index b6d723b546..0000000000 --- a/Nitrox.sln +++ /dev/null @@ -1,77 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32014.148 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{9BFAD807-A48B-4860-BCF8-2E94FD7B6908}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets - Nitrox.Shared.props = Nitrox.Shared.props - Nitrox.Shared.targets = Nitrox.Shared.targets - .gitignore = .gitignore - Nitrox.sln.DotSettings = Nitrox.sln.DotSettings - Directory.Packages.props = Directory.Packages.props - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Launcher", "Nitrox.Launcher\Nitrox.Launcher.csproj", "{30493A43-7EB3-4898-A82F-98A387284EB2}" -EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Nitrox.Assets.Subnautica", "Nitrox.Assets.Subnautica\Nitrox.Assets.Subnautica.shproj", "{79E92B6D-5D25-4254-AC9F-FA9A1CD3CBC6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Test", "Nitrox.Test\Nitrox.Test.csproj", "{B4C9C786-10A1-4091-A88F-C22AA14C05D4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NitroxClient", "NitroxClient\NitroxClient.csproj", "{5453E724-5A8B-46A4-850B-1F17FA2E938D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Model", "Nitrox.Model\Nitrox.Model.csproj", "{EBEBC7DC-FAE3-4FBC-BDD3-38ED8FC072D9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Model.Subnautica", "Nitrox.Model.Subnautica\Nitrox.Model.Subnautica.csproj", "{47D774E0-750C-427B-8C38-F8F985114A2E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NitroxPatcher", "NitroxPatcher\NitroxPatcher.csproj", "{39E377AD-2163-4428-952D-EBECD402C8F3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nitrox.Server.Subnautica", "Nitrox.Server.Subnautica\Nitrox.Server.Subnautica.csproj", "{77692FDB-F713-41F1-B2AB-9019457B8909}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EBEBC7DC-FAE3-4FBC-BDD3-38ED8FC072D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EBEBC7DC-FAE3-4FBC-BDD3-38ED8FC072D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBEBC7DC-FAE3-4FBC-BDD3-38ED8FC072D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EBEBC7DC-FAE3-4FBC-BDD3-38ED8FC072D9}.Release|Any CPU.Build.0 = Release|Any CPU - {39E377AD-2163-4428-952D-EBECD402C8F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39E377AD-2163-4428-952D-EBECD402C8F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39E377AD-2163-4428-952D-EBECD402C8F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39E377AD-2163-4428-952D-EBECD402C8F3}.Release|Any CPU.Build.0 = Release|Any CPU - {47D774E0-750C-427B-8C38-F8F985114A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47D774E0-750C-427B-8C38-F8F985114A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47D774E0-750C-427B-8C38-F8F985114A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47D774E0-750C-427B-8C38-F8F985114A2E}.Release|Any CPU.Build.0 = Release|Any CPU - {5453E724-5A8B-46A4-850B-1F17FA2E938D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5453E724-5A8B-46A4-850B-1F17FA2E938D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5453E724-5A8B-46A4-850B-1F17FA2E938D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5453E724-5A8B-46A4-850B-1F17FA2E938D}.Release|Any CPU.Build.0 = Release|Any CPU - {77692FDB-F713-41F1-B2AB-9019457B8909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {77692FDB-F713-41F1-B2AB-9019457B8909}.Debug|Any CPU.Build.0 = Debug|Any CPU - {77692FDB-F713-41F1-B2AB-9019457B8909}.Release|Any CPU.ActiveCfg = Release|Any CPU - {77692FDB-F713-41F1-B2AB-9019457B8909}.Release|Any CPU.Build.0 = Release|Any CPU - {30493A43-7EB3-4898-A82F-98A387284EB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30493A43-7EB3-4898-A82F-98A387284EB2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30493A43-7EB3-4898-A82F-98A387284EB2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30493A43-7EB3-4898-A82F-98A387284EB2}.Release|Any CPU.Build.0 = Release|Any CPU - {B4C9C786-10A1-4091-A88F-C22AA14C05D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4C9C786-10A1-4091-A88F-C22AA14C05D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4C9C786-10A1-4091-A88F-C22AA14C05D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4C9C786-10A1-4091-A88F-C22AA14C05D4}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {AC56EA37-FBBC-4D19-8796-29A42A2331A2} - EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - Nitrox.Assets.Subnautica\Nitrox.Assets.Subnautica.projitems*{79e92b6d-5d25-4254-ac9f-fa9a1cd3cbc6}*SharedItemsImports = 13 - EndGlobalSection -EndGlobal diff --git a/Nitrox.slnx b/Nitrox.slnx new file mode 100644 index 0000000000..c401b3ce35 --- /dev/null +++ b/Nitrox.slnx @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Nitrox.sln.DotSettings b/Nitrox.slnx.DotSettings similarity index 100% rename from Nitrox.sln.DotSettings rename to Nitrox.slnx.DotSettings From 0368a3fbe4bff0f6d8e2358ce99b03a5a362d454 Mon Sep 17 00:00:00 2001 From: CodesInChaos Date: Sat, 21 Mar 2026 13:33:48 +0100 Subject: [PATCH 34/59] Grant access to steam libraries in sandbox (#2689) Co-authored-by: CodesInChaos --- Nitrox.Model/Platforms/Store/Steam.cs | 12 ++++++++++++ NitroxPatcher/Main.cs | 9 +++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Nitrox.Model/Platforms/Store/Steam.cs b/Nitrox.Model/Platforms/Store/Steam.cs index f56c1c0c54..c3d3ffd667 100644 --- a/Nitrox.Model/Platforms/Store/Steam.cs +++ b/Nitrox.Model/Platforms/Store/Steam.cs @@ -359,10 +359,22 @@ private static ProcessStartInfo CreateSteamGameStartInfo(string gameFilePath, st result.EnvironmentVariables.Add("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath); result.EnvironmentVariables.Add("STEAM_COMPAT_DATA_PATH", compatdataPath); result.EnvironmentVariables.Add("STEAM_OVERLAY_LINUX", "1"); // Enable Steam overlay and API for controller input and OSK support (Proton-specific) + result.EnvironmentVariables.Add("PRESSURE_VESSEL_FILESYSTEMS_RW", JoinPaths(GetAllLibraryPaths(steamPath))); } return result; + static string JoinPaths(params IEnumerable paths) + { + paths = paths.Where(path => path != null).Distinct().ToList(); + string? invalidPath = paths.FirstOrDefault(path => path != null && path.Contains(':')); + if (invalidPath != null) + { + throw new Exception($"Path '{invalidPath}' contains invalid character ':'"); + } + return string.Join(":", paths); + } + // function to get library path for given game id static string GetLibraryPath(string steamPath, string gameId) { diff --git a/NitroxPatcher/Main.cs b/NitroxPatcher/Main.cs index f6d0ff55e9..5a3e0cf4f8 100644 --- a/NitroxPatcher/Main.cs +++ b/NitroxPatcher/Main.cs @@ -34,7 +34,7 @@ public static class Main { string path = (args[i], args[i + 1]) switch { - ("--nitrox", { } value) when Directory.Exists(value) => Path.GetFullPath(value), + ("--nitrox", { } value) when !string.IsNullOrEmpty(value) => Path.GetFullPath(value), _ => null }; if (!string.IsNullOrEmpty(path)) @@ -69,9 +69,14 @@ public static void Execute() AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomainOnAssemblyResolve; Console.WriteLine("Checking if Nitrox should run..."); + if (string.IsNullOrEmpty(nitroxLauncherDir.Value)) + { + Console.WriteLine($"Nitrox will not load because launcher path was not provided"); + return; + } if (!Directory.Exists(nitroxLauncherDir.Value)) { - Console.WriteLine($"Nitrox will not load because launcher path was not provided or does not exist: {nitroxLauncherDir.Value}"); + Console.WriteLine($"Nitrox will not load because launcher path does not exist or is inaccessible: '{nitroxLauncherDir.Value}'"); return; } From d67d4f7223e44ca4fc555d1c3fb7baf8a3623bf6 Mon Sep 17 00:00:00 2001 From: jtywork <152744553+jtywork@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:54:11 -0500 Subject: [PATCH 35/59] Fix typo: "aquire" -> "acquire" in SimulationOwnership comment (#2690) --- .../Models/GameLogic/SimulationOwnership.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs b/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs index 2693c01a81..3a25927136 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/SimulationOwnership.cs @@ -24,7 +24,7 @@ public bool TryToAcquire(NitroxId id, Player player, SimulationLockType requeste { lock (playerLocksById) { - // If no one is simulating then aquire a lock for this player + // If no one is simulating then acquire a lock for this player if (!playerLocksById.TryGetValue(id, out PlayerLock playerLock)) { playerLocksById[id] = new PlayerLock(player, requestedLock); From 57ba288c7659632e21fb691be46fc6583ffb287e Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:04:49 +0100 Subject: [PATCH 36/59] Fixed change detection on server config files --- .gitignore | 2 - .../ConfigurationBuilderExtensions.cs | 60 +++---------------- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index 42d4ea1508..b657b89dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -server.cfg - # Microsoft DI related files *.[Dd]evelopment.json diff --git a/Nitrox.Server.Subnautica/Extensions/ConfigurationBuilderExtensions.cs b/Nitrox.Server.Subnautica/Extensions/ConfigurationBuilderExtensions.cs index 9a0466a20d..d536497b41 100644 --- a/Nitrox.Server.Subnautica/Extensions/ConfigurationBuilderExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/ConfigurationBuilderExtensions.cs @@ -11,7 +11,6 @@ internal static class ConfigurationBuilderExtensions public static IConfigurationBuilder AddNitroxConfigFile(this IConfigurationBuilder configurationBuilder, string filePath, string configSectionPath = "", bool optional = false, bool reloadOnChange = false) where TOptions : class, new() { ArgumentException.ThrowIfNullOrWhiteSpace(filePath); - reloadOnChange = reloadOnChange && CanChangeDetect(); string dirPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException(nameof(filePath)); Directory.CreateDirectory(dirPath); @@ -20,36 +19,12 @@ internal static class ConfigurationBuilderExtensions NitroxConfig.CreateFile(filePath); } - if (reloadOnChange) - { - // Link the config to a relative path within the working directory so that IOptionsMonitor works. See https://github.com/dotnet/runtime/issues/114833 - try - { - FileInfo configFile = new(Path.Combine(AppContext.BaseDirectory, Path.GetFileName(filePath))); - if (configFile.Exists && configFile.LinkTarget != null) - { - configFile.Delete(); - } - configFile.CreateAsSymbolicLink(filePath); - // Fix targets to point to symbolic link instead. - dirPath = AppContext.BaseDirectory; - filePath = configFile.Name; // Now a relative path. - } - catch (IOException) - { - if (!optional) - { - throw; - } - } - } - PhysicalFileProvider fileProvider = new(dirPath) { UsePollingFileWatcher = true, UseActivePolling = true }; - return configurationBuilder.Add(new NitroxConfigurationSource(filePath, configSectionPath, optional, fileProvider) + return configurationBuilder.Add(new NitroxConfigurationSource(Path.GetFileName(filePath), configSectionPath, optional, fileProvider) { ReloadOnChange = reloadOnChange, Optional = optional @@ -70,26 +45,18 @@ public static IConfigurationBuilder AddConditionalUpstreamJsonFile(this IConfigu { return builder; } - reloadOnChange = reloadOnChange && CanChangeDetect(); string? parentAppSettingsFile = null; if (reloadOnChange) { try { - // Symbolic link the first parent JSON file found. Required for change detection when file is in a parent directory. string current = AppContext.BaseDirectory.TrimEnd('/', '\\'); while ((current = Path.GetDirectoryName(current)) is not null) { parentAppSettingsFile = Path.Combine(current, fileName); if (File.Exists(parentAppSettingsFile)) { - FileInfo appSettingsFile = new(Path.Combine(AppContext.BaseDirectory, fileName)); - if (appSettingsFile.Exists && appSettingsFile.LinkTarget != null) - { - appSettingsFile.Delete(); - } - appSettingsFile.CreateAsSymbolicLink(parentAppSettingsFile); break; } } @@ -103,7 +70,7 @@ public static IConfigurationBuilder AddConditionalUpstreamJsonFile(this IConfigu } } - string? baseDirectory = CanChangeDetect() ? AppContext.BaseDirectory : Path.GetDirectoryName(parentAppSettingsFile); + string? baseDirectory = Path.GetDirectoryName(parentAppSettingsFile); if (baseDirectory == null) { return optional ? builder : throw new Exception($"Failed to get parent directory from JSON file: {fileName}"); @@ -112,9 +79,9 @@ public static IConfigurationBuilder AddConditionalUpstreamJsonFile(this IConfigu // On Linux, polling is needed to detect file changes. builder.AddJsonFile(new PhysicalFileProvider(baseDirectory) { - UseActivePolling = OperatingSystem.IsLinux(), - UsePollingFileWatcher = OperatingSystem.IsLinux() - }, fileName, optional, reloadOnChange); + UseActivePolling = true, + UsePollingFileWatcher = true + }, Path.GetFileName(fileName), optional, reloadOnChange); return builder; } @@ -126,7 +93,6 @@ public static IConfigurationBuilder AddConditionalCsharpProjectJsonFile(this ICo return builder; } - // Symbolic link the first parent JSON file found. Required for change detection when file is in a parent directory. string current = AppContext.BaseDirectory.TrimEnd('/', '\\'); string? parentAppSettingsFilePath = null; while ((current = Path.GetDirectoryName(current)) is not null) @@ -136,19 +102,10 @@ public static IConfigurationBuilder AddConditionalCsharpProjectJsonFile(this ICo continue; } parentAppSettingsFilePath = Path.Combine(current, projectName, fileName); - if (CanChangeDetect() && File.Exists(parentAppSettingsFilePath)) - { - FileInfo appSettingsFile = new(Path.Combine(AppContext.BaseDirectory, fileName)); - if (appSettingsFile.Exists && appSettingsFile.LinkTarget != null) - { - appSettingsFile.Delete(); - } - appSettingsFile.CreateAsSymbolicLink(parentAppSettingsFilePath); - } break; } - string? baseDirectory = CanChangeDetect() ? AppContext.BaseDirectory : Path.GetDirectoryName(parentAppSettingsFilePath); + string? baseDirectory = Path.GetDirectoryName(parentAppSettingsFilePath); if (baseDirectory == null) { return optional ? builder : throw new Exception($"Failed to get parent directory from JSON file: {fileName}"); @@ -158,7 +115,7 @@ public static IConfigurationBuilder AddConditionalCsharpProjectJsonFile(this ICo { UseActivePolling = OperatingSystem.IsLinux(), UsePollingFileWatcher = OperatingSystem.IsLinux() - }, fileName, optional, reloadOnChange); + }, Path.GetFileName(fileName), optional, reloadOnChange); return builder; @@ -180,7 +137,4 @@ static bool IsSolutionRootWithProject(string path, string projectName) return isSolutionRoot && projectExists; } } - - // TODO: Handle Windows differently because symbolic link requires admin perms there. Copy file over? - private static bool CanChangeDetect() => !OperatingSystem.IsWindows(); // For now change detection does not work on Windows. } From 4e26a2ade3e8483e077d6ab8d7e81a0e61fb9068 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:54:45 +0100 Subject: [PATCH 37/59] Optimized perf of PacketSerializationService --- .../Services/PacketSerializationService.cs | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs b/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs index 96d00cfaab..f440cd8ac4 100644 --- a/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs +++ b/Nitrox.Server.Subnautica/Services/PacketSerializationService.cs @@ -4,40 +4,29 @@ namespace Nitrox.Server.Subnautica.Services; internal sealed class PacketSerializationService : BackgroundService { - private readonly TaskCompletionSource init; + private readonly TaskCompletionSource initTcs; private readonly ILogger logger; private IPacketSerializer inner; - private readonly Lock innerLock = new(); public PacketSerializationService(ILogger logger) { this.logger = logger; - init = new TaskCompletionSource(); - inner = new UnloadedSerializer(init, serializer => - { - lock (innerLock) - { - inner = serializer; - } - }); + initTcs = new TaskCompletionSource(); + inner = new UnloadedSerializer(initTcs, serializer => Interlocked.Exchange(ref inner, serializer)); } public void SerializeInto(Packet packet, Stream stream) { - IPacketSerializer actual; - lock (innerLock) - { - actual = inner; - } - actual.SerializeInto(packet, stream); + IPacketSerializer serializer = Interlocked.CompareExchange(ref inner, null, null); + serializer.SerializeInto(packet, stream); } public override void Dispose() { - if (init.Task.IsCompleted) + if (initTcs.Task.IsCompleted) { - init.Task.Dispose(); + initTcs.Task.Dispose(); } base.Dispose(); } @@ -52,9 +41,9 @@ await Task.Run(() => } catch (Exception ex) { - init.TrySetException(ex); + initTcs.TrySetException(ex); } - if (!init.TrySetResult()) + if (!initTcs.TrySetResult()) { throw new Exception("Failed to set init result"); } From a09632b20427f6d2af2287f435247c1f6a85b2df Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:22:32 +0100 Subject: [PATCH 38/59] Handled nullability in NitroxEntityExtensions.cs --- NitroxClient/Extensions/NitroxEntityExtensions.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/NitroxClient/Extensions/NitroxEntityExtensions.cs b/NitroxClient/Extensions/NitroxEntityExtensions.cs index b807b5be30..7181fa4180 100644 --- a/NitroxClient/Extensions/NitroxEntityExtensions.cs +++ b/NitroxClient/Extensions/NitroxEntityExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using NitroxClient.MonoBehaviours; using Nitrox.Model.DataStructures; @@ -8,19 +9,19 @@ namespace NitroxClient.Extensions; public static class NitroxEntityExtensions { - public static bool TryGetNitroxEntity(this Component component, out NitroxEntity nitroxEntity) + public static bool TryGetNitroxEntity(this Component component, [NotNullWhen(true)] out NitroxEntity? nitroxEntity) { nitroxEntity = null; return component && component.TryGetComponent(out nitroxEntity); } - public static bool TryGetNitroxEntity(this GameObject gameObject, out NitroxEntity nitroxEntity) + public static bool TryGetNitroxEntity(this GameObject gameObject, [NotNullWhen(true)] out NitroxEntity? nitroxEntity) { nitroxEntity = null; return gameObject && gameObject.TryGetComponent(out nitroxEntity); } - public static bool TryGetNitroxId(this GameObject gameObject, out NitroxId nitroxId) + public static bool TryGetNitroxId(this GameObject gameObject, [NotNullWhen(true)] out NitroxId? nitroxId) { if (!gameObject || !gameObject.TryGetComponent(out NitroxEntity nitroxEntity)) { @@ -32,7 +33,7 @@ public static bool TryGetNitroxId(this GameObject gameObject, out NitroxId nitro return nitroxId != null; } - public static bool TryGetNitroxId(this Component component, out NitroxId nitroxId) + public static bool TryGetNitroxId(this Component component, [NotNullWhen(true)] out NitroxId? nitroxId) { if (!component || !component.TryGetComponent(out NitroxEntity nitroxEntity)) { @@ -46,7 +47,7 @@ public static bool TryGetNitroxId(this Component component, out NitroxId nitroxI public static bool TryGetIdOrWarn( this GameObject gameObject, - out NitroxId nitroxId, + [NotNullWhen(true)] out NitroxId? nitroxId, [CallerMemberName] string methodName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) @@ -70,7 +71,7 @@ public static bool TryGetIdOrWarn( public static bool TryGetIdOrWarn( this Component component, - out NitroxId nitroxId, + [NotNullWhen(true)] out NitroxId? nitroxId, [CallerMemberName] string methodName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) From 647b77ca38a2fd13d1815a42ebf677a8f21f8b65 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:24:59 +0100 Subject: [PATCH 39/59] Changed NitroxUser to use new "field" keyword in properties --- Nitrox.Model/Helper/NitroxUser.cs | 39 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Nitrox.Model/Helper/NitroxUser.cs b/Nitrox.Model/Helper/NitroxUser.cs index cd024dc289..e91468fd78 100644 --- a/Nitrox.Model/Helper/NitroxUser.cs +++ b/Nitrox.Model/Helper/NitroxUser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -16,12 +17,7 @@ public static class NitroxUser { public const string LAUNCHER_PATH_ENV_KEY = "NITROX_LAUNCHER_PATH"; private const string PREFERRED_GAMEPATH_KEY = "PreferredGamePath"; - private static string? appDataPath; - private static string? launcherPath; private static string gamePath = ""; - private static string? executableRootPath; - private static string? executablePath; - private static string? assetsPath; private static readonly IEnumerable> launcherPathDataSources = new List> { @@ -60,13 +56,14 @@ public static class NitroxUser } }; + [field: MaybeNull, AllowNull] public static string AppDataPath { get { - if (appDataPath != null) + if (field != null) { - return appDataPath; + return field; } string applicationData = null; @@ -87,7 +84,7 @@ public static string AppDataPath if (!string.IsNullOrWhiteSpace(cliDataPath) && Path.IsPathRooted(cliDataPath)) { Directory.CreateDirectory(cliDataPath); - return appDataPath = cliDataPath; + return field = cliDataPath; } if (!Directory.Exists(applicationData)) @@ -95,7 +92,7 @@ public static string AppDataPath applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); } - return appDataPath = Path.Combine(applicationData, "Nitrox"); + return field = Path.Combine(applicationData, "Nitrox"); } } @@ -112,9 +109,9 @@ public static string? LauncherPath { get { - if (launcherPath != null) + if (field != null) { - return launcherPath; + return field; } foreach (Func retriever in launcherPathDataSources) @@ -122,7 +119,7 @@ public static string? LauncherPath string path = retriever(); if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) { - return launcherPath = path; + return field = path; } } @@ -153,9 +150,9 @@ public static string ExecutableRootPath { get { - if (!string.IsNullOrWhiteSpace(executableRootPath)) + if (!string.IsNullOrWhiteSpace(field)) { - return executableRootPath; + return field; } string exePath = ExecutableFilePath; if (exePath == null) @@ -163,7 +160,7 @@ public static string ExecutableRootPath throw new Exception("Executable root path is unavailable"); } - return executableRootPath = Path.GetDirectoryName(exePath) ?? throw new Exception("Executable root path is unavailable"); + return field = Path.GetDirectoryName(exePath) ?? throw new Exception("Executable root path is unavailable"); } } @@ -171,9 +168,9 @@ public static string? ExecutableFilePath { get { - if (!string.IsNullOrWhiteSpace(executablePath)) + if (!string.IsNullOrWhiteSpace(field)) { - return executablePath; + return field; } Assembly entryAssembly = Assembly.GetEntryAssembly(); @@ -191,7 +188,7 @@ public static string? ExecutableFilePath { path = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException($"Failed to get directory from path: '{path}'"), Path.GetFileNameWithoutExtension(path)); } - return executablePath = path; + return field = path; } } @@ -199,9 +196,9 @@ public static string? AssetsPath { get { - if (!string.IsNullOrWhiteSpace(assetsPath)) + if (!string.IsNullOrWhiteSpace(field)) { - return assetsPath; + return field; } string nitroxAssets; @@ -221,7 +218,7 @@ public static string? AssetsPath { nitroxAssets = LauncherPath ?? ExecutableRootPath; } - return assetsPath = nitroxAssets; + return field = nitroxAssets; } } } From be6a6ad150df04b32978c8fa79d595a170d392c0 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:25:37 +0100 Subject: [PATCH 40/59] Fixed code doc ref in NitroxInt3.cs Model -> Nitrox.Model --- Nitrox.Model/DataStructures/NitroxInt3.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nitrox.Model/DataStructures/NitroxInt3.cs b/Nitrox.Model/DataStructures/NitroxInt3.cs index 8dcc864d22..65c50b85bd 100644 --- a/Nitrox.Model/DataStructures/NitroxInt3.cs +++ b/Nitrox.Model/DataStructures/NitroxInt3.cs @@ -5,7 +5,7 @@ namespace Nitrox.Model.DataStructures { /// - /// Model to allow to be decoupled from Assembly-csharp-firstpass (i.e. game code). + /// Model to allow to be decoupled from Assembly-csharp-firstpass (i.e. game code). /// [Serializable] [DataContract] From f837658b52c3d94dec6f820ca4d7d8bd8c498d94 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:31:07 +0100 Subject: [PATCH 41/59] Changed IAdminFeature to match behavior of IOptions --- .../Extensions/ServiceCollectionExtensions.cs | 24 +++++++++---------- .../Administration/Core/IAdminFeature.cs | 7 +++--- .../Models/GameLogic/JoiningManager.cs | 1 + 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs index 2ff960ad7a..8ba23e606f 100644 --- a/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs +++ b/Nitrox.Server.Subnautica/Extensions/ServiceCollectionExtensions.cs @@ -35,7 +35,13 @@ internal static partial class ServiceCollectionExtensions [GenerateServiceRegistrations(AssignableTo = typeof(IRedactor), Lifetime = ServiceLifetime.Singleton)] internal static partial IServiceCollection AddRedactors(this IServiceCollection services); - [GenerateServiceRegistrations(AssignableTo = typeof(IAdminFeature<>), CustomHandler = nameof(AddImplementedAdminFeatures))] + /// + /// Adds an interface -> service mapping that for handling administrative actions. + /// + /// + /// If multiple instances of the same interface type are registered, then the last registered implementation will be used. + /// + [GenerateServiceRegistrations(AssignableTo = typeof(IAdminFeature<>), CustomHandler = nameof(AddOpenGenericAsExistingSingleton))] internal static partial IServiceCollection AddAdminFeatures(this IServiceCollection services); [GenerateServiceRegistrations(AssignableTo = typeof(IGameResource), Lifetime = ServiceLifetime.Singleton, AsSelf = true, AsImplementedInterfaces = true)] @@ -59,6 +65,10 @@ internal static partial class ServiceCollectionExtensions [GenerateServiceRegistrations(AssignableTo = typeof(IArgConverter), Lifetime = ServiceLifetime.Singleton, AsSelf = true, AsImplementedInterfaces = true)] private static partial IServiceCollection AddCommandArgConverters(this IServiceCollection services); + private static void AddOpenGenericAsExistingSingleton(this IServiceCollection services) where TImplementation : class, TInterface => + services + .AddSingleton(typeof(TInterface), provider => provider.GetRequiredService()); + /// /// Registers a single command and all of its handlers as can be known by the implemented interfaces. /// @@ -81,18 +91,6 @@ private static void AddCommandHandler(this IServiceCollection services) where } } - private static void AddImplementedAdminFeatures(this IServiceCollection services) where TImplementation : class, IAdminFeature - { - foreach (Type featureInterfaceType in typeof(TImplementation).GetInterfaces() - .Where(i => typeof(IAdminFeature).IsAssignableFrom(i)) - .Select(i => i.GetGenericArguments()) - .Where(types => types.Length == 1) - .Select(types => types[0])) - { - services.AddSingleton(featureInterfaceType, provider => provider.GetRequiredService()); - } - } - extension(IServiceCollection services) { /// diff --git a/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs b/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs index 122518366c..219d7bae2e 100644 --- a/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs +++ b/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs @@ -1,5 +1,6 @@ namespace Nitrox.Server.Subnautica.Models.Administration.Core; -internal interface IAdminFeature; - -internal interface IAdminFeature : IAdminFeature where T : IAdminFeature; +/// +/// Implementors handle an administrative action. +/// +internal interface IAdminFeature where T : IAdminFeature; diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs index b53c184842..2173d6f497 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/JoiningManager.cs @@ -15,6 +15,7 @@ namespace Nitrox.Server.Subnautica.Models.GameLogic; +// TODO: Refactor this to a QueuingBackgroundService to simplify state tracking. internal sealed class JoiningManager( IPacketSender packetSender, PlayerManager playerManager, From b73a8859ac4010b0562c9d7104c491faf291aa96 Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:49:13 +0100 Subject: [PATCH 42/59] Removed unused packetSender field in PlayerInCyclopsMovementProcessor --- .../Packets/Processors/PlayerInCyclopsMovementProcessor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs index 74c09ada6f..f06bc3a607 100644 --- a/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs +++ b/Nitrox.Server.Subnautica/Models/Packets/Processors/PlayerInCyclopsMovementProcessor.cs @@ -4,9 +4,8 @@ namespace Nitrox.Server.Subnautica.Models.Packets.Processors; -internal sealed class PlayerInCyclopsMovementProcessor(IPacketSender packetSender, EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor +internal sealed class PlayerInCyclopsMovementProcessor(EntityRegistry entityRegistry, ILogger logger) : IAuthPacketProcessor { - private readonly IPacketSender packetSender = packetSender; private readonly EntityRegistry entityRegistry = entityRegistry; private readonly ILogger logger = logger; From f37fc2a352b0fe6c15a6ec0e7f4b171f8ce658aa Mon Sep 17 00:00:00 2001 From: Measurity <1107063+Measurity@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:51:12 +0100 Subject: [PATCH 43/59] Removed unused Player.PlayerSettings property --- Nitrox.Model/Configuration/SubnauticaServerOptions.cs | 1 - Nitrox.Server.Subnautica/Models/Player.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs index 5cb1c90eb7..8051c2e963 100644 --- a/Nitrox.Model/Configuration/SubnauticaServerOptions.cs +++ b/Nitrox.Model/Configuration/SubnauticaServerOptions.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Nitrox.Model.Constants; using Nitrox.Model.DataStructures.GameLogic; diff --git a/Nitrox.Server.Subnautica/Models/Player.cs b/Nitrox.Server.Subnautica/Models/Player.cs index 9e63f41095..2b63a5cc6d 100644 --- a/Nitrox.Server.Subnautica/Models/Player.cs +++ b/Nitrox.Server.Subnautica/Models/Player.cs @@ -17,7 +17,6 @@ internal sealed class Player public ThreadSafeList UsedItems { get; } public Optional[] QuickSlotsBindingIds { get; set; } - public PlayerSettings? PlayerSettings => PlayerContext?.PlayerSettings; public PlayerContext? PlayerContext { get; set; } public PeerId Id { get; init; } public SessionId SessionId { get; set; } From f7ab4327ec7b8c5d08b4357fe6e12d74257ebef6 Mon Sep 17 00:00:00 2001 From: jtywork <152744553+jtywork@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:43:12 -0500 Subject: [PATCH 44/59] Fix cache stampede in SubnauticaUwePrefabFactory (#2694) --- .../Entities/SubnauticaUwePrefabFactory.cs | 39 +++++----- .../Parsers/EntityDistributionsResource.cs | 4 +- .../Parsers/IEntityDistributionsAccessor.cs | 12 +++ .../Nitrox.Server.Subnautica.csproj | 9 ++- .../SubnauticaUwePrefabFactoryTest.cs | 74 +++++++++++++++++++ 5 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 Nitrox.Server.Subnautica/Models/Resources/Parsers/IEntityDistributionsAccessor.cs create mode 100644 Nitrox.Test/Server/GameLogic/Entities/SubnauticaUwePrefabFactoryTest.cs diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs index aebab7cc08..74d4f603a0 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/Entities/SubnauticaUwePrefabFactory.cs @@ -1,15 +1,18 @@ +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; using Nitrox.Server.Subnautica.Models.Resources.Parsers; using static LootDistributionData; namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; -internal sealed class SubnauticaUwePrefabFactory(EntityDistributionsResource distributionData) : IUwePrefabFactory +internal sealed class SubnauticaUwePrefabFactory(IEntityDistributionsAccessor distributionData) : IUwePrefabFactory { - private readonly EntityDistributionsResource resource = distributionData; - private readonly Dictionary> cache = new(); - private readonly Lock cacheLock = new(); + private readonly IEntityDistributionsAccessor resource = distributionData; + private readonly ConcurrentDictionary>>> cache = new(); public async Task> TryGetPossiblePrefabsAsync(string? biome) { @@ -17,18 +20,16 @@ public async Task> TryGetPossiblePrefabsAsync(string? biome) { return []; } - List prefabs; - lock (cacheLock) - { - if (cache.TryGetValue(biome, out prefabs)) - { - return prefabs; - } - } - prefabs = new(); + Lazy>> lazy = cache.GetOrAdd(biome, key => new Lazy>>(() => LoadPrefabsForBiomeAsync(key), LazyThreadSafetyMode.ExecutionAndPublication)); + return await lazy.Value.ConfigureAwait(false); + } + + private async Task> LoadPrefabsForBiomeAsync(string biome) + { + List prefabs = []; BiomeType biomeType = (BiomeType)Enum.Parse(typeof(BiomeType), biome); - LootDistributionData distributionData = await resource.GetLootDistributionDataAsync(); + LootDistributionData distributionData = await resource.GetLootDistributionDataAsync().ConfigureAwait(false); if (distributionData.GetBiomeLoot(biomeType, out DstData dstData)) { foreach (PrefabData prefabData in dstData.prefabs) @@ -39,17 +40,11 @@ public async Task> TryGetPossiblePrefabsAsync(string? biome) // You can verify this by looping through all of SrcData (e.g in LootDistributionData.Initialize) // print the prefabPath and check the TechType related to the provided classId (WorldEntityDatabase.TryGetInfo) with PDAScanner.IsFragment bool isFragment = srcData.prefabPath.Contains("Fragment") || srcData.prefabPath.Contains("BaseGlassDome"); - lock (cacheLock) - { - prefabs.Add(new(prefabData.classId, prefabData.count, prefabData.probability, isFragment)); - } + prefabs.Add(new(prefabData.classId, prefabData.count, prefabData.probability, isFragment)); } } } - lock (cacheLock) - { - cache[biome] = prefabs; - } + return prefabs; } } diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs index c660b71b3b..134bc2109d 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/EntityDistributionsResource.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using AssetsTools.NET; using AssetsTools.NET.Extra; @@ -7,7 +7,7 @@ namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; -internal sealed class EntityDistributionsResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource +internal sealed class EntityDistributionsResource(SubnauticaAssetsManager assetsManager, IOptions options) : IGameResource, IEntityDistributionsAccessor { private readonly SubnauticaAssetsManager assetsManager = assetsManager; diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/IEntityDistributionsAccessor.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/IEntityDistributionsAccessor.cs new file mode 100644 index 0000000000..d1ca5f31df --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/IEntityDistributionsAccessor.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Nitrox.Server.Subnautica.Models.Resources.Parsers; + +/// +/// Abstraction for tests and to consume loot distribution data without coupling to . +/// +internal interface IEntityDistributionsAccessor +{ + Task GetLootDistributionDataAsync(CancellationToken cancellationToken = default); +} diff --git a/Nitrox.Server.Subnautica/Nitrox.Server.Subnautica.csproj b/Nitrox.Server.Subnautica/Nitrox.Server.Subnautica.csproj index 80bf502b1e..4ec8668a7d 100644 --- a/Nitrox.Server.Subnautica/Nitrox.Server.Subnautica.csproj +++ b/Nitrox.Server.Subnautica/Nitrox.Server.Subnautica.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -36,6 +36,13 @@ <_ServerDevelopmentJsonFilePath>$(MSBuildThisFileDirectory)\server.Development.json + + + + <_Parameter1>DynamicProxyGenAssembly2 + + + <_ServerDevelopmentJsonContent> diff --git a/Nitrox.Test/Server/GameLogic/Entities/SubnauticaUwePrefabFactoryTest.cs b/Nitrox.Test/Server/GameLogic/Entities/SubnauticaUwePrefabFactoryTest.cs new file mode 100644 index 0000000000..d548fe0a49 --- /dev/null +++ b/Nitrox.Test/Server/GameLogic/Entities/SubnauticaUwePrefabFactoryTest.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Resources.Parsers; +using NSubstitute; +using LootDictionary = System.Collections.Generic.Dictionary; + +namespace Nitrox.Server.Subnautica.Models.GameLogic.Entities; + +[TestClass] +public class SubnauticaUwePrefabFactoryTest +{ + private static LootDistributionData CreateEmptyInitializedLootDistributionData() + { + LootDictionary empty = []; + LootDistributionData data = new(); + data.Initialize(empty); + return data; + } + + [TestMethod] + public async Task TryGetPossiblePrefabsAsync_NullBiome_ReturnsEmptyWithoutCallingResource() + { + IEntityDistributionsAccessor accessor = Substitute.For(); + SubnauticaUwePrefabFactory factory = new(accessor); + + List result = await factory.TryGetPossiblePrefabsAsync(null); + + result.Should().BeEmpty(); + await accessor.DidNotReceive().GetLootDistributionDataAsync(Arg.Any()); + } + + [TestMethod] + public async Task TryGetPossiblePrefabsAsync_ConcurrentCallsSameBiome_LoadsDistributionOnce() + { + int callCount = 0; + IEntityDistributionsAccessor accessor = Substitute.For(); + accessor.GetLootDistributionDataAsync(Arg.Any()) + .Returns(_ => + { + Interlocked.Increment(ref callCount); + return Task.FromResult(CreateEmptyInitializedLootDistributionData()); + }); + + SubnauticaUwePrefabFactory factory = new(accessor); + + string biomeName = Enum.GetNames(typeof(BiomeType))[0]; + Task[] tasks = Enumerable.Range(0, 10).Select(_ => factory.TryGetPossiblePrefabsAsync(biomeName)).ToArray(); + await Task.WhenAll(tasks); + + callCount.Should().Be(1); + } + + [TestMethod] + public async Task TryGetPossiblePrefabsAsync_SecondCall_ReusesCachedTask() + { + IEntityDistributionsAccessor accessor = Substitute.For(); + accessor.GetLootDistributionDataAsync(Arg.Any()) + .Returns(Task.FromResult(CreateEmptyInitializedLootDistributionData())); + + SubnauticaUwePrefabFactory factory = new(accessor); + + string biomeName = Enum.GetNames(typeof(BiomeType))[0]; + List first = await factory.TryGetPossiblePrefabsAsync(biomeName); + List second = await factory.TryGetPossiblePrefabsAsync(biomeName); + + second.Should().BeSameAs(first); + await accessor.Received(1).GetLootDistributionDataAsync(Arg.Any()); + } +} From 19f2c8012dddaa85cf8ad2da47f15d0c2f0aea3d Mon Sep 17 00:00:00 2001 From: Meas <1107063+Measurity@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:20:24 +0200 Subject: [PATCH 45/59] Disabled CET (#2697) --- Directory.Build.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 25dc1387b9..60a2f19203 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,8 @@ true false true + + false From f00e248d8c819bb7898937b566ec62ff0dc32ed5 Mon Sep 17 00:00:00 2001 From: jtywork <152744553+jtywork@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:41:46 -0500 Subject: [PATCH 46/59] [MacOS] Implement SetForegroundWindowAndRestore using Osascript (#2693) Co-authored-by: dartasen <10561268+dartasen@users.noreply.github.com> --- .../Models/Extensions/ProcessExExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs b/Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs index 38c44ff601..79732b373c 100644 --- a/Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs +++ b/Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs @@ -29,5 +29,20 @@ public static void SetForegroundWindowAndRestore(this ProcessEx process) // TODO: Support "bring to front" on Wayland window manager. } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + using Process proc = Process.Start(new ProcessStartInfo + { + FileName = "osascript", + ArgumentList = + { + "-e", + $"tell application \"System Events\" to set frontmost of every process whose unix id is {process.Id} to true", + }, + UseShellExecute = false, + RedirectStandardError = true, + }); + proc?.WaitForExit(milliseconds: 5000); + } } } From 11e206e5562cbd463ca626caaae3f0f67dc2091a Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:04:18 +0200 Subject: [PATCH 47/59] Fix Query Command (#2700) Co-authored-by: Meas <1107063+Measurity@users.noreply.github.com> --- Nitrox.Model/DataStructures/NitroxId.cs | 75 +++------- .../StringToNitroxIdConverter.cs | 23 +++ .../Models/Commands/Debugging/QueryCommand.cs | 46 ------ .../Models/Commands/QueryCommand.cs | 70 +++++++++ .../Model/DataStructures/NitroxIdTest.cs | 136 ++++++++++++++++-- 5 files changed, 243 insertions(+), 107 deletions(-) create mode 100644 Nitrox.Server.Subnautica/Models/Commands/ArgConverters/StringToNitroxIdConverter.cs delete mode 100644 Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs create mode 100644 Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs diff --git a/Nitrox.Model/DataStructures/NitroxId.cs b/Nitrox.Model/DataStructures/NitroxId.cs index ea0a5cb2f3..75513c734d 100644 --- a/Nitrox.Model/DataStructures/NitroxId.cs +++ b/Nitrox.Model/DataStructures/NitroxId.cs @@ -12,6 +12,10 @@ namespace Nitrox.Model.DataStructures; [DataContract] public sealed class NitroxId : ISerializable, IEquatable, IComparable { + [IgnoredMember] + private static readonly int[] byteOrder = [15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3]; + + [DataMember(Order = 1)] [SerializableMember] private Guid guid { get; init; } @@ -22,10 +26,6 @@ public NitroxId() guid = Guid.NewGuid(); } - /// - /// Create a NitroxId from a string - /// - /// a NitroxID as string public NitroxId(string str) { guid = new Guid(str); @@ -47,22 +47,19 @@ private NitroxId(SerializationInfo info, StreamingContext context) guid = new Guid(bytes); } - public void GetObjectData(SerializationInfo info, StreamingContext context) + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("id", guid.ToByteArray()); } public static bool operator ==(NitroxId? id1, NitroxId? id2) { - if (id1 is null) + if (id1 is not null) { - if (id2 is null) - { - return true; - } - return false; + return id1.Equals(id2); } - return id1.Equals(id2); + + return id2 is null; } public static bool operator !=(NitroxId? id1, NitroxId? id2) @@ -70,49 +67,26 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) return !(id1 == id2); } - public override bool Equals(object obj) + public static implicit operator NitroxId(string str) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((NitroxId)obj); + return new NitroxId(str); } - - public bool Equals(NitroxId? other) + public static implicit operator NitroxId(Guid guid) { - if (ReferenceEquals(null, other)) - { - return false; - } + return new NitroxId(guid); + } - if (ReferenceEquals(this, other)) - { - return true; - } + public override bool Equals(object? obj) => Equals(obj as NitroxId); - return guid.Equals(other.guid); - } - - public override int GetHashCode() + public bool Equals(NitroxId? other) { - return guid.GetHashCode(); + return other is not null && guid.Equals(other.guid); } - public override string ToString() - { - return guid.ToString(); - } + public override int GetHashCode() => guid.GetHashCode(); - [IgnoredMember] - private static int[] byteOrder = { 15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3 }; + public override string ToString() => guid.ToString(); public NitroxId Increment() { @@ -123,18 +97,13 @@ public NitroxId Increment() return new NitroxId(nextGuid); } - public int CompareTo(NitroxId other) + public int CompareTo(NitroxId? other) { - if (ReferenceEquals(this, other)) + if (Equals(this, other)) { return 0; } - if (ReferenceEquals(null, other)) - { - return 1; - } - - return guid.CompareTo(other.guid); + return other is null ? 1 : guid.CompareTo(other.guid); } } diff --git a/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/StringToNitroxIdConverter.cs b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/StringToNitroxIdConverter.cs new file mode 100644 index 0000000000..00ca1e088b --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/ArgConverters/StringToNitroxIdConverter.cs @@ -0,0 +1,23 @@ +using Nitrox.Model.DataStructures; +using Nitrox.Server.Subnautica.Models.Commands.ArgConverters.Core; + +namespace Nitrox.Server.Subnautica.Models.Commands.ArgConverters; + +/// +/// Converts a string to a NitroxId, if valid. +/// +internal sealed class StringToNitroxIdConverter : IArgConverter +{ + public Task ConvertAsync(string nitroxId) + { + try + { + NitroxId id = new(nitroxId); + return Task.FromResult(ConvertResult.Ok(id)); + } + catch (Exception) + { + return Task.FromResult(ConvertResult.Fail()); + } + } +} diff --git a/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs deleted file mode 100644 index fb74d79fa9..0000000000 --- a/Nitrox.Server.Subnautica/Models/Commands/Debugging/QueryCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -#if DEBUG -using System.ComponentModel; -using Nitrox.Model.DataStructures; -using Nitrox.Model.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic; -using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; -using Nitrox.Server.Subnautica.Models.Commands.Core; -using Nitrox.Server.Subnautica.Models.GameLogic; -using Nitrox.Server.Subnautica.Models.GameLogic.Entities; - -namespace Nitrox.Server.Subnautica.Models.Commands.Debugging; - -[RequiresPermission(Perms.HOST)] -internal sealed class QueryCommand(EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) : ICommandHandler -{ - private readonly EntityRegistry entityRegistry = entityRegistry; - private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; - private readonly ILogger logger = logger; - - [Description("Query the entity associated with the given NitroxId")] - public Task Execute(ICommandContext context, [Description("NitroxId of an entity")] NitroxId entityId) - { - if (!entityRegistry.TryGetEntityById(entityId, out Entity entity)) - { - logger.ZLogError($"Entity with id {entityId} not found"); - return Task.CompletedTask; - } - - logger.ZLogInformation($"{entity}"); - if (entity is WorldEntity worldEntity and not GlobalRootEntity) - { - logger.ZLogInformation($"{worldEntity.AbsoluteEntityCell}"); - } - if (simulationOwnershipData.TryGetLock(entityId, out SimulationOwnershipData.PlayerLock playerLock)) - { - logger.ZLogInformation($"Lock owner: {playerLock.Player.Name}"); - } - else - { - logger.ZLogInformation($"Not locked"); - } - - return Task.CompletedTask; - } -} -#endif diff --git a/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs b/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs new file mode 100644 index 0000000000..91d33aefd4 --- /dev/null +++ b/Nitrox.Server.Subnautica/Models/Commands/QueryCommand.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using System.Text; +using Nitrox.Model.DataStructures; +using Nitrox.Model.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic; +using Nitrox.Model.Subnautica.DataStructures.GameLogic.Entities; +using Nitrox.Server.Subnautica.Models.Commands.Core; +using Nitrox.Server.Subnautica.Models.GameLogic; +using Nitrox.Server.Subnautica.Models.GameLogic.Entities; + +namespace Nitrox.Server.Subnautica.Models.Commands; + +[RequiresPermission(Perms.ADMIN)] +internal sealed class QueryCommand(EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData, ILogger logger) : ICommandHandler +{ + private readonly EntityRegistry entityRegistry = entityRegistry; + private readonly SimulationOwnershipData simulationOwnershipData = simulationOwnershipData; + private readonly ILogger logger = logger; + + [Description("Query the entity associated with the given NitroxId")] + public async Task Execute(ICommandContext context, [Description("NitroxId of an entity")] NitroxId entityId) + { + if (!entityRegistry.TryGetEntityById(entityId, out Entity entity)) + { + await context.ReplyAsync($"Entity with id {entityId} not found"); + return; + } + + StringBuilder builder = new(); + builder.AppendLine("Entity"); + builder.AppendLine($" └ Type: {entity.GetType().Name}"); + builder.AppendLine($" └ Id: {entity.Id}"); + builder.AppendLine($" └ TechType: {entity.TechType}"); + builder.AppendLine($" └ ParentId: {entity.ParentId?.ToString() ?? ""}"); + builder.AppendLine($" └ Metadata: {entity.Metadata?.ToString() ?? ""}"); + builder.AppendLine($" └ Children: {entity.ChildEntities.Count}"); + if (entity.ChildEntities.Count > 0) + { + foreach (Entity childEntity in entity.ChildEntities) + { + builder.AppendLine(" └ Child"); + builder.AppendLine($" └ Type: {childEntity.GetType().Name}"); + builder.AppendLine($" └ Id: {childEntity.Id}"); + builder.AppendLine($" └ Metadata: {childEntity.Metadata?.ToString() ?? "none"}"); + builder.AppendLine($" └ Children: {childEntity.ChildEntities.Count}"); + } + } + + if (entity is WorldEntity worldEntity) + { + builder.AppendLine("World"); + builder.AppendLine($" └ ClassId: {worldEntity.ClassId}"); + builder.AppendLine($" └ Level: {worldEntity.Level}"); + builder.AppendLine($" └ SpawnedByServer: {worldEntity.SpawnedByServer}"); + builder.AppendLine($" └ {worldEntity.Transform}"); + builder.AppendLine($" └ Cell: {(worldEntity is GlobalRootEntity ? "global root" : worldEntity.AbsoluteEntityCell.ToString())}"); + } + + bool isLocked = simulationOwnershipData.TryGetLock(entityId, out SimulationOwnershipData.PlayerLock playerLock); + + builder.AppendLine("Lock status"); + builder.AppendLine($" └ Locked: {isLocked}"); + builder.AppendLine($" └ Owner: {(isLocked ? $"{playerLock.Player.Name} #{playerLock.Player.SessionId}" : "")}"); + + builder.AppendLine("Raw Data"); + builder.AppendLine(entity.ToString()); + + await context.ReplyAsync(builder.ToString()); + } +} diff --git a/Nitrox.Test/Model/DataStructures/NitroxIdTest.cs b/Nitrox.Test/Model/DataStructures/NitroxIdTest.cs index 67e506bfdc..6585402865 100644 --- a/Nitrox.Test/Model/DataStructures/NitroxIdTest.cs +++ b/Nitrox.Test/Model/DataStructures/NitroxIdTest.cs @@ -3,28 +3,55 @@ [TestClass] public class NitroxIdTest { - private NitroxId id1; - private NitroxId id2; - [TestMethod] public void SameGuidEquality() { + // Arrange Guid guid = Guid.NewGuid(); - id1 = new(guid); - id2 = new(guid); + NitroxId id1 = new(guid); + NitroxId id2 = new(guid); + // Act & Assert (id1 == id2).Should().BeTrue(); id1.Equals(id2).Should().BeTrue(); (id1 != id2).Should().BeFalse(); (!id1.Equals(id2)).Should().BeFalse(); } + [TestMethod] + public void SameReferenceEquality() + { + // Arrange + NitroxId id1 = new(Guid.NewGuid()); + NitroxId id2 = id1; + + // Act & Assert + (id1 == id2).Should().BeTrue(); + id1.Equals(id2).Should().BeTrue(); + id1.Equals((object)id2).Should().BeTrue(); + } + + [TestMethod] + public void DifferentGuidEquality() + { + // Arrange + NitroxId id1 = new(Guid.NewGuid()); + NitroxId id2 = new(Guid.NewGuid()); + + // Act & Assert + (id1 == id2).Should().BeFalse(); + id1.Equals(id2).Should().BeFalse(); + (id1 != id2).Should().BeTrue(); + } + [TestMethod] public void NullGuidEquality() { - id1 = new(); - id2 = null; + // Arrange + NitroxId id1 = new(); + NitroxId id2 = null; + // Act & Assert (id1 == id2).Should().BeFalse(); id1.Equals(id2).Should().BeFalse(); (id1 != id2).Should().BeTrue(); @@ -34,8 +61,101 @@ public void NullGuidEquality() [TestMethod] public void BothNullEquality() { - id1 = id2 = null; + // Arrange + NitroxId? id1 = null; + NitroxId? id2 = null; + + // Act & Assert (id1 != id2).Should().BeFalse(); (id1 == id2).Should().BeTrue(); } + + [TestMethod] + public void EqualsObjectReturnsFalseForDifferentType() + { + // Arrange + NitroxId id = new(Guid.NewGuid()); + + // Act & Assert + id.Equals(new object()).Should().BeFalse(); + } + + [TestMethod] + public void EqualIdsHaveSameHashCode() + { + // Arrange + Guid guid = Guid.NewGuid(); + NitroxId id1 = new(guid); + NitroxId id2 = new(guid); + + // Act & Assert + id1.GetHashCode().Should().Be(id2.GetHashCode()); + } + + [TestMethod] + public void CompareToReturnsZeroForEqualIds() + { + // Arrange + Guid guid = Guid.NewGuid(); + NitroxId id1 = new(guid); + NitroxId id2 = new(guid); + + // Act & Assert + id1.CompareTo(id2).Should().Be(0); + } + + [TestMethod] + public void CompareToReturnsOneForNull() + { + // Arrange + NitroxId id = new(Guid.NewGuid()); + + // Act & Assert + id.CompareTo(null).Should().Be(1); + } + + [TestMethod] + public void CompareToUsesUnderlyingGuidOrdering() + { + // Arrange + NitroxId id1 = new("00000000-0000-0000-0000-000000000001"); + NitroxId id2 = new("00000000-0000-0000-0000-000000000002"); + + // Act & Assert + id1.CompareTo(id2).Should().BeNegative(); + id2.CompareTo(id1).Should().BePositive(); + } + + [TestMethod] + public void IncrementAdvancesId() + { + // Arrange + NitroxId id = new(Guid.Empty); + + // Act + NitroxId next = id.Increment(); + + // Assert + next.Should().NotBe(id); + next.CompareTo(id).Should().BePositive(); + } + + [TestMethod] + public void IncrementWrapsAroundFromMaxValue() + { + // Arrange + NitroxId id = new(new Guid( + [ + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255 + ])); + + // Act + NitroxId wrapped = id.Increment(); + + // Assert + wrapped.Should().Be(new NitroxId(Guid.Empty)); + } } From bddbc2f920bbf7ae9f7f66e9739d0ff26f354912 Mon Sep 17 00:00:00 2001 From: NinjaPedroX <32976499+NinjaPedroX@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:10:03 -0500 Subject: [PATCH 48/59] Added player list tooltip to Nitrox Launcher (#2699) --- Nitrox.Launcher/Models/Design/ServerEntry.cs | 13 ++++++++++--- .../Models/Services/ServersManagement.cs | 5 +++-- .../ViewModels/ManageServerViewModel.cs | 12 ++++++------ Nitrox.Launcher/ViewModels/ServersViewModel.cs | 2 +- Nitrox.Launcher/Views/ManageServerView.axaml | 15 +++++++++++---- Nitrox.Launcher/Views/ServersView.axaml | 15 +++++++++++---- Nitrox.Model/MagicOnion/IServersManagement.cs | 2 +- .../Models/GameLogic/PlayerManager.cs | 2 -- .../Services/ServersManagementService.cs | 3 ++- 9 files changed, 45 insertions(+), 24 deletions(-) diff --git a/Nitrox.Launcher/Models/Design/ServerEntry.cs b/Nitrox.Launcher/Models/Design/ServerEntry.cs index d0c4e9dfe6..d0fd3c1912 100644 --- a/Nitrox.Launcher/Models/Design/ServerEntry.cs +++ b/Nitrox.Launcher/Models/Design/ServerEntry.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -92,7 +93,11 @@ internal sealed partial class ServerEntry : ObservableObject private Perms playerPermissions = serverDefaults.DefaultPlayerPerm; [ObservableProperty] - private int players; + private int playerCount; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PlayerNamesTooltip))] + private List playerNames = []; [ObservableProperty] private int port = serverDefaults.ServerPort; @@ -111,6 +116,7 @@ internal sealed partial class ServerEntry : ObservableObject internal ServerProcess? Process { get; private set; } public AvaloniaList Output { get; } = []; + public string? PlayerNamesTooltip => PlayerNames.Count == 0 ? null : string.Join(Environment.NewLine, PlayerNames); /// /// Gets the last process id known by this server entry. @@ -337,7 +343,7 @@ protected override void OnPropertyChanged(PropertyChangedEventArgs e) switch (e.PropertyName) { case nameof(IsOnline) when LastProcessId > 0: - WeakReferenceMessenger.Default.Send(new ServerStatusMessage(LastProcessId, IsOnline, Players)); + WeakReferenceMessenger.Default.Send(new ServerStatusMessage(LastProcessId, IsOnline, PlayerCount)); break; } base.OnPropertyChanged(e); @@ -372,7 +378,8 @@ await Dispatcher.UIThread.InvokeAsync(async () => } } CommandQueue = Channel.CreateUnbounded(); - Players = 0; + PlayerCount = 0; + PlayerNames = []; IsOnline = false; Output.Clear(); }); diff --git a/Nitrox.Launcher/Models/Services/ServersManagement.cs b/Nitrox.Launcher/Models/Services/ServersManagement.cs index 555fe95602..8133ade4b4 100644 --- a/Nitrox.Launcher/Models/Services/ServersManagement.cs +++ b/Nitrox.Launcher/Models/Services/ServersManagement.cs @@ -20,14 +20,15 @@ internal sealed class ServersManagement(ServerService serverService) : Streaming private int processId; private string saveName; - public ValueTask SetPlayerCount(int playerCount) + public ValueTask SetPlayers(string[] players) { ServerEntry? entry = serverService.GetServerEntryByAnyOf(processId, saveName); if (entry == null) { return CompletedTask; } - entry.Players = playerCount; + entry.PlayerCount = players.Length; + entry.PlayerNames = [..players]; return CompletedTask; } diff --git a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs index 0c96c66679..8c84fc0af8 100644 --- a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs @@ -111,7 +111,7 @@ internal partial class ManageServerViewModel : RoutableViewModelBase [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private int serverPlayers; + private int serverPlayerCount; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] @@ -147,7 +147,7 @@ public ManageServerViewModel(DialogService dialogService, StorageService storage return; } vm.ServerIsOnline = status.IsOnline; - vm.ServerPlayers = status.PlayerCount; + vm.ServerPlayerCount = status.PlayerCount; }); } @@ -185,7 +185,7 @@ public void LoadFrom(ServerEntry serverEntry) ServerDefaultPlayerPerm = Server.PlayerPermissions; ServerAutoSaveInterval = Server.AutoSaveInterval; ServerMaxPlayers = Server.MaxPlayers; - ServerPlayers = Server.Players; + ServerPlayerCount = Server.PlayerCount; ServerPort = Server.Port; ServerAutoPortForward = Server.PortForward; ServerAllowLanDiscovery = Server.AllowLanDiscovery; @@ -203,7 +203,7 @@ private bool HasChanges() => Server != null && ServerDefaultPlayerPerm != Server.PlayerPermissions || ServerAutoSaveInterval != Server.AutoSaveInterval || ServerMaxPlayers != Server.MaxPlayers || - ServerPlayers != Server.Players || + ServerPlayerCount != Server.PlayerCount || ServerPort != Server.Port || ServerAutoPortForward != Server.PortForward || ServerAllowLanDiscovery != Server.AllowLanDiscovery || @@ -261,7 +261,7 @@ private void Save() Server.PlayerPermissions = ServerDefaultPlayerPerm; Server.AutoSaveInterval = ServerAutoSaveInterval; Server.MaxPlayers = ServerMaxPlayers; - Server.Players = ServerPlayers; + Server.PlayerCount = ServerPlayerCount; Server.Port = ServerPort; Server.PortForward = ServerAutoPortForward; Server.AllowLanDiscovery = ServerAllowLanDiscovery; @@ -311,7 +311,7 @@ private void Undo() ServerDefaultPlayerPerm = Server.PlayerPermissions; ServerAutoSaveInterval = Server.AutoSaveInterval; ServerMaxPlayers = Server.MaxPlayers; - ServerPlayers = Server.Players; + ServerPlayerCount = Server.PlayerCount; ServerPort = Server.Port; ServerAutoPortForward = Server.PortForward; ServerAllowLanDiscovery = Server.AllowLanDiscovery; diff --git a/Nitrox.Launcher/ViewModels/ServersViewModel.cs b/Nitrox.Launcher/ViewModels/ServersViewModel.cs index d558a87acd..74c9ceb620 100644 --- a/Nitrox.Launcher/ViewModels/ServersViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ServersViewModel.cs @@ -41,7 +41,7 @@ public ServersViewModel(IKeyValueStore keyValueStore, DialogService dialogServic { return; } - entry.Players = message.PlayerCount; + entry.PlayerCount = message.PlayerCount; entry.IsOnline = message.IsOnline; }); diff --git a/Nitrox.Launcher/Views/ManageServerView.axaml b/Nitrox.Launcher/Views/ManageServerView.axaml index d5186cc615..894fca792b 100644 --- a/Nitrox.Launcher/Views/ManageServerView.axaml +++ b/Nitrox.Launcher/Views/ManageServerView.axaml @@ -119,11 +119,18 @@ - - - + + + - + + + + diff --git a/Nitrox.Launcher/Views/ServersView.axaml b/Nitrox.Launcher/Views/ServersView.axaml index 61a24599a5..76f504e213 100644 --- a/Nitrox.Launcher/Views/ServersView.axaml +++ b/Nitrox.Launcher/Views/ServersView.axaml @@ -118,11 +118,18 @@ - - - + + + - + + + + diff --git a/Nitrox.Model/MagicOnion/IServersManagement.cs b/Nitrox.Model/MagicOnion/IServersManagement.cs index 2a21655b8d..b719cd95ac 100644 --- a/Nitrox.Model/MagicOnion/IServersManagement.cs +++ b/Nitrox.Model/MagicOnion/IServersManagement.cs @@ -9,7 +9,7 @@ namespace Nitrox.Model.MagicOnion; /// public interface IServersManagement : IStreamingHub { - ValueTask SetPlayerCount(int playerCount); + ValueTask SetPlayers(string[] players); ValueTask AddOutputLine(string category, DateTimeOffset? localTime, int level, string message); } diff --git a/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs b/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs index 626faf4cec..62b28d6682 100644 --- a/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs +++ b/Nitrox.Server.Subnautica/Models/GameLogic/PlayerManager.cs @@ -188,8 +188,6 @@ public Player CreatePlayerData(SessionId sessionId, string reservationKey, out b return player; } - public int PlayerCount => connectedPlayersBySessionId.Count; - public bool SetPlayerProperty(SessionId sessionId, T value, Action action) { if (!TryGetPlayerBySessionId(sessionId, out Player? player)) diff --git a/Nitrox.Server.Subnautica/Services/ServersManagementService.cs b/Nitrox.Server.Subnautica/Services/ServersManagementService.cs index b75a07a609..af1f7ccc72 100644 --- a/Nitrox.Server.Subnautica/Services/ServersManagementService.cs +++ b/Nitrox.Server.Subnautica/Services/ServersManagementService.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.IO; +using System.Linq; using System.Threading.Channels; using Grpc.Core; using Grpc.Net.Client; @@ -114,7 +115,7 @@ Task CreateLoopingTask(Func action, private async Task PushPollDataAsync(IServersManagement api) { - await api.SetPlayerCount(playerManager.PlayerCount); + await api.SetPlayers(playerManager.ConnectedPlayers().Select(player => player.Name).ToArray()); } private async Task PushLogsAsync(IServersManagement api, CancellationToken cancellationToken) From d9e05d9a835f9b67eef5cc395b288fcc121a9312 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:33:01 +0200 Subject: [PATCH 49/59] Fix duplicated simulation state text inside NitroxEntity Debugger (#2701) Co-authored-by: Meas <1107063+Measurity@users.noreply.github.com> --- .../Drawer/Nitrox/NitroxEntityDrawer.cs | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/NitroxClient/Debuggers/Drawer/Nitrox/NitroxEntityDrawer.cs b/NitroxClient/Debuggers/Drawer/Nitrox/NitroxEntityDrawer.cs index 99e6497583..9b97ec0df2 100644 --- a/NitroxClient/Debuggers/Drawer/Nitrox/NitroxEntityDrawer.cs +++ b/NitroxClient/Debuggers/Drawer/Nitrox/NitroxEntityDrawer.cs @@ -9,61 +9,56 @@ namespace NitroxClient.Debuggers.Drawer.Nitrox; public class NitroxEntityDrawer : IDrawer, IDrawer { - private const float LABEL_WIDTH = 250; - public void Draw(NitroxEntity nitroxEntity) { - Draw(nitroxEntity.Id); + DrawNitroxIdField(nitroxEntity.Id); GUILayout.Space(8); using (new GUILayout.HorizontalScope()) { - GUILayout.Label("GameObject with IDs", GUILayout.Width(LABEL_WIDTH)); + GUILayout.Label("GameObject with IDs", GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); NitroxGUILayout.Separator(); GUILayout.TextField(NitroxEntity.GetGameObjects().Count().ToString()); } GUILayout.Space(8); - using (new GUILayout.HorizontalScope()) - { - GUILayout.Label("Simulating state", GUILayout.Width(LABEL_WIDTH)); - NitroxGUILayout.Separator(); - if (NitroxServiceLocator.Cache.Value.TryGetLockType(nitroxEntity.Id, out SimulationLockType simulationLockType)) - { - GUILayout.TextField(simulationLockType.ToString()); - } - else - { - GUILayout.TextField("NONE"); - } - } + DrawSimulatingStateField(nitroxEntity.Id); } public void Draw(NitroxId nitroxId) + { + DrawNitroxIdField(nitroxId); + + GUILayout.Space(8); + + DrawSimulatingStateField(nitroxId); + } + + private static void DrawNitroxIdField(NitroxId? nitroxId) { using (new GUILayout.HorizontalScope()) { - GUILayout.Label("NitroxId", GUILayout.Width(LABEL_WIDTH)); + GUILayout.Label("NitroxId", GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); NitroxGUILayout.Separator(); - GUILayout.TextField(nitroxId == null ? "ID IS NULL!!!" : nitroxId.ToString()); + GUILayout.TextField(nitroxId == null ? "" : nitroxId.ToString()); } + } - GUILayout.Space(8); - + private static void DrawSimulatingStateField(NitroxId? nitroxId) + { using (new GUILayout.HorizontalScope()) { - GUILayout.Label("Simulating state", GUILayout.Width(LABEL_WIDTH)); + GUILayout.Label("Simulating state", GUILayout.Width(NitroxGUILayout.DEFAULT_LABEL_WIDTH)); NitroxGUILayout.Separator(); if (NitroxServiceLocator.Cache.Value.TryGetLockType(nitroxId, out SimulationLockType simulationLockType)) { GUILayout.TextField(simulationLockType.ToString()); + return; } - else - { - GUILayout.TextField("NONE"); - } + + GUILayout.TextField("NONE"); } } } From 6642654d901a53eaea02f24391b364a12c8e67b6 Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:32:22 +0200 Subject: [PATCH 50/59] Improve PrefabPlaceHolder cache handling (#2704) --- .../PrefabPlaceholderGroupsResource.cs | 41 ++++++++++++++----- .../Resources/PrefabPlaceholderAsset.cs | 8 +++- .../Resources/PrefabPlaceholderRandomAsset.cs | 9 ++-- .../Resources/PrefabPlaceholdersGroupAsset.cs | 9 ++-- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs index 47f7e18fbc..dad60a465c 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/Parsers/PrefabPlaceholderGroupsResource.cs @@ -149,31 +149,43 @@ private async Task CreateOrLoadPrefabCacheAsync(string nitroxCachePath) { logger.ZLogWarning($"An error occurred while deserializing the prefab cache. Re-creating it: {ex.Message:@Error}"); } - if (cache is { Version: CACHE_VERSION }) + + if (cache is { } c && c.IsValid(CACHE_VERSION)) { - prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths; - randomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId; - groupsByClassId = cache.Value.GroupsByClassId; - placeholdersByClassId = cache.Value.PlaceholdersByClassId; + prefabPlaceholdersGroupPaths = c.PrefabPlaceholdersGroupPaths; + randomPossibilitiesByClassId = c.RandomPossibilitiesByClassId; + groupsByClassId = c.GroupsByClassId; + placeholdersByClassId = c.PlaceholdersByClassId; logger.ZLogDebug($"Successfully loaded cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours."); } // Fallback solution else { - if (cache.HasValue) + if (cache is { } invalidCache) { - logger.ZLogInformation($"Found outdated cache (is v{cache.Value.Version}, expected v{CACHE_VERSION})"); + if (invalidCache.Version != CACHE_VERSION) + { + logger.ZLogInformation($"Found outdated cache (is v{invalidCache.Version}, expected v{CACHE_VERSION})"); + } + else + { + logger.ZLogWarning($"Found cache v{CACHE_VERSION} but it contains no data. Re-creating it."); + } } + logger.ZLogInformation($"Building cache, this may take a while..."); + // Get all prefab-classIds linked to the (partial) bundle path string prefabDatabasePath = Path.Combine(options.Value.GetSubnauticaResourcesPath(), "StreamingAssets", "SNUnmanagedData", "prefabs.db"); Dictionary prefabDatabase = LoadPrefabDatabase(prefabDatabasePath); + (AddressableCatalogDictionary addressableCatalog, ClassIdByRuntimeKeyDictionary classIdByRuntimeKey) = LoadAddressableCatalog(options.Value.GetSubnauticaAaResourcePath(), prefabDatabase); - prefabPlaceholdersGroupPaths = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager, addressableCatalog, classIdByRuntimeKey), addressableCatalog, classIdByRuntimeKey)); + prefabPlaceholdersGroupPaths = new Dictionary(GetPrefabPlaceholderGroupAssetsByGroupClassId(assetsManager, GetAllPrefabPlaceholdersGroupsFast(assetsManager, addressableCatalog, classIdByRuntimeKey), addressableCatalog, classIdByRuntimeKey)); + await Cache.SerializeAsync(serializer, new Cache(CACHE_VERSION, prefabPlaceholdersGroupPaths, randomPossibilitiesByClassId, groupsByClassId, placeholdersByClassId), cacheFilePath); - logger.ZLogDebug( - $"Successfully built cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours. Future server starts will take less time."); + logger.ZLogDebug($"Successfully built cache with {prefabPlaceholdersGroupPaths.Count:@PrefabPlaceholdersCount} prefab placeholder groups and {randomPossibilitiesByClassId.Count:@RandomPossibilitiesCount} random spawn behaviours. Future server starts will take less time."); } + Validate.IsTrue(prefabPlaceholdersGroupPaths.Count > 0); Validate.IsTrue(randomPossibilitiesByClassId.Count > 0); Validate.IsTrue(groupsByClassId.Count > 0); @@ -471,7 +483,7 @@ private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(Sub return prefabPlaceholderAsset; } - private record struct Cache( + private readonly record struct Cache( int Version, Dictionary PrefabPlaceholdersGroupPaths, ConcurrentDictionary RandomPossibilitiesByClassId, @@ -479,6 +491,13 @@ private record struct Cache( ConcurrentDictionary PlaceholdersByClassId ) { + public bool IsValid(int expectedVersion) => + Version == expectedVersion && + PrefabPlaceholdersGroupPaths.Count > 0 && + RandomPossibilitiesByClassId.Count > 0 && + GroupsByClassId.Count > 0 && + PlaceholdersByClassId.Count > 0; + public static async Task SerializeAsync(JsonSerializer serializer, Cache cache, string filePath) { Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? throw new Exception("Failed to get directory path from cache file path")); diff --git a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderAsset.cs b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderAsset.cs index cb1e7d34b1..82904152c3 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderAsset.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderAsset.cs @@ -4,9 +4,13 @@ namespace Nitrox.Server.Subnautica.Models.Resources; -[Serializable] /// /// Some PrefabPlaceholders spawn GameObjects that are always there (decor, environment ...) /// And some others spawn a GameObject with an EntitySlot in which case this field is not null. /// -public record struct PrefabPlaceholderAsset(string ClassId, NitroxEntitySlot? EntitySlot = null, NitroxTransform Transform = null) : IPrefabAsset; +[Serializable] +public record struct PrefabPlaceholderAsset( + string ClassId, + NitroxEntitySlot? EntitySlot = null, + NitroxTransform? Transform = null +) : IPrefabAsset; diff --git a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderRandomAsset.cs b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderRandomAsset.cs index f39dee0df7..ad6805a9d4 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderRandomAsset.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholderRandomAsset.cs @@ -4,7 +4,8 @@ namespace Nitrox.Server.Subnautica.Models.Resources; -public record struct PrefabPlaceholderRandomAsset(List ClassIds, NitroxTransform Transform = null, string ClassId = null) : IPrefabAsset -{ - public NitroxTransform Transform { get; set; } = Transform; -} +public record struct PrefabPlaceholderRandomAsset( + List ClassIds, + NitroxTransform? Transform = null, + string? ClassId = null +) : IPrefabAsset; diff --git a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholdersGroupAsset.cs b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholdersGroupAsset.cs index d8897a0342..1699836cbc 100644 --- a/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholdersGroupAsset.cs +++ b/Nitrox.Server.Subnautica/Models/Resources/PrefabPlaceholdersGroupAsset.cs @@ -3,9 +3,12 @@ namespace Nitrox.Server.Subnautica.Models.Resources; - -[Serializable] /// /// All attached PrefabPlaceholders (and PrefabPlaceholdersGroup). Is in sync with PrefabPlaceholdersGroup.prefabPlaceholders /// -public record struct PrefabPlaceholdersGroupAsset(string ClassId, IPrefabAsset[] PrefabAssets, NitroxTransform Transform = null) : IPrefabAsset; +[Serializable] +public record struct PrefabPlaceholdersGroupAsset( + string ClassId, + IPrefabAsset[] PrefabAssets, + NitroxTransform? Transform = null +) : IPrefabAsset; From 11ea9657caea6ec628777c078461f449821184ac Mon Sep 17 00:00:00 2001 From: dartasen <10561268+dartasen@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:59:52 +0200 Subject: [PATCH 51/59] [Launcher] Migrate to partial properties for ObservableProperty (#2702) --- Directory.Packages.props | 2 +- .../Models/Design/AsyncCommandButtonTagger.cs | 2 +- .../Models/Design/NotificationItem.cs | 4 +- Nitrox.Launcher/Models/Design/ServerEntry.cs | 47 +++++++++---------- .../ViewModels/Abstract/ModalViewModelBase.cs | 7 +-- .../ViewModels/BackupRestoreViewModel.cs | 21 ++++----- Nitrox.Launcher/ViewModels/BlogViewModel.cs | 2 +- .../ViewModels/CrashWindowViewModel.cs | 5 +- .../ViewModels/CreateServerViewModel.cs | 5 +- .../Designer/DesignOptionsViewModel.cs | 8 ++-- .../ViewModels/DialogBoxViewModel.cs | 28 +++++++---- .../ViewModels/EmbeddedServerViewModel.cs | 9 ++-- .../ViewModels/LaunchGameViewModel.cs | 4 +- .../ViewModels/MainWindowViewModel.cs | 8 ++-- .../ViewModels/ManageServerViewModel.cs | 40 ++++++++-------- .../ObjectPropertyEditorViewModel.cs | 10 ++-- .../ViewModels/OptionsViewModel.cs | 34 +++++++------- .../ViewModels/ServersViewModel.cs | 3 +- .../ViewModels/UpdatesViewModel.cs | 17 ++++--- Nitrox.Launcher/Views/MainWindow.axaml | 2 +- Nitrox.Launcher/Views/OptionsView.axaml | 16 +++---- 21 files changed, 142 insertions(+), 132 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 188852d146..b78046676c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + diff --git a/Nitrox.Launcher/Models/Design/AsyncCommandButtonTagger.cs b/Nitrox.Launcher/Models/Design/AsyncCommandButtonTagger.cs index d83939affc..f61ccad9e6 100644 --- a/Nitrox.Launcher/Models/Design/AsyncCommandButtonTagger.cs +++ b/Nitrox.Launcher/Models/Design/AsyncCommandButtonTagger.cs @@ -11,7 +11,7 @@ namespace Nitrox.Launcher.Models.Design; /// /// Listens for async command changes on buttons to add the chosen classname to, for use with styling. /// -public class AsyncCommandButtonTagger : IDisposable +public sealed class AsyncCommandButtonTagger : IDisposable { public string ClassName { get; init; } private readonly ConcurrentDictionary states = []; diff --git a/Nitrox.Launcher/Models/Design/NotificationItem.cs b/Nitrox.Launcher/Models/Design/NotificationItem.cs index f6516878ef..15d5f6d2bc 100644 --- a/Nitrox.Launcher/Models/Design/NotificationItem.cs +++ b/Nitrox.Launcher/Models/Design/NotificationItem.cs @@ -9,11 +9,13 @@ namespace Nitrox.Launcher.Models.Design; public partial class NotificationItem : ObservableObject { public string Message { get; } + public NotificationType Type { get; } + public ICommand CloseCommand { get; } [ObservableProperty] - private bool dismissed; + public partial bool IsDismissed { get; set; } public NotificationItem(string message, NotificationType type = NotificationType.Information, ICommand? closeCommand = null) { diff --git a/Nitrox.Launcher/Models/Design/ServerEntry.cs b/Nitrox.Launcher/Models/Design/ServerEntry.cs index d0fd3c1912..3cd5f34129 100644 --- a/Nitrox.Launcher/Models/Design/ServerEntry.cs +++ b/Nitrox.Launcher/Models/Design/ServerEntry.cs @@ -36,83 +36,82 @@ internal sealed partial class ServerEntry : ObservableObject public const string DEFAULT_SERVER_ICON_NAME = "servericon.png"; private static readonly ConcurrentDictionary entriesByDirectory = []; - private static readonly SubnauticaServerOptions serverDefaults = new(); [ObservableProperty] - private bool allowCommands = !serverDefaults.DisableConsole; + public partial bool AllowCommands { get; set; } = !serverDefaults.DisableConsole; [ObservableProperty] - private bool allowKeepInventory = serverDefaults.KeepInventoryOnDeath; + public partial bool AllowKeepInventory { get; set; } = serverDefaults.KeepInventoryOnDeath; [ObservableProperty] - private bool allowLanDiscovery = serverDefaults.LanDiscovery; + public partial bool AllowLanDiscovery { get; set; } = serverDefaults.LanDiscovery; [ObservableProperty] - private bool allowPvP = serverDefaults.PvpEnabled; + public partial bool AllowPvP { get; set; } = serverDefaults.PvpEnabled; [ObservableProperty] - private int autoSaveInterval = serverDefaults.SaveInterval / 1000; + public partial int AutoSaveInterval { get; set; } = serverDefaults.SaveInterval / 1000; public Channel CommandQueue = Channel.CreateUnbounded(); private CancellationTokenSource? cts; [ObservableProperty] - private SubnauticaGameMode gameMode = serverDefaults.GameMode; + public partial SubnauticaGameMode GameMode { get; set; } = serverDefaults.GameMode; /// /// Should not be set to persist change. Use instead. /// [ObservableProperty] - private bool isEmbedded; + public partial bool IsEmbedded { get; set; } [ObservableProperty] - private bool isNewServer = true; + public partial bool IsNewServer { get; set; } = true; [ObservableProperty] - private bool isOnline; + public partial bool IsOnline { get; set; } [ObservableProperty] - private bool isServerClosing; + public partial bool IsServerClosing { get; set; } [ObservableProperty] - private DateTime lastAccessedTime = DateTime.Now; + public partial DateTime LastAccessedTime { get; set; } = DateTime.Now; private int lastProcessId; [ObservableProperty] - private int maxPlayers = serverDefaults.MaxConnections; + public partial int MaxPlayers { get; set; } = serverDefaults.MaxConnections; [ObservableProperty] - private string? name; + public partial string? Name { get; set; } [ObservableProperty] - private string? password; + public partial string? Password { get; set; } [ObservableProperty] - private Perms playerPermissions = serverDefaults.DefaultPlayerPerm; + public partial Perms PlayerPermissions { get; set; } = serverDefaults.DefaultPlayerPerm; [ObservableProperty] - private int playerCount; + public partial int PlayerCount { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(PlayerNamesTooltip))] - private List playerNames = []; + public partial List PlayerNames { get; set; } = []; [ObservableProperty] - private int port = serverDefaults.ServerPort; + public partial int Port { get; set; } = serverDefaults.ServerPort; [ObservableProperty] - private bool portForward = serverDefaults.PortForward; + public partial bool PortForward { get; set; } = serverDefaults.PortForward; [ObservableProperty] - private string? seed; + public partial string? Seed { get; set; } [ObservableProperty] - private Bitmap? serverIcon; + public partial Bitmap? ServerIcon { get; set; } [ObservableProperty] - private Version version = NitroxEnvironment.Version; + public partial Version Version { get; set; } = NitroxEnvironment.Version; internal ServerProcess? Process { get; private set; } public AvaloniaList Output { get; } = []; @@ -357,7 +356,7 @@ internal async Task ResetCtsAsync(bool cancelPreexisting = false) await c.CancelAsync(); } cts?.Dispose(); - cts = new(); + cts = new CancellationTokenSource(); cts.Token.Register(async void () => { try diff --git a/Nitrox.Launcher/ViewModels/Abstract/ModalViewModelBase.cs b/Nitrox.Launcher/ViewModels/Abstract/ModalViewModelBase.cs index e4e6b90e74..a20468e205 100644 --- a/Nitrox.Launcher/ViewModels/Abstract/ModalViewModelBase.cs +++ b/Nitrox.Launcher/ViewModels/Abstract/ModalViewModelBase.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using CommunityToolkit.Mvvm.ComponentModel; @@ -13,8 +13,9 @@ namespace Nitrox.Launcher.ViewModels.Abstract; /// public abstract partial class ModalViewModelBase : ObservableValidator, IMessageReceiver { - [ObservableProperty] private ButtonOptions? selectedOption; - + [ObservableProperty] + public partial ButtonOptions? SelectedOption { get; set; } + protected ModalViewModelBase() { // Always run validation first so HasErrors is set (i.e. trigger CanExecute logic). diff --git a/Nitrox.Launcher/ViewModels/BackupRestoreViewModel.cs b/Nitrox.Launcher/ViewModels/BackupRestoreViewModel.cs index 332247ade5..bb9b6bba57 100644 --- a/Nitrox.Launcher/ViewModels/BackupRestoreViewModel.cs +++ b/Nitrox.Launcher/ViewModels/BackupRestoreViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -11,26 +11,30 @@ using Nitrox.Launcher.Models.Validators; using Nitrox.Launcher.ViewModels.Abstract; using Nitrox.Model.Constants; -using Nitrox.Server.Subnautica.Models.Serialization.World; namespace Nitrox.Launcher.ViewModels; public partial class BackupRestoreViewModel : ModalViewModelBase { [ObservableProperty] - private AvaloniaList backups = []; + public partial AvaloniaList Backups { get; set; } = []; [ObservableProperty] - private string? saveFolderDirectory; + public partial string? SaveFolderDirectory { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RestoreBackupCommand))] [NotifyDataErrorInfo] [Backup] - private BackupItem? selectedBackup; + public partial BackupItem? SelectedBackup { get; set; } [ObservableProperty] - private string? title; + public partial string? Title { get; set; } + + [RelayCommand(CanExecute = nameof(CanRestoreBackup))] + public void RestoreBackup() => Close(ButtonOptions.Ok); + + public bool CanRestoreBackup() => !HasErrors; protected override void OnPropertyChanged(PropertyChangedEventArgs e) { @@ -44,11 +48,6 @@ protected override void OnPropertyChanged(PropertyChangedEventArgs e) } } - [RelayCommand(CanExecute = nameof(CanRestoreBackup))] - public void RestoreBackup() => Close(ButtonOptions.Ok); - - public bool CanRestoreBackup() => !HasErrors; - private static IEnumerable GetBackups(string? saveDirectory) { IEnumerable GetBackupFilePaths(string backupRootDir) => diff --git a/Nitrox.Launcher/ViewModels/BlogViewModel.cs b/Nitrox.Launcher/ViewModels/BlogViewModel.cs index 9861793c9c..bcb12486de 100644 --- a/Nitrox.Launcher/ViewModels/BlogViewModel.cs +++ b/Nitrox.Launcher/ViewModels/BlogViewModel.cs @@ -20,7 +20,7 @@ internal sealed partial class BlogViewModel : RoutableViewModelBase public static Bitmap FallbackImage { get; } = AssetHelper.GetAssetFromStream("/Assets/Images/blog/vines.png", static stream => new Bitmap(stream)); [ObservableProperty] - private AvaloniaList nitroxBlogs = []; + public partial AvaloniaList NitroxBlogs { get; set; } = []; public BlogViewModel() { diff --git a/Nitrox.Launcher/ViewModels/CrashWindowViewModel.cs b/Nitrox.Launcher/ViewModels/CrashWindowViewModel.cs index 1f12d51110..a0f0dcde5e 100644 --- a/Nitrox.Launcher/ViewModels/CrashWindowViewModel.cs +++ b/Nitrox.Launcher/ViewModels/CrashWindowViewModel.cs @@ -13,9 +13,10 @@ namespace Nitrox.Launcher.ViewModels; internal partial class CrashWindowViewModel : ViewModelBase { [ObservableProperty] - private string? title; + public partial string? Title { get; set; } + [ObservableProperty] - private string? message; + public partial string? Message { get; set; } [RelayCommand(CanExecute = nameof(CanRestart))] private void Restart() diff --git a/Nitrox.Launcher/ViewModels/CreateServerViewModel.cs b/Nitrox.Launcher/ViewModels/CreateServerViewModel.cs index 894e357bf8..e4340a1d18 100644 --- a/Nitrox.Launcher/ViewModels/CreateServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/CreateServerViewModel.cs @@ -22,15 +22,16 @@ public partial class CreateServerViewModel : ModalViewModelBase [FileName] [NotEndsWith(".")] [NitroxUniqueSaveName(nameof(SavesFolderDir))] - private string name = ""; + public partial string Name { get; set; } = ""; [ObservableProperty] - private SubnauticaGameMode selectedGameMode = SubnauticaGameMode.SURVIVAL; + public partial SubnauticaGameMode SelectedGameMode { get; set; } = SubnauticaGameMode.SURVIVAL; private string SavesFolderDir => keyValueStore.GetSavesFolderDir(); public CreateServerViewModel() { + } public CreateServerViewModel(IKeyValueStore keyValueStore) diff --git a/Nitrox.Launcher/ViewModels/Designer/DesignOptionsViewModel.cs b/Nitrox.Launcher/ViewModels/Designer/DesignOptionsViewModel.cs index 0dd405589e..f317a87d05 100644 --- a/Nitrox.Launcher/ViewModels/Designer/DesignOptionsViewModel.cs +++ b/Nitrox.Launcher/ViewModels/Designer/DesignOptionsViewModel.cs @@ -13,9 +13,9 @@ public DesignOptionsViewModel() : base(null!, null!) Platform = Platform.STEAM }; LaunchArgs = "-vrmode none"; - ProgramDataFolderDir = @"C:\Users\Me\AppData\Roaming\Nitrox"; - ScreenshotsFolderDir = @"C:\Users\Me\AppData\Roaming\Nitrox\screenshots"; - SavesFolderDir = @"C:\Users\Me\AppData\Roaming\Nitrox\saves"; - LogsFolderDir = @"C:\Users\Me\AppData\Roaming\Nitrox\logs"; + ProgramDataPath = @"C:\Users\Me\AppData\Roaming\Nitrox"; + ScreenshotsPath = @"C:\Users\Me\AppData\Roaming\Nitrox\screenshots"; + SavesPath = @"C:\Users\Me\AppData\Roaming\Nitrox\saves"; + LogsPath = @"C:\Users\Me\AppData\Roaming\Nitrox\logs"; } } diff --git a/Nitrox.Launcher/ViewModels/DialogBoxViewModel.cs b/Nitrox.Launcher/ViewModels/DialogBoxViewModel.cs index f1b489745e..ed1d65ce1b 100644 --- a/Nitrox.Launcher/ViewModels/DialogBoxViewModel.cs +++ b/Nitrox.Launcher/ViewModels/DialogBoxViewModel.cs @@ -12,17 +12,29 @@ namespace Nitrox.Launcher.ViewModels; /// public partial class DialogBoxViewModel : ModalViewModelBase { - [ObservableProperty] private string? windowTitle; + [ObservableProperty] + public partial string? WindowTitle { get; set; } - [ObservableProperty] private string title = ""; - [ObservableProperty] private double titleFontSize = 24; - [ObservableProperty] private FontWeight titleFontWeight = FontWeight.Bold; + [ObservableProperty] + public partial string Title { get; set; } = ""; - [ObservableProperty] private string description = ""; - [ObservableProperty] private double descriptionFontSize = 14; - [ObservableProperty] private FontWeight descriptionFontWeight = FontWeight.Normal; + [ObservableProperty] + public partial double TitleFontSize { get; set; } = 24; - [ObservableProperty] private ButtonOptions buttonOptions = ButtonOptions.Ok; + [ObservableProperty] + public partial FontWeight TitleFontWeight { get; set; } = FontWeight.Bold; + + [ObservableProperty] + public partial string Description { get; set; } = ""; + + [ObservableProperty] + public partial double DescriptionFontSize { get; set; } = 14; + + [ObservableProperty] + public partial FontWeight DescriptionFontWeight { get; set; } = FontWeight.Normal; + + [ObservableProperty] + public partial ButtonOptions ButtonOptions { get; set; } = ButtonOptions.Ok; public KeyGesture OkHotkey { get; } = new(Key.Return); public KeyGesture NoHotkey { get; } = new(Key.Escape); diff --git a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs index 2021f0bd1c..7cb08ebcf2 100644 --- a/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/EmbeddedServerViewModel.cs @@ -23,13 +23,14 @@ internal partial class EmbeddedServerViewModel : RoutableViewModelBase private int? selectedHistoryIndex; [ObservableProperty] - private string? serverCommand; + public partial string? ServerCommand { get; set; } [ObservableProperty] - private ServerEntry serverEntry; + public partial ServerEntry ServerEntry { get; set; } + [ObservableProperty] - private bool shouldAutoScroll = true; + public partial bool ShouldAutoScroll { get; set; } = true; private int previousContentLength; @@ -37,7 +38,7 @@ internal partial class EmbeddedServerViewModel : RoutableViewModelBase public EmbeddedServerViewModel(ServerEntry serverEntry) { - this.serverEntry = serverEntry; + ServerEntry = serverEntry; this.RegisterMessageListener(static (status, model) => { if (status.ProcessId != model.ServerEntry.LastProcessId) diff --git a/Nitrox.Launcher/ViewModels/LaunchGameViewModel.cs b/Nitrox.Launcher/ViewModels/LaunchGameViewModel.cs index 89e8ce73df..f7c800eb21 100644 --- a/Nitrox.Launcher/ViewModels/LaunchGameViewModel.cs +++ b/Nitrox.Launcher/ViewModels/LaunchGameViewModel.cs @@ -34,10 +34,10 @@ internal partial class LaunchGameViewModel(DialogService dialogService, ServerSe private readonly ServerService serverService = serverService; [ObservableProperty] - private Platform gamePlatform; + public partial Platform GamePlatform { get; set; } [ObservableProperty] - private string? platformToolTip; + public partial string? PlatformToolTip { get; set; } public Bitmap[] GalleryImageSources { get; } = [ diff --git a/Nitrox.Launcher/ViewModels/MainWindowViewModel.cs b/Nitrox.Launcher/ViewModels/MainWindowViewModel.cs index 08339d09b4..27e77127b5 100644 --- a/Nitrox.Launcher/ViewModels/MainWindowViewModel.cs +++ b/Nitrox.Launcher/ViewModels/MainWindowViewModel.cs @@ -35,10 +35,10 @@ internal partial class MainWindowViewModel : ViewModelBase, IRoutingScreen private readonly UpdatesViewModel updatesViewModel; [ObservableProperty] - private object? activeViewModel; + public partial object? ActiveViewModel { get; set; } [ObservableProperty] - private bool updateAvailableOrUnofficial; + public partial bool UpdateAvailableOrUnofficial { get; set; } public AvaloniaList Notifications { get; init; } = []; @@ -75,7 +75,7 @@ IKeyValueStore keyValueStore }); this.RegisterMessageListener(static async (message, vm) => { - message.Item.Dismissed = true; + message.Item.IsDismissed = true; await Task.Delay(1000); // Wait for animations if (!IsDesignMode) // Prevent design preview crashes { @@ -145,7 +145,7 @@ public async Task ClosingAsync(WindowClosingEventArgs args) } // As closing handler isn't async, cancellation might have happened anyway. So check manually if we should close the window after all the tasks are done. - if (args.Cancel == false && mainWindowProvider().IsClosingByUser(args)) + if (!args.Cancel && mainWindowProvider().IsClosingByUser(args)) { mainWindowProvider().CloseByCode(); } diff --git a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs index 8c84fc0af8..5ab0e4647f 100644 --- a/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ManageServerViewModel.cs @@ -41,60 +41,59 @@ internal partial class ManageServerViewModel : RoutableViewModelBase private readonly StorageService storageService; [ObservableProperty] - private ServerEntry? server; + public partial ServerEntry? Server { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private bool serverAllowCommands; + public partial bool ServerAllowCommands { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private bool serverAllowKeepInventory; + public partial bool ServerAllowKeepInventory { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private bool serverAllowLanDiscovery; + public partial bool ServerAllowLanDiscovery { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private bool serverAllowPvP; + public partial bool ServerAllowPvP { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private bool serverAutoPortForward; + public partial bool ServerAutoPortForward { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] [NotifyDataErrorInfo] [Range(10, 86400, ErrorMessage = "Value must be between 10s and 24 hours (86400s).")] - private int serverAutoSaveInterval; + public partial int ServerAutoSaveInterval { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private Perms serverDefaultPlayerPerm; + public partial Perms ServerDefaultPlayerPerm { get; set; } [ObservableProperty] - private bool serverEmbedded; + public partial bool ServerEmbedded { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private SubnauticaGameMode serverGameMode; + public partial SubnauticaGameMode ServerGameMode { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private Bitmap? serverIcon; - + public partial Bitmap? ServerIcon { get; set; } private string? serverIconDir; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RestoreBackupCommand), nameof(DeleteServerCommand))] - private bool serverIsOnline; + public partial bool ServerIsOnline { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] [Range(1, 1000)] [NotifyDataErrorInfo] - private int serverMaxPlayers; + public partial int ServerMaxPlayers { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] @@ -103,28 +102,27 @@ internal partial class ManageServerViewModel : RoutableViewModelBase [FileName] [NotEndsWith(".")] [NitroxUniqueSaveName(nameof(SavesFolderDir), true, nameof(OriginalServerName))] - private string? serverName; + public partial string? ServerName { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private string? serverPassword; + public partial string? ServerPassword { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] - private int serverPlayerCount; + public partial int ServerPlayerCount { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] [NotifyDataErrorInfo] [Range(ushort.MinValue, ushort.MaxValue)] - private int serverPort; + public partial int ServerPort { get; set; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(RestoreBackupCommand), nameof(StartServerCommand))] [NotifyDataErrorInfo] [NitroxWorldSeed] - private string? serverSeed; - + public partial string? ServerSeed { get; set; } public static Array PlayerPerms => Enum.GetValues(typeof(Perms)); public string? OriginalServerName => Server?.Name; @@ -142,7 +140,7 @@ public ManageServerViewModel(DialogService dialogService, StorageService storage this.RegisterMessageListener((status, vm) => { - if (vm.server?.Process?.Id != status.ProcessId) + if (vm.Server?.Process?.Id != status.ProcessId) { return; } diff --git a/Nitrox.Launcher/ViewModels/ObjectPropertyEditorViewModel.cs b/Nitrox.Launcher/ViewModels/ObjectPropertyEditorViewModel.cs index 326cbf080e..630f00f358 100644 --- a/Nitrox.Launcher/ViewModels/ObjectPropertyEditorViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ObjectPropertyEditorViewModel.cs @@ -17,22 +17,20 @@ internal partial class ObjectPropertyEditorViewModel(DialogService dialogService private readonly DialogService dialogService = dialogService; [ObservableProperty] - private AvaloniaList editorFields = []; + public partial AvaloniaList EditorFields { get; set; } = []; [ObservableProperty] - private object? ownerObject; + public partial object? OwnerObject { get; set; } [ObservableProperty] - private string? title; - + public partial string? Title { get; set; } /// /// Gets or sets the field filter to use. If filter returns false, it will omit the field. /// public Func FieldAcceptFilter { get; set; } = _ => true; [ObservableProperty] - private bool disableButtons; - + public partial bool DisableButtons { get; set; } [RelayCommand(CanExecute = nameof(CanSave))] public async Task Save() { diff --git a/Nitrox.Launcher/ViewModels/OptionsViewModel.cs b/Nitrox.Launcher/ViewModels/OptionsViewModel.cs index 41ed632aa6..439802eb11 100644 --- a/Nitrox.Launcher/ViewModels/OptionsViewModel.cs +++ b/Nitrox.Launcher/ViewModels/OptionsViewModel.cs @@ -27,38 +27,38 @@ internal partial class OptionsViewModel(IKeyValueStore keyValueStore, StorageSer [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SetArgumentsCommand))] - private string launchArgs; + public partial string LaunchArgs { get; set; } [ObservableProperty] - private string programDataFolderDir; + public partial string ProgramDataPath { get; set; } [ObservableProperty] - private string screenshotsFolderDir; + public partial string ScreenshotsPath { get; set; } [ObservableProperty] - private string savesFolderDir; + public partial string SavesPath { get; set; } [ObservableProperty] - private string logsFolderDir; + public partial string LogsPath { get; set; } [ObservableProperty] - private KnownGame selectedGame; + public partial KnownGame SelectedGame { get; set; } [ObservableProperty] - private bool showResetArgsBtn; + public partial bool ShowResetArgsBtn { get; set; } [ObservableProperty] - private bool lightModeEnabled; + public partial bool LightModeEnabled { get; set; } [ObservableProperty] - private bool allowMultipleGameInstances; + public partial bool AllowMultipleGameInstances { get; set; } [ObservableProperty] - private bool useBigPictureMode; - - [ObservableProperty] - private bool isInReleaseMode; + public partial bool UseBigPictureMode { get; set; } + [ObservableProperty] + public partial bool IsInReleaseMode { get; set; } + private static string DefaultLaunchArg => "-vrmode none"; private bool isResettingArgs; @@ -66,10 +66,10 @@ internal override async Task ViewContentLoadAsync(CancellationToken cancellation { SelectedGame = new() { PathToGame = NitroxUser.GamePath, Platform = NitroxUser.GamePlatform?.Platform ?? Platform.NONE }; LaunchArgs = keyValueStore.GetLaunchArguments(GameInfo.Subnautica, DefaultLaunchArg); - ProgramDataFolderDir = NitroxUser.AppDataPath; - ScreenshotsFolderDir = NitroxUser.ScreenshotsPath; - SavesFolderDir = keyValueStore.GetSavesFolderDir(); - LogsFolderDir = Model.Logger.Log.LogDirectory; + ProgramDataPath = NitroxUser.AppDataPath; + ScreenshotsPath = NitroxUser.ScreenshotsPath; + SavesPath = keyValueStore.GetSavesFolderDir(); + LogsPath = Model.Logger.Log.LogDirectory; LightModeEnabled = keyValueStore.GetIsLightModeEnabled(); AllowMultipleGameInstances = keyValueStore.GetIsMultipleGameInstancesAllowed(); UseBigPictureMode = keyValueStore.GetUseBigPictureMode(); diff --git a/Nitrox.Launcher/ViewModels/ServersViewModel.cs b/Nitrox.Launcher/ViewModels/ServersViewModel.cs index 74c9ceb620..3778e97a38 100644 --- a/Nitrox.Launcher/ViewModels/ServersViewModel.cs +++ b/Nitrox.Launcher/ViewModels/ServersViewModel.cs @@ -25,8 +25,7 @@ internal partial class ServersViewModel : RoutableViewModelBase private readonly ServerService serverService; private readonly ManageServerViewModel manageServerViewModel; [ObservableProperty] - private AvaloniaList? servers; - + public partial AvaloniaList? Servers { get; set; } public ServersViewModel(IKeyValueStore keyValueStore, DialogService dialogService, ServerService serverService, ManageServerViewModel manageServerViewModel) { this.keyValueStore = keyValueStore; diff --git a/Nitrox.Launcher/ViewModels/UpdatesViewModel.cs b/Nitrox.Launcher/ViewModels/UpdatesViewModel.cs index f15edcdbe7..ed4f282799 100644 --- a/Nitrox.Launcher/ViewModels/UpdatesViewModel.cs +++ b/Nitrox.Launcher/ViewModels/UpdatesViewModel.cs @@ -32,29 +32,28 @@ internal partial class UpdatesViewModel(NitroxWebsiteApiService nitroxWebsiteApi private CancellationTokenSource? downloadCts; [ObservableProperty] - private double downloadProgress; + public partial double DownloadProgress { get; set; } [ObservableProperty] - private string? downloadStatus; + public partial string? DownloadStatus { get; set; } [ObservableProperty] - private bool newUpdateAvailable; + public partial bool NewUpdateAvailable { get; set; } [ObservableProperty] - private AvaloniaList nitroxChangelogs = []; + public partial AvaloniaList NitroxChangelogs { get; set; } = []; [ObservableProperty] - private string? officialVersion; + public partial string? OfficialVersion { get; set; } [ObservableProperty] - private bool usingOfficialVersion; + public partial bool UsingOfficialVersion { get; set; } [ObservableProperty] - private AvaloniaList availableBackups = []; + public partial AvaloniaList AvailableBackups { get; set; } = []; [ObservableProperty] - private string? version; - + public partial string? Version { get; set; } public async Task IsNitroxUpdateAvailableAsync() { try diff --git a/Nitrox.Launcher/Views/MainWindow.axaml b/Nitrox.Launcher/Views/MainWindow.axaml index bad357283d..daaad65165 100644 --- a/Nitrox.Launcher/Views/MainWindow.axaml +++ b/Nitrox.Launcher/Views/MainWindow.axaml @@ -384,7 +384,7 @@