Own server tags in the web panel (Settings) and server.json
Tags (max. 6, normalized like the keymaster) are sent with the heartbeat; official and featured are only given by the MSCO admins and are refused with a note. MSCO_TAGS environment variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -10,6 +10,7 @@ public static class ConfigEditor
|
||||
{
|
||||
name = config.Name,
|
||||
description = config.Description,
|
||||
tags = string.Join(", ", config.Tags ?? new()),
|
||||
maxPlayers = config.MaxPlayers,
|
||||
isPublic = config.Public,
|
||||
requireSteam = config.RequireSteam,
|
||||
@@ -63,6 +64,13 @@ public static class ConfigEditor
|
||||
if (name.Length == 0) return "The server name cannot be empty";
|
||||
config.Name = name;
|
||||
config.Description = Text("description", config.Description, 300);
|
||||
string tagNote = "";
|
||||
if (b.TryGetProperty("tags", out var tagValue) && tagValue.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
var tags = ServerConfig.NormalizeTags(tagValue.GetString().Split(','), out var reserved);
|
||||
if (!tags.SequenceEqual(config.Tags ?? new())) { config.Tags = tags; changed.Add("tags"); }
|
||||
if (reserved.Count > 0) tagNote = $" - the tag {string.Join(", ", reserved)} is only given by the MSCO admins";
|
||||
}
|
||||
config.MaxPlayers = Number("maxPlayers", config.MaxPlayers, 1, 64);
|
||||
config.Public = Flag("isPublic", config.Public);
|
||||
config.RequireSteam = Flag("requireSteam", config.RequireSteam);
|
||||
@@ -93,7 +101,7 @@ public static class ConfigEditor
|
||||
restartNeeded = true;
|
||||
}
|
||||
|
||||
if (changed.Count == 0) return "Nothing changed";
|
||||
if (changed.Count == 0) return "Nothing changed" + tagNote;
|
||||
try { config.Save(configPath); }
|
||||
catch (Exception e) { return "Could not save server.json: " + e.Message; }
|
||||
GameServer.Log($"Settings changed by {by}: {string.Join(", ", changed)}");
|
||||
@@ -101,6 +109,7 @@ public static class ConfigEditor
|
||||
return "Settings saved" +
|
||||
(portChanged ? " - the new port only applies after the whole program restarts (docker compose restart); in Docker publish it in docker-compose.yml first"
|
||||
: restartNeeded ? " - some changes need a restart" : "") +
|
||||
(overridden.Count > 0 ? " (note: " + string.Join(", ", overridden) + " is also set by an environment variable, which wins on restart)" : "");
|
||||
(overridden.Count > 0 ? " (note: " + string.Join(", ", overridden) + " is also set by an environment variable, which wins on restart)" : "") +
|
||||
tagNote;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -22,7 +22,7 @@ public sealed class MasterClient
|
||||
public bool Unreachable { get; init; }
|
||||
}
|
||||
record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers,
|
||||
string Version, bool Public, bool RequireSteam);
|
||||
string Version, bool Public, bool RequireSteam, List<string> Tags);
|
||||
|
||||
readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(10) };
|
||||
readonly string baseUrl;
|
||||
@@ -72,7 +72,8 @@ public sealed class MasterClient
|
||||
try
|
||||
{
|
||||
var body = new HeartbeatBody(config.LicenseKey, config.Name, config.Description, config.PublicAddress, config.Port,
|
||||
players, config.MaxPlayers, version, config.Public, config.RequireSteam);
|
||||
players, config.MaxPlayers, version, config.Public, config.RequireSteam,
|
||||
ServerConfig.NormalizeTags(config.Tags, out _));
|
||||
var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body);
|
||||
int status = (int)response.StatusCode;
|
||||
if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0);
|
||||
|
||||
@@ -396,6 +396,8 @@ function renderSettings(page) {
|
||||
h("div", { class: "section-body form-grid wide" },
|
||||
text("name", "Server name", c.name, { max: 60 }),
|
||||
text("description", "Description", c.description, { max: 300 }),
|
||||
text("tags", "Tags", c.tags, { max: 140, placeholder: "e.g. german, roleplay, vanilla",
|
||||
hint: "Shown in the server list, comma separated (max. 6). The tag official is only given by the MSCO admins." }),
|
||||
number("maxPlayers", "Max players", c.maxPlayers, { min: 1, max: 64 }),
|
||||
text("publicAddress", "Public address", c.publicAddress, { max: 200, placeholder: "automatic", hint: "IP or domain players connect to (empty = automatic)" }),
|
||||
toggle("isPublic", "Listed in the server browser", c.isPublic),
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
window.MSCO_LANG = lang;
|
||||
|
||||
const DE = {
|
||||
// Tags
|
||||
"Tags": "Tags",
|
||||
"e.g. german, roleplay, vanilla": "z.B. deutsch, roleplay, vanilla",
|
||||
"Shown in the server list, comma separated (max. 6). The tag official is only given by the MSCO admins.":
|
||||
"Werden in der Serverliste angezeigt, mit Komma getrennt (max. 6). Den Tag official vergeben nur die MSCO-Admins.",
|
||||
|
||||
// Updates
|
||||
"Update": "Update",
|
||||
"Update available": "Update verfügbar",
|
||||
@@ -377,6 +383,7 @@
|
||||
[/^Save game uploaded \((\d+) files\) - start the server to use it$/, "Spielstand hochgeladen ($1 Dateien) - starte den Server, um ihn zu benutzen"],
|
||||
[/^Settings saved(.*)$/, (m, rest) => "Einstellungen gespeichert" + rest
|
||||
.replace(" - some changes need a restart", " - manche Änderungen brauchen einen Neustart")
|
||||
.replace(/ - the tag (.+) is only given by the MSCO admins$/, " - den Tag $1 vergeben nur die MSCO-Admins")
|
||||
.replace(" - the new port only applies after the whole program restarts (docker compose restart); in Docker publish it in docker-compose.yml first",
|
||||
" - der neue Port gilt erst nach einem Neustart des ganzen Programms (docker compose restart); in Docker vorher in docker-compose.yml freigeben")],
|
||||
[/^The server is already (\w+)$/, (m, s) => "Der Server " + (s === "running" ? "läuft schon" : "startet schon")],
|
||||
@@ -385,6 +392,7 @@
|
||||
[/^No license key set - (.+)$/, "Kein Lizenzschlüssel gesetzt - hol dir einen unter msco.mbm.wtf/keys und trag ihn in den Einstellungen ein"],
|
||||
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
|
||||
[/^Update available: (.+)$/, "Update verfügbar: $1"],
|
||||
[/^(.*) - the tag (.+) is only given by the MSCO admins$/, (m, before, tag) => (DE[before] || before) + " - den Tag " + tag + " vergeben nur die MSCO-Admins"],
|
||||
[/^Version (.+) is available$/, "Version $1 ist verfügbar"],
|
||||
[/^Version ([0-9.]+)$/, "Version $1"],
|
||||
[/^Install update (.+)\?$/, "Update $1 installieren?"],
|
||||
|
||||
@@ -8,6 +8,11 @@ public sealed class ServerConfig
|
||||
public string LicenseKey { get; set; } = "";
|
||||
public string Name { get; set; } = "My MSCO Server";
|
||||
public string Description { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Eigene Tags für die Serverliste (z.B. "deutsch", "roleplay"), höchstens 6. "official" und "featured" vergeben
|
||||
/// nur die MSCO-Admins im Keymaster.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
public int Port { get; set; } = 7777;
|
||||
/// <summary>
|
||||
/// Adresse, unter der Spieler den Server erreichen (IP oder Domain). Leer = die IP, von der
|
||||
@@ -90,6 +95,7 @@ public sealed class ServerConfig
|
||||
Name = Take("MSCO_NAME", Name);
|
||||
Description = Take("MSCO_DESCRIPTION", Description);
|
||||
PublicAddress = Take("MSCO_PUBLIC_ADDRESS", PublicAddress);
|
||||
if (Env("MSCO_TAGS") is string tags) { Tags = NormalizeTags(tags.Split(','), out _); used.Add("MSCO_TAGS"); }
|
||||
SteamWebApiKey = Take("MSCO_STEAM_WEB_API_KEY", SteamWebApiKey);
|
||||
if (int.TryParse(Env("MSCO_PORT"), out int port)) { Port = port; used.Add("MSCO_PORT"); }
|
||||
if (int.TryParse(Env("MSCO_MAX_PLAYERS"), out int max)) { MaxPlayers = max; used.Add("MSCO_MAX_PLAYERS"); }
|
||||
@@ -103,6 +109,28 @@ public sealed class ServerConfig
|
||||
return used;
|
||||
}
|
||||
|
||||
public const int MaxTags = 6;
|
||||
/// <summary>Diese Tags vergeben nur die MSCO-Admins (Keymaster), nicht der Server selbst.</summary>
|
||||
public static readonly string[] ReservedTags = { "official", "featured" };
|
||||
|
||||
/// <summary>Wie der Keymaster: "Deutsch, Role Play!" -> ["deutsch", "role-play"]; reservierte fallen weg.</summary>
|
||||
public static List<string> NormalizeTags(IEnumerable<string> input, out List<string> reserved)
|
||||
{
|
||||
reserved = new List<string>();
|
||||
var tags = new List<string>();
|
||||
foreach (string raw in input ?? Array.Empty<string>())
|
||||
{
|
||||
string t = (raw ?? "").Trim().ToLowerInvariant().Replace("ä", "ae").Replace("ö", "oe").Replace("ü", "ue").Replace("ß", "ss");
|
||||
t = System.Text.RegularExpressions.Regex.Replace(t, @"\s+", "-");
|
||||
t = System.Text.RegularExpressions.Regex.Replace(t, "[^a-z0-9-]", "").Trim('-');
|
||||
if (t.Length > 20) t = t[..20].Trim('-');
|
||||
if (t.Length == 0 || tags.Contains(t)) continue;
|
||||
if (ReservedTags.Contains(t)) { if (!reserved.Contains(t)) reserved.Add(t); continue; }
|
||||
if (tags.Count < MaxTags) tags.Add(t);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
static string Env(string name)
|
||||
{
|
||||
string value = Environment.GetEnvironmentVariable(name);
|
||||
|
||||
@@ -486,6 +486,17 @@ async Task PanelTests()
|
||||
using (var da = new System.IO.Compression.ZipArchive(new MemoryStream(downZip)))
|
||||
Check(down.IsSuccessStatusCode && da.GetEntry("defaultES2File.txt") != null, "The current save can be downloaded as ZIP");
|
||||
|
||||
// Eigene Tags: bereinigt, "official"/"featured" nur über die MSCO-Admins
|
||||
var cleanTags = ServerConfig.NormalizeTags(new[] { " Deutsch ", "Role Play!", "OFFICIAL", "für alle", "deutsch", "featured" }, out var reservedTags);
|
||||
Check(string.Join(",", cleanTags) == "deutsch,role-play,fuer-alle" && string.Join(",", reservedTags) == "official,featured",
|
||||
"Tags are normalized and official/featured are reserved: " + string.Join(",", cleanTags));
|
||||
var tagRes = await Call(owner, "POST", "api/config", new { tags = "Vanilla, official, Deutsch" });
|
||||
Check(tagRes.status == 200 && tagRes.json.GetProperty("message").GetString().Contains("only given by the MSCO admins")
|
||||
&& string.Join(",", ServerConfig.LoadOrCreate(configPath, out _).Tags) == "vanilla,deutsch",
|
||||
"Own tags can be set in the panel, official is refused: " + tagRes.json);
|
||||
var tagCfg = await Call(owner, "GET", "api/config");
|
||||
Check(tagCfg.json.GetProperty("tags").GetString() == "vanilla, deutsch", "The panel shows the tags");
|
||||
|
||||
// Updates: Versionen vergleichen, Status im Panel, Installieren nur mit verfügbarem Update
|
||||
Check(Updater.ParseCsprojVersion("<Project><PropertyGroup><Version>2.10.1</Version></PropertyGroup></Project>") == "2.10.1"
|
||||
&& Updater.ParseModVersion("public const string ModVersion = \"2.9\";") == "2.9", "Update versions are read from the source");
|
||||
|
||||
Reference in New Issue
Block a user