Add C# dedicated server replacement

This commit is contained in:
Nomads_Reach
2026-08-16 02:23:04 -04:00
parent 0024436ce8
commit ddadf87423
17 changed files with 3516 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace CommonwealthOnline.Server;
internal sealed class ServerOptions
{
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; }
public string ConfigPath { get; set; } = Path.GetFullPath("commonwealth-server.json");
public string BaseDirectory => Path.GetDirectoryName(ConfigPath)!;
public string BansPath => Path.Combine(BaseDirectory, "bans.json");
public string AdminTokenPath => Path.Combine(BaseDirectory, ".admin-token");
public static ServerOptions Load(string path)
{
var fullPath = Path.GetFullPath(path);
if (!File.Exists(fullPath)) throw new FileNotFoundException("Config file not found", fullPath);
var root = JsonNode.Parse(File.ReadAllBytes(fullPath)) as JsonObject ?? throw new InvalidDataException("Config file must contain a JSON object at root level.");
var options = new ServerOptions { ConfigPath = fullPath };
options.Host = JsonHelpers.String(root["host"]) ?? options.Host;
options.Port = ReadInt(root["port"], options.Port, "port");
options.ServerName = JsonHelpers.String(root["server_name"] ?? root["serverName"]) ?? options.ServerName;
options.ServerDescription = JsonHelpers.String(root["server_description"] ?? root["serverDescription"]) ?? options.ServerDescription;
options.MaxPlayers = ReadInt(root["max_players"] ?? root["maxPlayers"], options.MaxPlayers, "max_players");
options.LogVerbosity = (JsonHelpers.String(root["log_verbosity"] ?? root["logVerbosity"]) ?? options.LogVerbosity).Trim().ToLowerInvariant();
options.AdminPort = ReadInt(root["admin_port"] ?? root["adminPort"], options.AdminPort, "admin_port");
options.EnableGnsTransport = ReadBool(root["enable_gns_transport"] ?? root["enableGnsTransport"], false, "enable_gns_transport");
var bridge = JsonHelpers.String(root["gns_bridge_path"] ?? root["gnsBridgePath"]);
options.GnsBridgePath = string.IsNullOrWhiteSpace(bridge) ? null : bridge.Trim();
return options;
}
public static ServerOptions CreateDefault(string path)
{
var options = new ServerOptions { ConfigPath = Path.GetFullPath(path) };
options.Save();
return options;
}
public void Save()
{
Directory.CreateDirectory(BaseDirectory);
var root = new JsonObject
{
["host"] = Host,
["port"] = Port,
["server_name"] = ServerName,
["server_description"] = ServerDescription,
["max_players"] = MaxPlayers,
["log_verbosity"] = LogVerbosity,
["admin_port"] = AdminPort,
["enable_gns_transport"] = EnableGnsTransport,
["gns_bridge_path"] = GnsBridgePath
};
using var stream = File.Create(ConfigPath);
JsonSerializer.Serialize(stream, root, new JsonSerializerOptions { WriteIndented = true });
stream.WriteByte((byte)'\n');
}
public IReadOnlyList<string> Validate()
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(Host))
errors.Add("host cannot be empty");
else if (!TryResolveIpv4(Host, out _))
errors.Add($"host '{Host}' is not a valid IPv4 address or resolvable hostname");
else if (EnableGnsTransport && (!IPAddress.TryParse(Host.Trim(), out var gnsAddress) || gnsAddress.AddressFamily != AddressFamily.InterNetwork))
errors.Add("enable_gns_transport requires host to be an explicit IPv4 bind address such as 0.0.0.0 or 127.0.0.1");
if (Port is < 1 or > 65535) errors.Add($"port must be 1-65535, got {Port}");
if (AdminPort is < 1 or > 65535) errors.Add($"admin_port must be 1-65535, got {AdminPort}");
else if (AdminPort == Port) errors.Add("admin_port must differ from the game port");
if (string.IsNullOrEmpty(ServerName)) errors.Add("server_name cannot be empty");
else if (ServerName.Length > 64) errors.Add($"server_name must be <= 64 characters, got {ServerName.Length}");
if (ServerDescription.Length > 256) errors.Add($"server_description must be <= 256 characters, got {ServerDescription.Length}");
if (MaxPlayers < 1) errors.Add($"max_players must be >= 1, got {MaxPlayers}");
else if (MaxPlayers > 256) errors.Add($"max_players must be <= 256, got {MaxPlayers}");
if (LogVerbosity is not ("debug" or "info" or "warning" or "error")) errors.Add($"log_verbosity must be debug/info/warning/error, got {LogVerbosity}");
return errors;
}
public static bool TryResolveIpv4(string host, out IPAddress address)
{
if (IPAddress.TryParse(host.Trim(), out var parsed) && parsed.AddressFamily == AddressFamily.InterNetwork)
{
address = parsed;
return true;
}
try
{
address = Dns.GetHostAddresses(host.Trim()).First(x => x.AddressFamily == AddressFamily.InterNetwork);
return true;
}
catch
{
address = IPAddress.None;
return false;
}
}
private static int ReadInt(JsonNode? node, int defaultValue, string field)
{
if (node is null) return defaultValue;
if (node is JsonValue value && value.TryGetValue<int>(out var number)) return number;
if (node is JsonValue textValue && textValue.TryGetValue<string>(out var text) && int.TryParse(text, out number)) return number;
throw new InvalidDataException($"Invalid numeric config field: {field}");
}
private static bool ReadBool(JsonNode? node, bool defaultValue, string field)
{
if (node is null) return defaultValue;
if (node is JsonValue value && value.TryGetValue<bool>(out var boolean)) return boolean;
if (node is JsonValue intValue && intValue.TryGetValue<int>(out var number) && number is 0 or 1) return number == 1;
if (node is JsonValue textValue && textValue.TryGetValue<string>(out var text))
{
switch (text.Trim().ToLowerInvariant())
{
case "1": case "true": case "yes": case "on": return true;
case "0": case "false": case "no": case "off": case "": return false;
}
}
throw new InvalidDataException($"{field} must be a boolean or one of true/false, yes/no, on/off, 1/0");
}
}