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:
NomadsReach
2026-08-16 18:15:40 -04:00
parent 6861fa4276
commit 1f01e68794
11 changed files with 431 additions and 0 deletions
+49
View File
@@ -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));
}
+106
View File
@@ -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;
}
}