Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.
- launcher/ Avalonia 12 desktop client; single-file win-x64 publish.
Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
portable install path, sidecar settings.
- server/ brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
MC subprocess, embedded Kestrel admin panel with cookie
auth + rate limiting, RCON bridge, scheduled backups,
BlueMap CLI integration with player markers + skin proxy,
friend-side whitelist request flow, world wipe with seed
selection (keep current / random / custom).
- pack/ pack.lock.json (Modrinth + manual CurseForge entries),
data-only tweak source under tweaks/, build outputs in
overrides/ (gitignored).
- scripts/ Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
plus Deploy-Brass.ps1 unified one-shot deploy with
version-bump pre-flight and daemon-state detection.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
using BrassAndSigil.Server.Models;
|
||||
|
||||
namespace BrassAndSigil.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes Minecraft's <c>server.properties</c>. Editable keys are
|
||||
/// gated by an allowlist so a compromised panel can't flip security-critical
|
||||
/// fields like <c>online-mode</c> arbitrarily -- only common gameplay knobs.
|
||||
/// Preserves comments and key order on write; appends new keys at the end.
|
||||
/// </summary>
|
||||
public sealed class ServerPropertiesService
|
||||
{
|
||||
private readonly ServerConfig _config;
|
||||
|
||||
public ServerPropertiesService(ServerConfig config) => _config = config;
|
||||
|
||||
/// <summary>
|
||||
/// Keys that may be modified via /api/server/settings. Anything outside this
|
||||
/// set is silently dropped from the update payload -- admin must SSH for those.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlySet<string> EditableKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"motd",
|
||||
"difficulty",
|
||||
"gamemode",
|
||||
"view-distance",
|
||||
"simulation-distance",
|
||||
"max-players",
|
||||
"pvp",
|
||||
"hardcore",
|
||||
"white-list",
|
||||
"enforce-whitelist",
|
||||
"allow-flight",
|
||||
"enable-command-block",
|
||||
"spawn-protection",
|
||||
};
|
||||
|
||||
public string PropertiesPath => Path.Combine(Path.GetFullPath(_config.ServerDir), "server.properties");
|
||||
|
||||
public Dictionary<string, string> ReadAll()
|
||||
{
|
||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (!File.Exists(PropertiesPath)) return dict;
|
||||
foreach (var raw in File.ReadAllLines(PropertiesPath))
|
||||
{
|
||||
var line = raw.TrimStart();
|
||||
if (line.Length == 0 || line[0] == '#' || line[0] == '!') continue;
|
||||
var idx = line.IndexOf('=');
|
||||
if (idx < 0) continue;
|
||||
dict[line.Substring(0, idx).Trim()] = line.Substring(idx + 1);
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>Returns just the editable subset, with values left as raw strings.</summary>
|
||||
public Dictionary<string, string> ReadEditable()
|
||||
{
|
||||
var all = ReadAll();
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var key in EditableKeys)
|
||||
{
|
||||
if (all.TryGetValue(key, out var v)) result[key] = v;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current <c>level-seed</c> value, or null if absent / empty.
|
||||
/// </summary>
|
||||
public string? GetLevelSeed()
|
||||
{
|
||||
if (!File.Exists(PropertiesPath)) return null;
|
||||
foreach (var raw in File.ReadAllLines(PropertiesPath))
|
||||
{
|
||||
var line = raw.TrimStart();
|
||||
if (!line.StartsWith("level-seed=", StringComparison.Ordinal)) continue;
|
||||
var v = line.Substring("level-seed=".Length).Trim();
|
||||
return string.IsNullOrEmpty(v) ? null : v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct write of <c>level-seed</c>. Bypasses <see cref="EditableKeys"/>
|
||||
/// because the seed is set as part of the wipe flow (with confirmation),
|
||||
/// not by general settings UI -- exposing it through the regular Update()
|
||||
/// path would let it be flipped from any settings save. Empty string
|
||||
/// clears the field, which makes Minecraft pick a random seed on next
|
||||
/// world generation.
|
||||
/// </summary>
|
||||
public void SetLevelSeed(string seed)
|
||||
{
|
||||
var lines = File.Exists(PropertiesPath)
|
||||
? File.ReadAllLines(PropertiesPath).ToList()
|
||||
: new List<string>();
|
||||
var done = false;
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var trimmed = lines[i].TrimStart();
|
||||
if (trimmed.StartsWith("level-seed=", StringComparison.Ordinal))
|
||||
{
|
||||
lines[i] = $"level-seed={seed}";
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!done) lines.Add($"level-seed={seed}");
|
||||
File.WriteAllLines(PropertiesPath, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply updates to the file. Keys not in <see cref="EditableKeys"/> are
|
||||
/// silently dropped. Lines that already exist are updated in-place to
|
||||
/// preserve order and comments; new keys are appended at the end.
|
||||
/// </summary>
|
||||
public void Update(IDictionary<string, string> updates)
|
||||
{
|
||||
// Filter to allowed keys only.
|
||||
var filtered = updates
|
||||
.Where(kv => EditableKeys.Contains(kv.Key))
|
||||
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
|
||||
if (filtered.Count == 0) return;
|
||||
|
||||
var lines = File.Exists(PropertiesPath)
|
||||
? File.ReadAllLines(PropertiesPath).ToList()
|
||||
: new List<string>();
|
||||
|
||||
var applied = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var raw = lines[i];
|
||||
var trimmed = raw.TrimStart();
|
||||
if (trimmed.Length == 0 || trimmed[0] == '#' || trimmed[0] == '!') continue;
|
||||
var idx = trimmed.IndexOf('=');
|
||||
if (idx < 0) continue;
|
||||
var key = trimmed.Substring(0, idx).Trim();
|
||||
if (filtered.TryGetValue(key, out var newValue))
|
||||
{
|
||||
lines[i] = $"{key}={newValue}";
|
||||
applied.Add(key);
|
||||
}
|
||||
}
|
||||
foreach (var (key, value) in filtered)
|
||||
{
|
||||
if (!applied.Contains(key)) lines.Add($"{key}={value}");
|
||||
}
|
||||
File.WriteAllLines(PropertiesPath, lines);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user