Add Avalonia server-host launcher (MVP scaffold)
Cross-platform C# GUI to replace the Qt/C++ Host GUI, on the same dotnet toolchain as the server. MVP: load/save commonwealth-server.json, Start/Stop the CommonwealthOnline.Server process (published exe, then dll, then source run), and stream its output to a live log. Themed to the Commonwealth Online brand (near-black + amber). Builds clean on net8.0. Next: player list + kick/ban via the admin port, then retire the Qt host.
This commit is contained in:
@@ -58,3 +58,6 @@ server/bans.json
|
|||||||
server/.admin-token
|
server/.admin-token
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
host/bin/
|
||||||
|
host/obj/
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="CommonwealthOnline.Host.App"
|
||||||
|
RequestedThemeVariant="Dark">
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<Color x:Key="CoBackground">#0E120B</Color>
|
||||||
|
<Color x:Key="CoSurface">#171C10</Color>
|
||||||
|
<Color x:Key="CoAccent">#E6D28C</Color>
|
||||||
|
<Color x:Key="CoText">#D8D2BE</Color>
|
||||||
|
<Color x:Key="CoDanger">#C24B4B</Color>
|
||||||
|
<SolidColorBrush x:Key="CoBackgroundBrush" Color="{StaticResource CoBackground}" />
|
||||||
|
<SolidColorBrush x:Key="CoSurfaceBrush" Color="{StaticResource CoSurface}" />
|
||||||
|
<SolidColorBrush x:Key="CoAccentBrush" Color="{StaticResource CoAccent}" />
|
||||||
|
<SolidColorBrush x:Key="CoTextBrush" Color="{StaticResource CoText}" />
|
||||||
|
<SolidColorBrush x:Key="CoDangerBrush" Color="{StaticResource CoDanger}" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using CommonwealthOnline.Host.ViewModels;
|
||||||
|
using CommonwealthOnline.Host.Views;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 407 KiB |
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<AssemblyName>CommonwealthOnline.Host</AssemblyName>
|
||||||
|
<RootNamespace>CommonwealthOnline.Host</RootNamespace>
|
||||||
|
<ApplicationIcon></ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="Assets/**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.1.0" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.1.0" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args) =>
|
||||||
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
|
AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host.Services;
|
||||||
|
|
||||||
|
// Mirrors CommonwealthOnline.Server Configuration. Keep the JSON shape aligned
|
||||||
|
// with the server's own serializer so a config saved here loads there.
|
||||||
|
public sealed class ServerConfig
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "0.0.0.0";
|
||||||
|
public int Port { get; set; } = 7777;
|
||||||
|
public string ServerName { get; set; } = "Commonwealth Online Server";
|
||||||
|
public string ServerDescription { get; set; } = string.Empty;
|
||||||
|
public int MaxPlayers { get; set; } = 16;
|
||||||
|
public string LogVerbosity { get; set; } = "info";
|
||||||
|
public int AdminPort { get; set; } = 7779;
|
||||||
|
public bool EnableGnsTransport { get; set; }
|
||||||
|
public string? GnsBridgePath { get; set; }
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ServerConfig Load(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<ServerConfig>(File.ReadAllText(path), Options)
|
||||||
|
?? new ServerConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Fall back to defaults on unreadable/invalid config.
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(string path) =>
|
||||||
|
File.WriteAllText(path, JsonSerializer.Serialize(this, Options));
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host.Services;
|
||||||
|
|
||||||
|
// Launches the CommonwealthOnline.Server process, preferring a published
|
||||||
|
// apphost, then a framework-dependent DLL, then a source-tree dotnet run.
|
||||||
|
public sealed class ServerController
|
||||||
|
{
|
||||||
|
private Process? _process;
|
||||||
|
|
||||||
|
public bool IsRunning => _process is { HasExited: false };
|
||||||
|
|
||||||
|
public event Action<string>? LogReceived;
|
||||||
|
public event Action<bool>? RunningChanged;
|
||||||
|
|
||||||
|
public void Start(string serverDir, string configPath)
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startInfo = ResolveLaunch(serverDir, configPath);
|
||||||
|
var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
|
||||||
|
process.OutputDataReceived += (_, e) => Emit(e.Data);
|
||||||
|
process.ErrorDataReceived += (_, e) => Emit(e.Data);
|
||||||
|
process.Exited += (_, _) => RunningChanged?.Invoke(false);
|
||||||
|
|
||||||
|
_process = process;
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
RunningChanged?.Invoke(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (_process is { HasExited: false } process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Process already gone or not killable; RunningChanged fires on Exited.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Emit(string? line)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(line))
|
||||||
|
{
|
||||||
|
LogReceived?.Invoke(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProcessStartInfo ResolveLaunch(string serverDir, string configPath)
|
||||||
|
{
|
||||||
|
var exeName = OperatingSystem.IsWindows()
|
||||||
|
? "CommonwealthOnline.Server.exe"
|
||||||
|
: "CommonwealthOnline.Server";
|
||||||
|
var apphost = Path.Combine(serverDir, exeName);
|
||||||
|
var dll = Path.Combine(serverDir, "CommonwealthOnline.Server.dll");
|
||||||
|
var project = Path.Combine(serverDir, "CommonwealthOnline.Server.csproj");
|
||||||
|
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
WorkingDirectory = serverDir,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (File.Exists(apphost))
|
||||||
|
{
|
||||||
|
info.FileName = apphost;
|
||||||
|
info.ArgumentList.Add("serve");
|
||||||
|
}
|
||||||
|
else if (File.Exists(dll))
|
||||||
|
{
|
||||||
|
info.FileName = "dotnet";
|
||||||
|
info.ArgumentList.Add(dll);
|
||||||
|
info.ArgumentList.Add("serve");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
info.FileName = "dotnet";
|
||||||
|
info.ArgumentList.Add("run");
|
||||||
|
info.ArgumentList.Add("--project");
|
||||||
|
info.ArgumentList.Add(project);
|
||||||
|
info.ArgumentList.Add("-c");
|
||||||
|
info.ArgumentList.Add("Release");
|
||||||
|
info.ArgumentList.Add("--");
|
||||||
|
info.ArgumentList.Add("serve");
|
||||||
|
}
|
||||||
|
|
||||||
|
info.ArgumentList.Add("--config");
|
||||||
|
info.ArgumentList.Add(configPath);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommonwealthOnline.Host.Services;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainWindowViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private const int MaxLogLines = 2000;
|
||||||
|
|
||||||
|
private readonly ServerController _controller = new();
|
||||||
|
private readonly string _serverDir = System.IO.Directory.GetCurrentDirectory();
|
||||||
|
private readonly string _configPath =
|
||||||
|
Path.Combine(System.IO.Directory.GetCurrentDirectory(), "commonwealth-server.json");
|
||||||
|
|
||||||
|
[ObservableProperty] private string _serverName;
|
||||||
|
[ObservableProperty] private string _host;
|
||||||
|
[ObservableProperty] private int _port;
|
||||||
|
[ObservableProperty] private int _maxPlayers;
|
||||||
|
[ObservableProperty] private int _adminPort;
|
||||||
|
[ObservableProperty] private string _logVerbosity;
|
||||||
|
[ObservableProperty] private bool _enableGnsTransport;
|
||||||
|
[ObservableProperty] private bool _isRunning;
|
||||||
|
[ObservableProperty] private string _statusText = "Stopped";
|
||||||
|
|
||||||
|
public ObservableCollection<string> Log { get; } = new();
|
||||||
|
|
||||||
|
public string[] VerbosityOptions { get; } = { "error", "warning", "info", "debug" };
|
||||||
|
|
||||||
|
public MainWindowViewModel()
|
||||||
|
{
|
||||||
|
var config = ServerConfig.Load(_configPath);
|
||||||
|
_serverName = config.ServerName;
|
||||||
|
_host = config.Host;
|
||||||
|
_port = config.Port;
|
||||||
|
_maxPlayers = config.MaxPlayers;
|
||||||
|
_adminPort = config.AdminPort;
|
||||||
|
_logVerbosity = config.LogVerbosity;
|
||||||
|
_enableGnsTransport = config.EnableGnsTransport;
|
||||||
|
|
||||||
|
_controller.LogReceived += line =>
|
||||||
|
Dispatcher.UIThread.Post(() => Append(line));
|
||||||
|
_controller.RunningChanged += running =>
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
IsRunning = running;
|
||||||
|
StatusText = running ? "Running" : "Stopped";
|
||||||
|
StartCommand.NotifyCanExecuteChanged();
|
||||||
|
StopCommand.NotifyCanExecuteChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Append(string line)
|
||||||
|
{
|
||||||
|
Log.Add(line);
|
||||||
|
while (Log.Count > MaxLogLines)
|
||||||
|
{
|
||||||
|
Log.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerConfig CurrentConfig() => new()
|
||||||
|
{
|
||||||
|
ServerName = ServerName,
|
||||||
|
Host = Host,
|
||||||
|
Port = Port,
|
||||||
|
MaxPlayers = MaxPlayers,
|
||||||
|
AdminPort = AdminPort,
|
||||||
|
LogVerbosity = LogVerbosity,
|
||||||
|
EnableGnsTransport = EnableGnsTransport,
|
||||||
|
};
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save() => CurrentConfig().Save(_configPath);
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanStart))]
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
Append($"[host] starting server on {Host}:{Port}...");
|
||||||
|
_controller.Start(_serverDir, _configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanStart() => !IsRunning;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanStop))]
|
||||||
|
private void Stop()
|
||||||
|
{
|
||||||
|
Append("[host] stopping server...");
|
||||||
|
_controller.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanStop() => IsRunning;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:CommonwealthOnline.Host.ViewModels"
|
||||||
|
x:Class="CommonwealthOnline.Host.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Width="920" Height="640"
|
||||||
|
MinWidth="760" MinHeight="520"
|
||||||
|
Title="Commonwealth Online — Server Host"
|
||||||
|
Background="{StaticResource CoBackgroundBrush}">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="18">
|
||||||
|
|
||||||
|
<Image Grid.Row="0" Source="/Assets/logo.png" Height="88"
|
||||||
|
HorizontalAlignment="Left" Margin="0,0,0,14" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="330,*">
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Spacing="9" Margin="0,0,18,0">
|
||||||
|
<TextBlock Text="SERVER" Foreground="{StaticResource CoAccentBrush}"
|
||||||
|
FontWeight="Bold" FontSize="13" />
|
||||||
|
<TextBox Watermark="Server name" Text="{Binding ServerName}" />
|
||||||
|
<Grid ColumnDefinitions="*,*">
|
||||||
|
<TextBox Grid.Column="0" Watermark="Host" Text="{Binding Host}" Margin="0,0,4,0" />
|
||||||
|
<TextBox Grid.Column="1" Watermark="Port" Text="{Binding Port}" Margin="4,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
<Grid ColumnDefinitions="*,*">
|
||||||
|
<TextBox Grid.Column="0" Watermark="Max players" Text="{Binding MaxPlayers}" Margin="0,0,4,0" />
|
||||||
|
<TextBox Grid.Column="1" Watermark="Admin port" Text="{Binding AdminPort}" Margin="4,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="LOG VERBOSITY" Foreground="{StaticResource CoAccentBrush}"
|
||||||
|
FontWeight="Bold" FontSize="12" Margin="0,4,0,0" />
|
||||||
|
<ComboBox HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding VerbosityOptions}"
|
||||||
|
SelectedItem="{Binding LogVerbosity}" />
|
||||||
|
<CheckBox Content="Enable GNS transport" IsChecked="{Binding EnableGnsTransport}"
|
||||||
|
Foreground="{StaticResource CoTextBrush}" />
|
||||||
|
<Button Content="Save config" Command="{Binding SaveCommand}"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Background="{StaticResource CoSurfaceBrush}"
|
||||||
|
CornerRadius="4" Padding="10">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ItemsControl ItemsSource="{Binding Log}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" Foreground="{StaticResource CoTextBrush}"
|
||||||
|
FontFamily="Cascadia Mono,Consolas,monospace" FontSize="12"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,14,0,0">
|
||||||
|
<TextBlock Grid.Column="0" Text="Status:" Foreground="{StaticResource CoTextBrush}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0" />
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding StatusText}"
|
||||||
|
Foreground="{StaticResource CoAccentBrush}" FontWeight="Bold"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<Button Grid.Column="2" Content="Start" Command="{Binding StartCommand}"
|
||||||
|
Margin="0,0,8,0" Padding="26,7"
|
||||||
|
Background="{StaticResource CoAccentBrush}" Foreground="#0E120B" FontWeight="Bold" />
|
||||||
|
<Button Grid.Column="3" Content="Stop" Command="{Binding StopCommand}"
|
||||||
|
Padding="26,7"
|
||||||
|
Background="{StaticResource CoDangerBrush}" Foreground="White" FontWeight="Bold" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace CommonwealthOnline.Host.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user