first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Concurrent;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Reactive;
using CommunityToolkit.Mvvm.Input;
namespace Nitrox.Launcher.Models.Design;
/// <summary>
/// Listens for async command changes on buttons to add the chosen classname to, for use with styling.
/// </summary>
public class AsyncCommandButtonTagger : IDisposable
{
public string ClassName { get; init; }
private readonly ConcurrentDictionary<ICommand, BusyState> states = [];
private readonly IDisposable commandChangeSubscription;
public AsyncCommandButtonTagger(string className)
{
ClassName = className;
commandChangeSubscription = Button.CommandProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<ICommand>>(ButtonCommandChangedOnNext));
void ButtonCommandChangedOnNext(AvaloniaPropertyChangedEventArgs<ICommand> args)
{
if (args.Sender is not Button button)
{
return;
}
if (args.OldValue.Value is { } oldCommand && states.TryRemove(oldCommand, out BusyState oldState))
{
oldState.Dispose();
}
if (args.NewValue.Value is { } newCommand)
{
states.TryAdd(newCommand, new BusyState(ClassName, newCommand, button));
}
}
}
private class BusyState : IDisposable
{
public string ClassName { get; }
private ICommand Command { get; }
private Button Button { get; }
public BusyState(string className, ICommand command, Button button)
{
ClassName = className;
Command = command;
Button = button;
Command.CanExecuteChanged += CommandOnCanExecuteChanged;
}
public void Dispose()
{
Command.CanExecuteChanged -= CommandOnCanExecuteChanged;
Button.Classes.Set(ClassName, false);
}
private void CommandOnCanExecuteChanged(object sender, EventArgs e)
{
if (sender is IAsyncRelayCommand asyncCommand)
{
Button.Classes.Set(ClassName, asyncCommand.IsRunning);
}
}
}
public void Dispose()
{
commandChangeSubscription.Dispose();
}
/// <summary>
/// Removes the busy states of buttons.
/// </summary>
public void Clear()
{
foreach ((ICommand _, BusyState value) in states)
{
value.Dispose();
}
states.Clear();
}
}

View File

@@ -0,0 +1,5 @@
using System;
namespace Nitrox.Launcher.Models.Design;
public record BackupItem(DateTime BackupDate, string BackupFileName);

View File

@@ -0,0 +1,34 @@
using System.Reflection;
using Avalonia.Collections;
using NitroxModel.Serialization;
namespace Nitrox.Launcher.Models.Design;
public record EditorField
{
public object Value { get; set; }
public PropertyInfo PropertyInfo { get; init; }
public AvaloniaList<object> PossibleValues { get; set; }
public string Description
{
get
{
string description = PropertyInfo.GetCustomAttribute<PropertyDescriptionAttribute>()?.Description;
if (string.IsNullOrWhiteSpace(description))
{
description = null;
}
return description;
}
}
public EditorField(PropertyInfo propertyInfo, object value, AvaloniaList<object> possibleValues)
{
PropertyInfo = propertyInfo;
Value = value;
PossibleValues = possibleValues;
}
}

View File

@@ -0,0 +1,6 @@
namespace Nitrox.Launcher.Models.Design;
public interface IRoutingScreen
{
object ActiveViewModel { get; set; }
}

View File

@@ -0,0 +1,9 @@
using NitroxModel.Discovery.Models;
namespace Nitrox.Launcher.Models.Design;
public class KnownGame
{
public string PathToGame { get; init; }
public Platform Platform { get; init; }
}

View File

@@ -0,0 +1,59 @@
extern alias JB;
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Metadata;
using JB::JetBrains.Annotations;
namespace Nitrox.Launcher.Models.Design;
/// <summary>
/// Selects a <see cref="DataTemplate" /> based on its <see cref="DataTemplate.DataType" />.
/// </summary>
public class MultiDataTemplate : AvaloniaList<DataTemplate>, IRecyclingDataTemplate
{
[Content]
[UsedImplicitly]
public List<DataTemplate> Content { get; set; } = new();
private readonly Dictionary<Type, Control> typeToControlCache = [];
public bool Match(object data) => GetTemplateForType(data?.GetType()) != null;
public Control Build(object data, Control existing)
{
Type type = data?.GetType();
if (type != null && typeToControlCache.TryGetValue(type, out Control control))
{
return control;
}
Control build = GetTemplateForType(type)?.Build(data);
if (type != null && build != null)
{
typeToControlCache[type] = build;
}
return build ?? existing;
}
public Control Build(object data) => GetTemplateForType(data.GetType())?.Build(data) ?? new TextBlock { Text = "" };
private IDataTemplate GetTemplateForType(Type type)
{
if (type == null)
{
return null;
}
foreach (DataTemplate template in Content)
{
if (template.DataType?.IsAssignableTo(type) ?? false)
{
return template;
}
}
return null;
}
}

View File

@@ -0,0 +1,167 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Reactive;
namespace Nitrox.Launcher.Models.Design;
/// <summary>
/// Container class for our attached properties.
/// </summary>
public class NitroxAttached : AvaloniaObject
{
public static readonly AttachedProperty<bool> SelectedProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, Interactive, bool>("Selected");
public static readonly AttachedProperty<bool> AutoScrollToHomeProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, ScrollViewer, bool>("AutoScrollToHome");
public static readonly AttachedProperty<Orientation> PrimaryScrollWheelDirectionProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, ScrollViewer, Orientation>("PrimaryScrollWheelDirection", Orientation.Vertical);
public static readonly AttachedProperty<bool> IsNumericInputProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, InputElement, bool>("IsNumericInput");
public static readonly AttachedProperty<bool> HasUserInteractedProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, InputElement, bool>("HasUserInteracted");
public static readonly AttachedProperty<bool> UseCustomTitleBarProperty = AvaloniaProperty.RegisterAttached<NitroxAttached, Window, bool>("UseCustomTitleBar", true);
internal static readonly AsyncCommandButtonTagger AsyncCommandButtonTagger;
static NitroxAttached()
{
InputElement.LostFocusEvent.Raised.Subscribe(new AnonymousObserver<(object, RoutedEventArgs)>(HasUserInteractedOnNext));
InputElement.TextInputEvent.Raised.Subscribe(new AnonymousObserver<(object, RoutedEventArgs)>(HasUserInteractedOnNext));
AsyncCommandButtonTagger = new AsyncCommandButtonTagger("busy");
void HasUserInteractedOnNext((object Sender, RoutedEventArgs EventArgs) args)
{
if (args.Sender is InputElement element)
{
SetHasUserInteracted(element, true);
}
}
}
public static bool GetSelected(AvaloniaObject element) => element.GetValue(SelectedProperty);
public static void SetSelected(AvaloniaObject obj, bool value) => obj.SetValue(SelectedProperty, value);
public static void SetAutoScrollToHome(AvaloniaObject obj, bool value)
{
static void VisualAttached(object sender, VisualTreeAttachmentEventArgs e) => (sender as ScrollViewer)?.ScrollToHome();
obj.SetValue(AutoScrollToHomeProperty, value);
if (obj is not Visual visual)
{
return;
}
if (value)
{
visual.AttachedToVisualTree += VisualAttached;
}
else
{
visual.AttachedToVisualTree -= VisualAttached;
}
}
public static bool GetAutoScrollToHome(AvaloniaObject element) => element.GetValue(AutoScrollToHomeProperty);
public static Orientation GetPrimaryScrollWheelDirection(AvaloniaObject obj) => obj.GetValue(PrimaryScrollWheelDirectionProperty);
/// <summary>
/// Changes scroll wheel input to move scroll viewer left and right if set to <see cref="Orientation.Horizontal"/>.
/// </summary>
public static void SetPrimaryScrollWheelDirection(AvaloniaObject obj, Orientation orientation)
{
static void RotatedOrientationWheelHandler(object sender, PointerWheelEventArgs e)
{
ScrollViewer scrollViewer = sender as ScrollViewer;
if (scrollViewer == null)
{
return;
}
if (GetPrimaryScrollWheelDirection(scrollViewer) == Orientation.Vertical)
{
return;
}
if (e.Delta.Y < 0)
{
for (int i = 0; i <= -e.Delta.Y; i++)
{
scrollViewer.LineRight();
}
}
else
{
for (int i = 0; i <= e.Delta.Y; i++)
{
scrollViewer.LineLeft();
}
}
e.Handled = true;
}
obj.SetValue(PrimaryScrollWheelDirectionProperty, orientation);
if (obj is not ScrollViewer scrollViewer)
{
return;
}
switch (orientation)
{
case Orientation.Horizontal:
scrollViewer.PointerWheelChanged += RotatedOrientationWheelHandler;
break;
case Orientation.Vertical:
scrollViewer.PointerWheelChanged -= RotatedOrientationWheelHandler;
break;
}
}
public static void SetIsNumericInput(AvaloniaObject obj, bool value)
{
static void OnKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
if (sender is not TextBox textBox)
{
throw new NotSupportedException($"{sender.GetType()} is not supported by property {nameof(IsNumericInputProperty)}");
}
string previousText = textBox.Text;
if (int.TryParse(textBox.Text, out int val))
{
val += e.Key == Key.Up ? 1 : -1;
}
textBox.Text = Math.Clamp(val, 0, int.MaxValue).ToString();
if (textBox.Text.Length > textBox.MaxLength)
{
textBox.Text = previousText;
}
break;
}
}
if (obj is not InputElement inputElement)
{
return;
}
if (value)
{
inputElement.KeyDown += OnKeyDown;
}
else
{
inputElement.KeyDown -= OnKeyDown;
}
}
public static bool GetHasUserInteracted(InputElement input) => input.GetValue(HasUserInteractedProperty);
public static void SetHasUserInteracted(InputElement input, bool value) => input.SetValue(HasUserInteractedProperty, value);
public static bool GetUseCustomTitleBar(Window window) => window.GetValue(UseCustomTitleBarProperty);
public static void SetUseCustomTitleBar(Window window, bool value) => window.SetValue(UseCustomTitleBarProperty, value);
}

View File

@@ -0,0 +1,12 @@
using System;
using Avalonia.Media.Imaging;
namespace Nitrox.Launcher.Models.Design;
public sealed record NitroxBlog(string Title, DateOnly Date, string Url, Bitmap Image)
{
public NitroxBlog() : this("", default, "", null)
{
}
}

View File

@@ -0,0 +1,6 @@
using System;
namespace Nitrox.Launcher.Models.Design;
[Serializable]
public record NitroxChangelog(string Version, DateTime Released, string PatchNotes);

View File

@@ -0,0 +1,28 @@
using System.Windows.Input;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
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 NotificationItem()
{
}
public NotificationItem(string message, NotificationType type = NotificationType.Information, ICommand closeCommand = null)
{
Message = message;
Type = type;
CloseCommand = closeCommand ?? new RelayCommand(() => WeakReferenceMessenger.Default.Send(new NotificationCloseMessage(this)));
}
}

View File

@@ -0,0 +1,8 @@
namespace Nitrox.Launcher.Models.Design;
public record OutputLine
{
public string Timestamp { get; init; }
public string LogText { get; init; }
public OutputLineType Type { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Nitrox.Launcher.Models.Design;
public enum OutputLineType
{
INFO_LOG,
DEBUG_LOG,
WARNING_LOG,
ERROR_LOG,
COMMAND
}

View File

@@ -0,0 +1,20 @@
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
namespace Nitrox.Launcher.Models.Design;
public partial class RoutingScreen : ObservableObject, IRoutingScreen
{
[ObservableProperty]
private object activeViewModel;
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ActiveViewModel))
{
WeakReferenceMessenger.Default.Send(new ViewShownMessage(ActiveViewModel));
}
base.OnPropertyChanged(e);
}
}

View File

@@ -0,0 +1,413 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Nitrox.Launcher.Models.Exceptions;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Helper;
using NitroxModel.Logger;
using NitroxModel.Serialization;
using NitroxModel.Server;
using NitroxServer.Serialization;
using NitroxServer.Serialization.World;
namespace Nitrox.Launcher.Models.Design;
/// <summary>
/// Manager object for a server. Used to start/stop a server and change its settings.
/// </summary>
public partial class ServerEntry : ObservableObject
{
public const string DEFAULT_SERVER_ICON_NAME = "servericon.png";
public const string DEFAULT_SERVER_CONFIG_NAME = "server.cfg";
private static readonly SubnauticaServerConfig serverDefaults = new();
[ObservableProperty]
private bool allowCommands = !serverDefaults.DisableConsole;
[ObservableProperty]
private bool allowLanDiscovery = serverDefaults.LANDiscoveryEnabled;
[ObservableProperty]
private bool autoPortForward = serverDefaults.AutoPortForward;
[ObservableProperty]
private int autoSaveInterval = serverDefaults.SaveInterval / 1000;
[ObservableProperty]
private NitroxGameMode gameMode = serverDefaults.GameMode;
[ObservableProperty]
private bool isEmbedded;
[ObservableProperty]
private bool isNewServer = true;
[ObservableProperty]
private bool isOnline;
[ObservableProperty]
private DateTime lastAccessedTime = DateTime.Now;
[ObservableProperty]
private int maxPlayers = serverDefaults.MaxConnections;
[ObservableProperty]
private string name;
[ObservableProperty]
private string password;
[ObservableProperty]
private Perms playerPermissions = serverDefaults.DefaultPlayerPerm;
[ObservableProperty]
private int players;
[ObservableProperty]
private int port = serverDefaults.ServerPort;
[ObservableProperty]
private string seed;
[ObservableProperty]
private Bitmap serverIcon;
[ObservableProperty]
private Version version = NitroxEnvironment.Version;
internal ServerProcess Process { get; private set; }
public static ServerEntry FromDirectory(string saveDir)
{
ServerEntry result = new();
return result.RefreshFromDirectory(saveDir) ? result : null;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IsOnline):
WeakReferenceMessenger.Default.Send(new ServerStatusMessage(this, IsOnline));
break;
}
base.OnPropertyChanged(e);
}
public static ServerEntry CreateNew(string saveDir, NitroxGameMode saveGameMode)
{
ArgumentException.ThrowIfNullOrWhiteSpace(saveDir, nameof(saveDir));
Directory.CreateDirectory(saveDir);
SubnauticaServerConfig config = SubnauticaServerConfig.Load(saveDir);
string fileEnding = config.SerializerMode switch
{
ServerSerializerMode.JSON => ServerJsonSerializer.FILE_ENDING,
ServerSerializerMode.PROTOBUF => ServerProtoBufSerializer.FILE_ENDING,
_ => throw new NotImplementedException()
};
File.WriteAllText(Path.Combine(saveDir, $"Version{fileEnding}"), null);
using (config.Update(saveDir))
{
config.GameMode = saveGameMode;
}
return FromDirectory(saveDir);
}
public bool RefreshFromDirectory(string saveDir)
{
if (!File.Exists(Path.Combine(saveDir, DEFAULT_SERVER_CONFIG_NAME)))
{
Log.Warn($"Tried loading invalid save directory at '{saveDir}'");
return false;
}
Bitmap serverIcon = null;
string serverIconPath = Path.Combine(saveDir, DEFAULT_SERVER_ICON_NAME);
if (File.Exists(serverIconPath))
{
serverIcon = new Bitmap(Path.Combine(saveDir, DEFAULT_SERVER_ICON_NAME));
}
SubnauticaServerConfig config = SubnauticaServerConfig.Load(saveDir);
string fileEnding = config.SerializerMode switch
{
ServerSerializerMode.JSON => ServerJsonSerializer.FILE_ENDING,
ServerSerializerMode.PROTOBUF => ServerProtoBufSerializer.FILE_ENDING,
_ => throw new NotImplementedException()
};
string saveFileVersion = Path.Combine(saveDir, $"Version{fileEnding}");
if (!File.Exists(saveFileVersion))
{
Log.Warn($"Tried loading invalid save directory at '{saveDir}', Version file is missing");
return false;
}
Version version;
using (FileStream stream = new(saveFileVersion, FileMode.Open, FileAccess.Read, FileShare.Read))
{
version = config.SerializerMode switch
{
ServerSerializerMode.JSON => new ServerJsonSerializer().Deserialize<SaveFileVersion>(stream)?.Version ?? NitroxEnvironment.Version,
ServerSerializerMode.PROTOBUF => new ServerProtoBufSerializer().Deserialize<SaveFileVersion>(stream)?.Version ?? NitroxEnvironment.Version,
_ => throw new NotImplementedException()
};
}
Name = Path.GetFileName(saveDir);
ServerIcon = serverIcon;
Password = config.ServerPassword;
Seed = config.Seed;
GameMode = config.GameMode;
PlayerPermissions = config.DefaultPlayerPerm;
AutoSaveInterval = config.SaveInterval / 1000;
MaxPlayers = config.MaxConnections;
Port = config.ServerPort;
AutoPortForward = config.AutoPortForward;
AllowLanDiscovery = config.LANDiscoveryEnabled;
AllowCommands = !config.DisableConsole;
IsNewServer = !File.Exists(Path.Combine(saveDir, $"PlayerData{fileEnding}"));
Version = version;
IsEmbedded = config.IsEmbedded || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); // Force embedded on MacOS
LastAccessedTime = File.GetLastWriteTime(File.Exists(Path.Combine(saveDir, $"PlayerData{fileEnding}"))
?
// This file is affected by server saving
Path.Combine(saveDir, $"PlayerData{fileEnding}")
:
// If the above file doesn't exist (server was never ran), use the Version file instead
Path.Combine(saveDir, $"Version{fileEnding}"));
return true;
}
public void Start(string savesDir)
{
if (!Directory.Exists(savesDir))
{
throw new DirectoryNotFoundException($"Directory '{savesDir}' not found");
}
if (Process?.IsRunning ?? false)
{
throw new DuplicateSingularApplicationException("Nitrox Server");
}
// Start server and add notify when server closed.
Process = ServerProcess.Start(Path.Combine(savesDir, Name), () => Dispatcher.UIThread.InvokeAsync(StopAsync), IsEmbedded);
IsNewServer = false;
IsOnline = true;
}
[RelayCommand]
public async Task<bool> StopAsync()
{
if (Process is not { IsRunning: true })
{
IsOnline = false;
return true;
}
if (await Process.CloseAsync())
{
IsOnline = false;
return true;
}
return false;
}
[RelayCommand]
public void OpenSaveFolder()
{
System.Diagnostics.Process.Start(new ProcessStartInfo
{
FileName = Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), Name),
Verb = "open",
UseShellExecute = true
})?.Dispose();
}
internal partial class ServerProcess : IDisposable
{
private NamedPipeClientStream commandStream;
private OutputLineType lastOutputType;
private Process serverProcess;
[GeneratedRegex(@"^\[(?<timestamp>\d{2}:\d{2}:\d{2}\.\d{3})\]\s\[(?<level>\w+)\](?<logText>.*)?$")]
private static partial Regex OutputLineRegex { get; }
public bool IsRunning => !serverProcess?.HasExited ?? false;
public AvaloniaList<OutputLine> Output { get; } = [];
private ServerProcess(string saveDir, Action onExited, bool isEmbeddedMode = false)
{
string serverExeName = "NitroxServer-Subnautica.exe";
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
serverExeName = "NitroxServer-Subnautica";
}
string serverFile = Path.Combine(NitroxUser.ExecutableRootPath, serverExeName);
ProcessStartInfo startInfo = new(serverFile)
{
WorkingDirectory = NitroxUser.ExecutableRootPath,
ArgumentList =
{
"--save",
Path.GetFileName(saveDir)
},
RedirectStandardOutput = isEmbeddedMode,
RedirectStandardError = isEmbeddedMode,
RedirectStandardInput = isEmbeddedMode,
WindowStyle = isEmbeddedMode ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal,
CreateNoWindow = isEmbeddedMode
};
if (isEmbeddedMode)
{
startInfo.ArgumentList.Add("--embedded");
}
Log.Info($"Starting server:{Environment.NewLine}File: {startInfo.FileName}{Environment.NewLine}Working directory: {startInfo.WorkingDirectory}{Environment.NewLine}Arguments: {string.Join(", ", startInfo.ArgumentList)}");
serverProcess = System.Diagnostics.Process.Start(startInfo);
if (serverProcess != null)
{
serverProcess.EnableRaisingEvents = true; // Required for 'Exited' event from process.
if (isEmbeddedMode)
{
serverProcess.OutputDataReceived += (_, args) =>
{
if (args.Data == null)
{
return;
}
Match match = OutputLineRegex.Match(args.Data);
if (match.Success)
{
OutputLine outputLine = new()
{
Timestamp = $"[{match.Groups["timestamp"].ValueSpan}]",
LogText = match.Groups["logText"].ValueSpan.Trim().ToString(),
Type = match.Groups["level"].ValueSpan switch
{
"DBG" => OutputLineType.DEBUG_LOG,
"WRN" => OutputLineType.WARNING_LOG,
"ERR" => OutputLineType.ERROR_LOG,
_ => OutputLineType.INFO_LOG
}
};
lastOutputType = outputLine.Type;
Output.Add(outputLine);
}
else
{
Output.Add(new OutputLine
{
Timestamp = "",
LogText = args.Data,
Type = lastOutputType
});
}
};
serverProcess.BeginOutputReadLine();
}
serverProcess.Exited += (_, _) =>
{
onExited?.Invoke();
};
}
}
public static ServerProcess Start(string saveDir, Action onExited, bool isEmbedded) => new(saveDir, onExited, isEmbedded);
/// <summary>
/// Tries to close the server gracefully with a timeout of 30 seconds. If it fails, returns false.
/// </summary>
public async Task<bool> CloseAsync()
{
using CancellationTokenSource ctsCloseTimeout = new(TimeSpan.FromSeconds(30));
try
{
do
{
if (!await SendCommandAsync("stop"))
{
await Task.Delay(100, ctsCloseTimeout.Token);
}
} while (IsRunning && !ctsCloseTimeout.IsCancellationRequested);
}
catch (OperationCanceledException)
{
// ignored
}
if (IsRunning)
{
return false;
}
Dispose();
return true;
}
public async Task<bool> SendCommandAsync(string command)
{
if (!IsRunning || string.IsNullOrWhiteSpace(command))
{
return false;
}
try
{
commandStream ??= new NamedPipeClientStream(".", $"Nitrox Server {serverProcess.Id}", PipeDirection.Out, PipeOptions.Asynchronous);
if (!commandStream.IsConnected)
{
await commandStream.ConnectAsync(5000);
}
byte[] commandBytes = Encoding.UTF8.GetBytes(command);
await commandStream.WriteAsync(BitConverter.GetBytes((uint)commandBytes.Length));
await commandStream.WriteAsync(commandBytes);
return true;
}
catch (TimeoutException)
{
// ignored
}
catch (IOException)
{
// ignored - "broken pipe" or "socket shutdown"
}
return false;
}
public void Dispose()
{
try
{
commandStream?.Dispose();
}
catch
{
// ignored
}
serverProcess?.Dispose();
serverProcess = null;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Nitrox.Launcher.Models.Design;
public sealed class ServerStartEventArgs : EventArgs
{
public bool IsEmbedded { get; }
public ServerStartEventArgs(bool embedded)
{
IsEmbedded = embedded;
}
public override string ToString()
{
return $"[ServerStartEventArgs - IsEmbedded: {IsEmbedded}]";
}
}