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/.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
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 @@
truefalsetrue
+
+ false
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1450e135cd..b78046676c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,21 +1,28 @@
+
truetrue
+
+
+ 11.3.12
+ 10.0.5
+
+
-
-
+
+
-
+
-
+
-
+
@@ -26,24 +33,24 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
@@ -60,8 +67,9 @@
-
+
+
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": "Скасувати",
diff --git a/Nitrox.Launcher/Models/Controls/CustomTitlebar.axaml b/Nitrox.Launcher/Models/Controls/CustomTitlebar.axaml
index de1cff957c..18629d45cc 100644
--- a/Nitrox.Launcher/Models/Controls/CustomTitlebar.axaml
+++ b/Nitrox.Launcher/Models/Controls/CustomTitlebar.axaml
@@ -2,6 +2,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Nitrox.Launcher.Models.Controls"
+ xmlns:design="clr-namespace:Nitrox.Launcher.Models.Design"
xmlns:converters="clr-namespace:Nitrox.Launcher.Models.Converters">
@@ -77,6 +78,7 @@
+
+
diff --git a/Nitrox.Launcher/Views/OptionsView.axaml b/Nitrox.Launcher/Views/OptionsView.axaml
index 4ba8efe0ef..0b4a3c9bc3 100644
--- a/Nitrox.Launcher/Views/OptionsView.axaml
+++ b/Nitrox.Launcher/Views/OptionsView.axaml
@@ -236,12 +236,12 @@
FontSize="15"
Foreground="{DynamicResource BrandBlack}"
Opacity="0.75"
- Text="{Binding ProgramDataFolderDir}"
+ Text="{Binding ProgramDataPath}"
VerticalAlignment="Center" />
/// 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.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs b/Nitrox.Model/Platforms/Discovery/InstallationFinders/SteamFinder.cs
index 59a5147bb0..3b6429d835 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,19 @@ 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))
+ // 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);
if (string.IsNullOrWhiteSpace(homePath))
{
homePath = Environment.GetEnvironmentVariable("HOME");
}
-
if (!Directory.Exists(homePath))
{
- return null;
+ return "";
}
// Steam should always be here
@@ -123,7 +73,7 @@ public GameFinderResult FindGame(GameInfo gameInfo)
}
}
- return null;
+ return Path.GetDirectoryName(Steam.GetExeFile()) ?? "";
}
///
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.Model/Platforms/OS/Windows/WindowsApi.cs b/Nitrox.Model/Platforms/OS/Windows/WindowsApi.cs
index 459729a0c4..a8e114212b 100644
--- a/Nitrox.Model/Platforms/OS/Windows/WindowsApi.cs
+++ b/Nitrox.Model/Platforms/OS/Windows/WindowsApi.cs
@@ -1,17 +1,17 @@
using System;
using System.Runtime.InteropServices;
-using Nitrox.Model.Platforms.OS.Windows.Internal;
using static Nitrox.Model.Platforms.OS.Windows.Internal.Win32Native;
namespace Nitrox.Model.Platforms.OS.Windows;
-public class WindowsApi
+public static partial class WindowsApi
{
///
/// Applies default OS animations to the window handle.
///
///
- /// Note on Windows OS: it will force enable resizing of a Window if is true. Make sure to set it correctly.
+ /// Note on Windows OS: it will force enable resizing of a Window if is true. Make sure to set
+ /// it correctly.
///
public static void EnableDefaultWindowAnimations(nint windowHandle, bool canResize)
{
@@ -20,10 +20,10 @@ public static void EnableDefaultWindowAnimations(nint windowHandle, bool canResi
return;
}
- Win32Native.WS dwNewLong = Win32Native.WS.WS_CAPTION | Win32Native.WS.WS_CLIPCHILDREN | Win32Native.WS.WS_MINIMIZEBOX | Win32Native.WS.WS_MAXIMIZEBOX | Win32Native.WS.WS_SYSMENU;
+ WS dwNewLong = WS.WS_CAPTION | WS.WS_CLIPCHILDREN | WS.WS_MINIMIZEBOX | WS.WS_MAXIMIZEBOX | WS.WS_SYSMENU;
if (canResize)
{
- dwNewLong |= Win32Native.WS.WS_SIZEBOX;
+ dwNewLong |= WS.WS_SIZEBOX;
}
HandleRef handle = new(null, windowHandle);
@@ -53,12 +53,26 @@ public static void BringProcessToFront(IntPtr windowHandle)
SetForegroundWindow(windowHandle);
}
+#if NET
+ [LibraryImport("User32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SetForegroundWindow(IntPtr handle);
+
+ [LibraryImport("User32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool ShowWindow(IntPtr handle, int nCmdShow);
+
+ [LibraryImport("User32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool IsIconic(IntPtr handle);
+#else
[DllImport("User32.dll")]
private static extern bool SetForegroundWindow(IntPtr handle);
+
[DllImport("User32.dll")]
private static extern bool ShowWindow(IntPtr handle, int nCmdShow);
+
[DllImport("User32.dll")]
private static extern bool IsIconic(IntPtr handle);
- [DllImport("User32.dll", CharSet = CharSet.Unicode)]
- private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
+#endif
}
diff --git a/Nitrox.Model/Platforms/Store/Steam.cs b/Nitrox.Model/Platforms/Store/Steam.cs
index 608c882218..c3d3ffd667 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)
{
- steamPath = Path.Combine(userHomePath, ".var", "app", "com.valvesoftware.Steam", "data", "Steam");
+ try
+ {
+ if (Directory.GetFileSystemEntries(path).Any())
+ {
+ steamPath = path;
+ break;
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ if (!string.IsNullOrWhiteSpace(steamPath))
+ {
+ steamExecutable = Path.Combine(steamPath, "steam.sh");
}
-
- steamExecutable = Path.Combine(steamPath, "steam.sh");
}
return File.Exists(steamExecutable) ? Path.GetFullPath(steamExecutable) : null;
@@ -318,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/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/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.
}
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..8ba23e606f 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,17 @@ 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.
+ /// Adds an interface -> service mapping that for handling administrative actions.
///
- 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());
+ ///
+ /// 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)]
private static partial IServiceCollection AddGameResources(this IServiceCollection services);
@@ -228,19 +50,189 @@ 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);
+
+ 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.
+ ///
+ 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);
+ });
+ }
+ }
+
+ 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(ServerStartOptions startOptions)
+ {
+ services.AddOptionsWithValidateOnStart()
+ .BindConfiguration("")
+ .Configure(options =>
+ {
+ if (string.IsNullOrWhiteSpace(options.GamePath))
+ {
+ options.GamePath = startOptions.GamePath;
+ }
+ if (string.IsNullOrWhiteSpace(options.NitroxAssetsPath))
+ {
+ options.NitroxAssetsPath = startOptions.NitroxAssetsPath;
+ }
+ if (string.IsNullOrWhiteSpace(options.NitroxAppDataPath))
+ {
+ options.NitroxAppDataPath = startOptions.NitroxAppDataPath;
+ }
+ });
+ 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..219d7bae2e
--- /dev/null
+++ b/Nitrox.Server.Subnautica/Models/Administration/Core/IAdminFeature.cs
@@ -0,0 +1,6 @@
+namespace Nitrox.Server.Subnautica.Models.Administration.Core;
+
+///
+/// Implementors handle an administrative action.
+///
+internal interface 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..de7c08b9ca
--- /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 NewSessionTotal);
+
+ 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