Adds an admin-protocol client (token-authed TCP JSON on 127.0.0.1:AdminPort, reusing the server's .admin-token) that polls connected players and status every 3s, plus Kick/Ban on the selected player and a players/uptime readout.
244 lines
7.4 KiB
C#
244 lines
7.4 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading.Tasks;
|
|
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 = Directory.GetCurrentDirectory();
|
|
private readonly string _configPath =
|
|
Path.Combine(Directory.GetCurrentDirectory(), "commonwealth-server.json");
|
|
private readonly DispatcherTimer _pollTimer;
|
|
private bool _polling;
|
|
|
|
[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";
|
|
[ObservableProperty] private string _statsText = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(KickCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(BanCommand))]
|
|
private PlayerRow? _selectedPlayer;
|
|
|
|
public ObservableCollection<string> Log { get; } = new();
|
|
public ObservableCollection<PlayerRow> Players { 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();
|
|
if (!running)
|
|
{
|
|
Players.Clear();
|
|
StatsText = string.Empty;
|
|
}
|
|
});
|
|
|
|
_pollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
|
|
_pollTimer.Tick += async (_, _) => await PollAsync();
|
|
_pollTimer.Start();
|
|
}
|
|
|
|
private AdminClient CreateAdminClient()
|
|
{
|
|
var tokenPath = Path.Combine(Path.GetDirectoryName(_configPath) ?? _serverDir, ".admin-token");
|
|
return new AdminClient(AdminPort, tokenPath);
|
|
}
|
|
|
|
private async Task PollAsync()
|
|
{
|
|
if (!IsRunning || _polling)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_polling = true;
|
|
try
|
|
{
|
|
var admin = CreateAdminClient();
|
|
var clients = await admin.ClientsAsync().ConfigureAwait(true);
|
|
ApplyClients(clients);
|
|
|
|
var status = await admin.StatusAsync().ConfigureAwait(true);
|
|
ApplyStatus(status);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Server still starting, admin port not up yet, or token not written — ignore this tick.
|
|
}
|
|
finally
|
|
{
|
|
_polling = false;
|
|
}
|
|
}
|
|
|
|
private void ApplyClients(JsonObject? response)
|
|
{
|
|
if (response?["clients"] is not JsonArray array)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var previouslySelected = SelectedPlayer?.PlayerId;
|
|
Players.Clear();
|
|
foreach (var node in array)
|
|
{
|
|
if (node is not JsonObject client)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Players.Add(new PlayerRow
|
|
{
|
|
PlayerId = (uint)(client["player_id"]?.GetValue<long>() ?? 0),
|
|
Label = client["label"]?.GetValue<string>() ?? string.Empty,
|
|
Address = client["address"]?.GetValue<string>() ?? string.Empty,
|
|
PacketsReceived = client["packets_received"]?.GetValue<long>() ?? 0,
|
|
PacketsSent = client["packets_sent"]?.GetValue<long>() ?? 0,
|
|
});
|
|
}
|
|
|
|
if (previouslySelected is { } id)
|
|
{
|
|
foreach (var row in Players)
|
|
{
|
|
if (row.PlayerId == id)
|
|
{
|
|
SelectedPlayer = row;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyStatus(JsonObject? response)
|
|
{
|
|
if (response is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var connected = response["connected_clients"]?.GetValue<long>() ?? Players.Count;
|
|
var uptime = response["uptime_seconds"]?.GetValue<long>() ?? 0;
|
|
StatsText = $"{connected}/{MaxPlayers} players · up {uptime}s";
|
|
}
|
|
|
|
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;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanActOnPlayer))]
|
|
private async Task Kick()
|
|
{
|
|
if (SelectedPlayer is not { } player)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RunAdminAction(admin => admin.KickAsync(player.PlayerId, "Kicked by host"),
|
|
$"[host] kick #{player.PlayerId}").ConfigureAwait(true);
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(CanActOnPlayer))]
|
|
private async Task Ban()
|
|
{
|
|
if (SelectedPlayer is not { } player)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RunAdminAction(admin => admin.BanAsync(player.PlayerId, "Banned by host"),
|
|
$"[host] ban #{player.PlayerId}").ConfigureAwait(true);
|
|
}
|
|
|
|
private bool CanActOnPlayer() => IsRunning && SelectedPlayer is not null;
|
|
|
|
private async Task RunAdminAction(Func<AdminClient, Task<JsonObject?>> action, string label)
|
|
{
|
|
try
|
|
{
|
|
var response = await action(CreateAdminClient()).ConfigureAwait(true);
|
|
var message = response?["message"]?.GetValue<string>();
|
|
Append(string.IsNullOrEmpty(message) ? $"{label} sent" : $"{label}: {message}");
|
|
await PollAsync().ConfigureAwait(true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Append($"{label} failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|