using BrassAndSigil.Server.Models;
namespace BrassAndSigil.Server.Services;
///
/// Reads and writes Minecraft's server.properties. Editable keys are
/// gated by an allowlist so a compromised panel can't flip security-critical
/// fields like online-mode arbitrarily -- only common gameplay knobs.
/// Preserves comments and key order on write; appends new keys at the end.
///
public sealed class ServerPropertiesService
{
private readonly ServerConfig _config;
public ServerPropertiesService(ServerConfig config) => _config = config;
///
/// 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.
///
public static readonly IReadOnlySet EditableKeys = new HashSet(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 ReadAll()
{
var dict = new Dictionary(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;
}
/// Returns just the editable subset, with values left as raw strings.
public Dictionary ReadEditable()
{
var all = ReadAll();
var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var key in EditableKeys)
{
if (all.TryGetValue(key, out var v)) result[key] = v;
}
return result;
}
///
/// Read the current level-seed value, or null if absent / empty.
///
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;
}
///
/// Direct write of level-seed. Bypasses
/// 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.
///
public void SetLevelSeed(string seed)
{
var lines = File.Exists(PropertiesPath)
? File.ReadAllLines(PropertiesPath).ToList()
: new List();
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);
}
///
/// Apply updates to the file. Keys not in are
/// silently dropped. Lines that already exist are updated in-place to
/// preserve order and comments; new keys are appended at the end.
///
public void Update(IDictionary 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();
var applied = new HashSet(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);
}
}