Files
Matt Sijbers a1331212cb 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.
2026-05-05 00:19:05 +01:00

142 lines
5.9 KiB
C#

using System.ComponentModel;
using BrassAndSigil.Server.Models;
using BrassAndSigil.Server.Services;
using Spectre.Console;
using Spectre.Console.Cli;
namespace BrassAndSigil.Server.Commands;
public sealed class InstallCommand : AsyncCommand<InstallCommand.Settings>
{
public sealed class Settings : BaseCommandSettings
{
[CommandOption("--manifest <URL>")]
[Description("Manifest URL to bootstrap from")]
public string? ManifestUrl { get; set; }
[CommandOption("--server-dir <PATH>")]
[Description("Where to install the server (defaults to ./server)")]
public string? ServerDir { get; set; }
[CommandOption("--memory <MB>")]
[Description("RAM allocation in MB (defaults to 8192)")]
public int? MemoryMB { get; set; }
[CommandOption("--accept-eula")]
[Description("Accept the Minecraft EULA. Required for the server to actually run.")]
public bool AcceptEula { get; set; }
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
var config = ServerConfig.Load(settings.ConfigPath);
if (settings.ManifestUrl != null) config.ManifestUrl = settings.ManifestUrl;
if (settings.ServerDir != null) config.ServerDir = settings.ServerDir;
if (settings.MemoryMB != null) config.MemoryMB = settings.MemoryMB.Value;
if (settings.AcceptEula) config.AcceptEula = true;
AnsiConsole.MarkupLine("[bold yellow]Brass & Sigil Server install[/]");
AnsiConsole.MarkupLine($" Config: {settings.ConfigPath}");
AnsiConsole.MarkupLine($" ServerDir: {Path.GetFullPath(config.ServerDir)}");
AnsiConsole.MarkupLine($" Manifest: {config.ManifestUrl}");
AnsiConsole.MarkupLine($" Memory: {config.MemoryMB} MB");
AnsiConsole.MarkupLine("");
if (!config.AcceptEula)
{
AnsiConsole.MarkupLine("[red]EULA not accepted.[/] Re-run with --accept-eula to confirm you accept the");
AnsiConsole.MarkupLine("Minecraft End User License Agreement: [blue]https://aka.ms/MinecraftEULA[/]");
return 1;
}
Directory.CreateDirectory(config.ServerDir);
// Generate eula.txt
await File.WriteAllTextAsync(
Path.Combine(config.ServerDir, "eula.txt"),
$"# Generated by brass-sigil-server install\n" +
$"# By setting this to true you agree to the Minecraft EULA: https://aka.ms/MinecraftEULA\n" +
$"eula=true\n");
// Generate a default server.properties if none exists yet
var propsPath = Path.Combine(config.ServerDir, "server.properties");
if (!File.Exists(propsPath))
{
await File.WriteAllTextAsync(propsPath, DefaultServerProperties(config));
AnsiConsole.MarkupLine($"[grey]Wrote default server.properties[/]");
}
// Generate a random RCON password if missing
if (string.IsNullOrEmpty(config.RconPassword))
{
config.RconPassword = Convert.ToHexString(Guid.NewGuid().ToByteArray()).ToLowerInvariant();
UpdateProp(propsPath, "enable-rcon", "true");
UpdateProp(propsPath, "rcon.port", config.RconPort.ToString());
UpdateProp(propsPath, "rcon.password", config.RconPassword);
}
// Save config
config.Save(settings.ConfigPath);
AnsiConsole.MarkupLine($"[grey]Saved config to {settings.ConfigPath}[/]");
// Sync mods
AnsiConsole.MarkupLine("");
AnsiConsole.MarkupLine("[bold]Syncing mods from manifest...[/]");
var sync = new ManifestSync();
var progress = new Progress<string>(msg => AnsiConsole.MarkupLine($" [grey]{msg.EscapeMarkup()}[/]"));
try
{
var result = await sync.SyncAsync(config.ManifestUrl, config.ServerDir, progress);
AnsiConsole.MarkupLine("");
AnsiConsole.MarkupLine($"[green]✓ Sync complete:[/] pack v{result.PackVersion}, " +
$"{result.Downloaded} downloaded, {result.Removed} removed, " +
$"{result.Skipped} client-only mods skipped");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]✗ Sync failed:[/] {ex.Message.EscapeMarkup()}");
return 1;
}
AnsiConsole.MarkupLine("");
AnsiConsole.MarkupLine("[bold green]Install complete.[/]");
AnsiConsole.MarkupLine("Next steps:");
AnsiConsole.MarkupLine($" 1. Download the matching NeoForge server installer for the manifest's loader version");
AnsiConsole.MarkupLine($" (server side, into {Path.GetFullPath(config.ServerDir)}) -- the modpack manifest");
AnsiConsole.MarkupLine($" doesn't bundle it because each loader has its own installer.");
AnsiConsole.MarkupLine($" [blue]https://maven.neoforged.net/releases/net/neoforged/neoforge/[/]");
AnsiConsole.MarkupLine($" 2. Run the NeoForge installer with --installServer in the server dir");
AnsiConsole.MarkupLine($" 3. Then start: [yellow]brass-sigil-server run[/]");
return 0;
}
private static string DefaultServerProperties(ServerConfig config) => $@"# Brass & Sigil server defaults -- edit as needed
motd=Brass & Sigil
gamemode=survival
difficulty=normal
hardcore=false
pvp=true
online-mode=true
white-list=true
enforce-whitelist=true
max-players=20
view-distance=12
simulation-distance=10
spawn-protection=0
enable-rcon=true
rcon.port={config.RconPort}
rcon.password={config.RconPassword}
broadcast-rcon-to-ops=false
";
private static void UpdateProp(string path, string key, string value)
{
var lines = File.ReadAllLines(path).ToList();
var prefix = $"{key}=";
var idx = lines.FindIndex(l => l.StartsWith(prefix));
if (idx >= 0) lines[idx] = prefix + value;
else lines.Add(prefix + value);
File.WriteAllLines(path, lines);
}
}