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:
@@ -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