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,
|
name = config.Name,
|
||||||
description = config.Description,
|
description = config.Description,
|
||||||
|
tags = string.Join(", ", config.Tags ?? new()),
|
||||||
maxPlayers = config.MaxPlayers,
|
maxPlayers = config.MaxPlayers,
|
||||||
isPublic = config.Public,
|
isPublic = config.Public,
|
||||||
requireSteam = config.RequireSteam,
|
requireSteam = config.RequireSteam,
|
||||||
@@ -63,6 +64,13 @@ public static class ConfigEditor
|
|||||||
if (name.Length == 0) return "The server name cannot be empty";
|
if (name.Length == 0) return "The server name cannot be empty";
|
||||||
config.Name = name;
|
config.Name = name;
|
||||||
config.Description = Text("description", config.Description, 300);
|
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.MaxPlayers = Number("maxPlayers", config.MaxPlayers, 1, 64);
|
||||||
config.Public = Flag("isPublic", config.Public);
|
config.Public = Flag("isPublic", config.Public);
|
||||||
config.RequireSteam = Flag("requireSteam", config.RequireSteam);
|
config.RequireSteam = Flag("requireSteam", config.RequireSteam);
|
||||||
@@ -93,7 +101,7 @@ public static class ConfigEditor
|
|||||||
restartNeeded = true;
|
restartNeeded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed.Count == 0) return "Nothing changed";
|
if (changed.Count == 0) return "Nothing changed" + tagNote;
|
||||||
try { config.Save(configPath); }
|
try { config.Save(configPath); }
|
||||||
catch (Exception e) { return "Could not save server.json: " + e.Message; }
|
catch (Exception e) { return "Could not save server.json: " + e.Message; }
|
||||||
GameServer.Log($"Settings changed by {by}: {string.Join(", ", changed)}");
|
GameServer.Log($"Settings changed by {by}: {string.Join(", ", changed)}");
|
||||||
@@ -101,6 +109,7 @@ public static class ConfigEditor
|
|||||||
return "Settings saved" +
|
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"
|
(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" : "") +
|
: 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; }
|
public bool Unreachable { get; init; }
|
||||||
}
|
}
|
||||||
record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers,
|
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 HttpClient http = new() { Timeout = TimeSpan.FromSeconds(10) };
|
||||||
readonly string baseUrl;
|
readonly string baseUrl;
|
||||||
@@ -72,7 +72,8 @@ public sealed class MasterClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var body = new HeartbeatBody(config.LicenseKey, config.Name, config.Description, config.PublicAddress, config.Port,
|
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);
|
var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body);
|
||||||
int status = (int)response.StatusCode;
|
int status = (int)response.StatusCode;
|
||||||
if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0);
|
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" },
|
h("div", { class: "section-body form-grid wide" },
|
||||||
text("name", "Server name", c.name, { max: 60 }),
|
text("name", "Server name", c.name, { max: 60 }),
|
||||||
text("description", "Description", c.description, { max: 300 }),
|
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 }),
|
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)" }),
|
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),
|
toggle("isPublic", "Listed in the server browser", c.isPublic),
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
window.MSCO_LANG = lang;
|
window.MSCO_LANG = lang;
|
||||||
|
|
||||||
const DE = {
|
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
|
// Updates
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
"Update available": "Update verfügbar",
|
"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"],
|
[/^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
|
[/^Settings saved(.*)$/, (m, rest) => "Einstellungen gespeichert" + rest
|
||||||
.replace(" - some changes need a restart", " - manche Änderungen brauchen einen Neustart")
|
.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",
|
.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")],
|
" - 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")],
|
[/^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"],
|
[/^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"],
|
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
|
||||||
[/^Update available: (.+)$/, "Update verfügbar: $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 (.+) is available$/, "Version $1 ist verfügbar"],
|
||||||
[/^Version ([0-9.]+)$/, "Version $1"],
|
[/^Version ([0-9.]+)$/, "Version $1"],
|
||||||
[/^Install update (.+)\?$/, "Update $1 installieren?"],
|
[/^Install update (.+)\?$/, "Update $1 installieren?"],
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ public sealed class ServerConfig
|
|||||||
public string LicenseKey { get; set; } = "";
|
public string LicenseKey { get; set; } = "";
|
||||||
public string Name { get; set; } = "My MSCO Server";
|
public string Name { get; set; } = "My MSCO Server";
|
||||||
public string Description { get; set; } = "";
|
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;
|
public int Port { get; set; } = 7777;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adresse, unter der Spieler den Server erreichen (IP oder Domain). Leer = die IP, von der
|
/// 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);
|
Name = Take("MSCO_NAME", Name);
|
||||||
Description = Take("MSCO_DESCRIPTION", Description);
|
Description = Take("MSCO_DESCRIPTION", Description);
|
||||||
PublicAddress = Take("MSCO_PUBLIC_ADDRESS", PublicAddress);
|
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);
|
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_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"); }
|
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;
|
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)
|
static string Env(string name)
|
||||||
{
|
{
|
||||||
string value = Environment.GetEnvironmentVariable(name);
|
string value = Environment.GetEnvironmentVariable(name);
|
||||||
|
|||||||
@@ -486,6 +486,17 @@ async Task PanelTests()
|
|||||||
using (var da = new System.IO.Compression.ZipArchive(new MemoryStream(downZip)))
|
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");
|
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
|
// 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"
|
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");
|
&& Updater.ParseModVersion("public const string ModVersion = \"2.9\";") == "2.9", "Update versions are read from the source");
|
||||||
|
|||||||
Reference in New Issue
Block a user