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 { public sealed class Settings : BaseCommandSettings { [CommandOption("--manifest ")] [Description("Manifest URL to bootstrap from")] public string? ManifestUrl { get; set; } [CommandOption("--server-dir ")] [Description("Where to install the server (defaults to ./server)")] public string? ServerDir { get; set; } [CommandOption("--memory ")] [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 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(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); } }