first commit
This commit is contained in:
427
Nitrox.Launcher/ViewModels/ManageServerViewModel.cs
Normal file
427
Nitrox.Launcher/ViewModels/ManageServerViewModel.cs
Normal file
@@ -0,0 +1,427 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using HanumanInstitute.MvvmDialogs;
|
||||
using Nitrox.Launcher.Models;
|
||||
using Nitrox.Launcher.Models.Design;
|
||||
using Nitrox.Launcher.Models.Services;
|
||||
using Nitrox.Launcher.Models.Utils;
|
||||
using Nitrox.Launcher.Models.Validators;
|
||||
using Nitrox.Launcher.ViewModels.Abstract;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.Logger;
|
||||
using NitroxModel.Server;
|
||||
using Config = NitroxModel.Serialization.SubnauticaServerConfig;
|
||||
|
||||
namespace Nitrox.Launcher.ViewModels;
|
||||
|
||||
public partial class ManageServerViewModel : RoutableViewModelBase
|
||||
{
|
||||
private readonly string[] advancedSettingsDeniedFields =
|
||||
[
|
||||
"password", "filename", nameof(Config.ServerPort), nameof(Config.MaxConnections), nameof(Config.AutoPortForward), nameof(Config.SaveInterval), nameof(Config.Seed), nameof(Config.GameMode), nameof(Config.DisableConsole),
|
||||
nameof(Config.LANDiscoveryEnabled), nameof(Config.DefaultPlayerPerm), nameof(Config.IsEmbedded)
|
||||
];
|
||||
|
||||
private readonly IDialogService dialogService;
|
||||
private readonly IKeyValueStore keyValueStore;
|
||||
private readonly ServerService serverService;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private bool serverAllowCommands;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private bool serverAllowLanDiscovery;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private bool serverAutoPortForward;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
[NotifyDataErrorInfo]
|
||||
[Range(10, 86400, ErrorMessage = "Value must be between 10s and 24 hours (86400s).")]
|
||||
private int serverAutoSaveInterval;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private Perms serverDefaultPlayerPerm;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private NitroxGameMode serverGameMode;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private Bitmap serverIcon;
|
||||
|
||||
private string serverIconDir;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
[Range(1, 1000)]
|
||||
[NotifyDataErrorInfo]
|
||||
private int serverMaxPlayers;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
[NotifyDataErrorInfo]
|
||||
[Required]
|
||||
[FileName]
|
||||
[NotEndsWith(".")]
|
||||
[NitroxUniqueSaveName(nameof(SavesFolderDir), true, nameof(OriginalServerName))]
|
||||
private string serverName;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private string serverPassword;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private int serverPlayers;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
[NotifyDataErrorInfo]
|
||||
[Range(ushort.MinValue, ushort.MaxValue)]
|
||||
private int serverPort;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
[NotifyDataErrorInfo]
|
||||
[NitroxWorldSeed]
|
||||
private string serverSeed;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SaveCommand), nameof(UndoCommand), nameof(BackCommand), nameof(StartServerCommand))]
|
||||
private bool serverEmbedded = true;
|
||||
|
||||
public static Array PlayerPerms => Enum.GetValues(typeof(Perms));
|
||||
public string OriginalServerName => Server?.Name;
|
||||
|
||||
[ObservableProperty]
|
||||
private ServerEntry server;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(RestoreBackupCommand), nameof(DeleteServerCommand))]
|
||||
private bool serverIsOnline;
|
||||
|
||||
private string SaveFolderDirectory => Path.Combine(SavesFolderDir, Server.Name);
|
||||
private string SavesFolderDir => keyValueStore.GetSavesFolderDir();
|
||||
|
||||
public ManageServerViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public ManageServerViewModel(IDialogService dialogService, IKeyValueStore keyValueStore, ServerService serverService)
|
||||
{
|
||||
this.dialogService = dialogService;
|
||||
this.keyValueStore = keyValueStore;
|
||||
this.serverService = serverService;
|
||||
|
||||
this.RegisterMessageListener<ServerStatusMessage, ManageServerViewModel>((status, vm) =>
|
||||
{
|
||||
if (vm.server != status.Server)
|
||||
{
|
||||
return;
|
||||
}
|
||||
vm.ServerIsOnline = status.IsOnline;
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanGoBackAndStartServer))]
|
||||
public async Task StartServerAsync()
|
||||
{
|
||||
await serverService.StartServerAsync(Server);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task<bool> StopServerAsync()
|
||||
{
|
||||
if (!await Server.StopAsync())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void LoadFrom(ServerEntry serverEntry)
|
||||
{
|
||||
Server = serverEntry;
|
||||
|
||||
ServerName = Server.Name;
|
||||
ServerIcon = Server.ServerIcon;
|
||||
ServerPassword = Server.Password;
|
||||
ServerGameMode = Server.GameMode;
|
||||
ServerSeed = Server.Seed;
|
||||
ServerDefaultPlayerPerm = Server.PlayerPermissions;
|
||||
ServerAutoSaveInterval = Server.AutoSaveInterval;
|
||||
ServerMaxPlayers = Server.MaxPlayers;
|
||||
ServerPlayers = Server.Players;
|
||||
ServerPort = Server.Port;
|
||||
ServerAutoPortForward = Server.AutoPortForward;
|
||||
ServerAllowLanDiscovery = Server.AllowLanDiscovery;
|
||||
ServerAllowCommands = Server.AllowCommands;
|
||||
ServerEmbedded = Server.IsEmbedded;
|
||||
|
||||
// Force embedded on MacOS
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Server.IsEmbedded = ServerEmbedded = true;
|
||||
|
||||
Config config = Config.Load(SaveFolderDirectory);
|
||||
using (config.Update(SaveFolderDirectory))
|
||||
{
|
||||
config.IsEmbedded = Server.IsEmbedded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasChanges() => ServerName != Server.Name ||
|
||||
ServerIcon != Server.ServerIcon ||
|
||||
ServerPassword != Server.Password ||
|
||||
ServerGameMode != Server.GameMode ||
|
||||
ServerSeed != Server.Seed ||
|
||||
ServerDefaultPlayerPerm != Server.PlayerPermissions ||
|
||||
ServerAutoSaveInterval != Server.AutoSaveInterval ||
|
||||
ServerMaxPlayers != Server.MaxPlayers ||
|
||||
ServerPlayers != Server.Players ||
|
||||
ServerPort != Server.Port ||
|
||||
ServerAutoPortForward != Server.AutoPortForward ||
|
||||
ServerAllowLanDiscovery != Server.AllowLanDiscovery ||
|
||||
ServerAllowCommands != Server.AllowCommands ||
|
||||
ServerEmbedded != Server.IsEmbedded;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanGoBackAndStartServer))]
|
||||
private async Task BackAsync() => await HostScreen.BackToAsync<ServersViewModel>();
|
||||
|
||||
private bool CanGoBackAndStartServer() => !HasChanges();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSave))]
|
||||
private void Save()
|
||||
{
|
||||
// If world name was changed, rename save folder to match it
|
||||
string newPath = Path.Combine(SavesFolderDir, ServerName);
|
||||
if (SaveFolderDirectory != newPath)
|
||||
{
|
||||
// Windows, by default, ignores case when renaming folders. We circumvent this by changing the name to a random one, and then to the desired name.
|
||||
// OS tmp directory is not used because on Linux this causes cross-link error, see https://github.com/dotnet/runtime/issues/31149
|
||||
string tempSavePath = Path.Combine(SavesFolderDir, $"{Guid.NewGuid():N}_{ServerName[..Math.Min(ServerName.Length, 10)]}");
|
||||
Directory.Move(SaveFolderDirectory, tempSavePath);
|
||||
Directory.Move(tempSavePath, newPath);
|
||||
}
|
||||
|
||||
// Update the servericon.png file if needed
|
||||
if (Server.ServerIcon != ServerIcon && serverIconDir != null)
|
||||
{
|
||||
File.Copy(serverIconDir, Path.Combine(newPath, "servericon.png"), true);
|
||||
}
|
||||
|
||||
Server.Name = ServerName;
|
||||
Server.ServerIcon = ServerIcon;
|
||||
Server.Password = ServerPassword;
|
||||
Server.GameMode = ServerGameMode;
|
||||
Server.Seed = ServerSeed;
|
||||
Server.PlayerPermissions = ServerDefaultPlayerPerm;
|
||||
Server.AutoSaveInterval = ServerAutoSaveInterval;
|
||||
Server.MaxPlayers = ServerMaxPlayers;
|
||||
Server.Players = ServerPlayers;
|
||||
Server.Port = ServerPort;
|
||||
Server.AutoPortForward = ServerAutoPortForward;
|
||||
Server.AllowLanDiscovery = ServerAllowLanDiscovery;
|
||||
Server.AllowCommands = ServerAllowCommands;
|
||||
Server.IsEmbedded = ServerEmbedded || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); // Force embedded on MacOS;
|
||||
|
||||
Config config = Config.Load(SaveFolderDirectory);
|
||||
using (config.Update(SaveFolderDirectory))
|
||||
{
|
||||
config.ServerPassword = Server.Password;
|
||||
if (Server.IsNewServer) { config.Seed = Server.Seed; }
|
||||
config.GameMode = Server.GameMode;
|
||||
config.DefaultPlayerPerm = Server.PlayerPermissions;
|
||||
config.SaveInterval = (int)TimeSpan.FromSeconds(Server.AutoSaveInterval).TotalMilliseconds;
|
||||
config.MaxConnections = Server.MaxPlayers;
|
||||
config.ServerPort = Server.Port;
|
||||
config.AutoPortForward = Server.AutoPortForward;
|
||||
config.LANDiscoveryEnabled = Server.AllowLanDiscovery;
|
||||
config.DisableConsole = !Server.AllowCommands;
|
||||
config.IsEmbedded = Server.IsEmbedded;
|
||||
}
|
||||
|
||||
Undo(); // Used to update the UI with corrected values (Trims and ToUppers)
|
||||
|
||||
BackCommand.NotifyCanExecuteChanged();
|
||||
StartServerCommand.NotifyCanExecuteChanged();
|
||||
UndoCommand.NotifyCanExecuteChanged();
|
||||
SaveCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private bool CanSave() => !HasErrors && !ServerIsOnline && HasChanges();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanUndo))]
|
||||
private void Undo()
|
||||
{
|
||||
ServerName = Server.Name;
|
||||
ServerIcon = Server.ServerIcon;
|
||||
ServerPassword = Server.Password;
|
||||
ServerGameMode = Server.GameMode;
|
||||
ServerSeed = Server.Seed;
|
||||
ServerDefaultPlayerPerm = Server.PlayerPermissions;
|
||||
ServerAutoSaveInterval = Server.AutoSaveInterval;
|
||||
ServerMaxPlayers = Server.MaxPlayers;
|
||||
ServerPlayers = Server.Players;
|
||||
ServerPort = Server.Port;
|
||||
ServerAutoPortForward = Server.AutoPortForward;
|
||||
ServerAllowLanDiscovery = Server.AllowLanDiscovery;
|
||||
ServerAllowCommands = Server.AllowCommands;
|
||||
ServerEmbedded = Server.IsEmbedded;
|
||||
}
|
||||
|
||||
private bool CanUndo() => !ServerIsOnline && HasChanges();
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ChangeServerIconAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IReadOnlyList<IStorageFile> files = await MainWindow.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Select an image",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("All Images + Icons")
|
||||
{
|
||||
Patterns = ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.ico"],
|
||||
AppleUniformTypeIdentifiers = ["public.image"],
|
||||
MimeTypes = ["image/*"]
|
||||
}
|
||||
]
|
||||
});
|
||||
string newIconFile = files.FirstOrDefault()?.TryGetLocalPath();
|
||||
if (newIconFile == null || !File.Exists(newIconFile))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
serverIconDir = newIconFile;
|
||||
ServerIcon = new Bitmap(serverIconDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ShowAdvancedSettings()
|
||||
{
|
||||
ObjectPropertyEditorViewModel result = await dialogService.ShowAsync<ObjectPropertyEditorViewModel>(model =>
|
||||
{
|
||||
model.Title = $"Server '{ServerName}' config editor";
|
||||
model.FieldAcceptFilter = p => !advancedSettingsDeniedFields.Any(v => p.Name.Contains(v, StringComparison.OrdinalIgnoreCase));
|
||||
model.OwnerObject = Config.Load(SaveFolderDirectory);
|
||||
});
|
||||
if (result && result.OwnerObject is Config config)
|
||||
{
|
||||
config.Serialize(SaveFolderDirectory);
|
||||
}
|
||||
LoadFrom(Server);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenWorldFolder() =>
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = SaveFolderDirectory,
|
||||
Verb = "open",
|
||||
UseShellExecute = true
|
||||
})?.Dispose();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRestoreBackupAndDeleteServer))]
|
||||
private async Task RestoreBackup()
|
||||
{
|
||||
BackupRestoreViewModel result = await dialogService.ShowAsync<BackupRestoreViewModel>(model =>
|
||||
{
|
||||
model.Title = $"Restore a Backup for '{ServerName}'";
|
||||
model.SaveFolderDirectory = SaveFolderDirectory;
|
||||
});
|
||||
|
||||
if (result)
|
||||
{
|
||||
string backupFile = result.SelectedBackup.BackupFileName;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(backupFile))
|
||||
{
|
||||
throw new FileNotFoundException("Selected backup file not found.", backupFile);
|
||||
}
|
||||
|
||||
ZipFile.ExtractToDirectory(backupFile, SaveFolderDirectory, true);
|
||||
Server.RefreshFromDirectory(SaveFolderDirectory);
|
||||
LoadFrom(Server);
|
||||
LauncherNotifier.Success("Backup restored successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await dialogService.ShowErrorAsync(ex, "Error while restoring backup");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRestoreBackupAndDeleteServer))]
|
||||
private async Task DeleteServerAsync()
|
||||
{
|
||||
await CoreDeleteServerAsync();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRestoreBackupAndDeleteServer))]
|
||||
private async Task ForceDeleteServerAsync()
|
||||
{
|
||||
await CoreDeleteServerAsync(true);
|
||||
}
|
||||
|
||||
private async Task CoreDeleteServerAsync(bool force = false)
|
||||
{
|
||||
if (!force)
|
||||
{
|
||||
DialogBoxViewModel modal = await dialogService.ShowAsync<DialogBoxViewModel>(model =>
|
||||
{
|
||||
model.Title = $"Are you sure you want to delete the server '{ServerName}'?";
|
||||
model.ButtonOptions = ButtonOptions.YesNo;
|
||||
});
|
||||
if (!modal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(SaveFolderDirectory, true);
|
||||
WeakReferenceMessenger.Default.Send(new SaveDeletedMessage(ServerName));
|
||||
await HostScreen.BackAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await dialogService.ShowErrorAsync(ex, $"Error while deleting world \"{ServerName}\"");
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanRestoreBackupAndDeleteServer() => !ServerIsOnline;
|
||||
}
|
Reference in New Issue
Block a user