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,109 @@
|
||||
namespace BrassAndSigil.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Pushes player-visible messages and overlays into Minecraft via stdin
|
||||
/// commands (/say, /title, /bossbar). The boss-bar countdown is the primary
|
||||
/// primitive the updater uses for restart announcements.
|
||||
/// </summary>
|
||||
public sealed class Broadcaster
|
||||
{
|
||||
private readonly ServerProcess _proc;
|
||||
private const string BossBarId = "brass:announce";
|
||||
|
||||
public Broadcaster(ServerProcess proc) => _proc = proc;
|
||||
|
||||
public Task SayAsync(string message, CancellationToken ct = default)
|
||||
=> _proc.SendInputAsync($"say {SingleLine(message)}", ct);
|
||||
|
||||
public Task ActionBarAsync(string message, CancellationToken ct = default)
|
||||
=> _proc.SendInputAsync($"title @a actionbar {{\"text\":\"{EscapeJson(message)}\"}}", ct);
|
||||
|
||||
/// <summary>
|
||||
/// Re-sends the action bar text once per second so it stays sticky for the
|
||||
/// full duration. Action bar fades after ~2-3 s of inactivity, so the
|
||||
/// re-send is mandatory. Doesn't conflict with boss-bar UI for actual
|
||||
/// boss fights -- preferred over BossBarCountdownAsync for restart warnings.
|
||||
/// </summary>
|
||||
public async Task ActionBarCountdownAsync(
|
||||
string title, int durationSeconds, CancellationToken ct = default)
|
||||
{
|
||||
if (durationSeconds <= 0) return;
|
||||
// Silence /title's "Showing new title for X" chat broadcast for the loop --
|
||||
// otherwise it spams chat once per second per online player. Restored in
|
||||
// the finally block. World save typically isn't quick enough to persist
|
||||
// the off state if we crash mid-flight, but worst case admins can flip
|
||||
// it back manually with /gamerule sendCommandFeedback true.
|
||||
await _proc.SendInputAsync("gamerule sendCommandFeedback false", ct);
|
||||
try
|
||||
{
|
||||
for (int sec = durationSeconds; sec > 0; sec--)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var mins = sec / 60;
|
||||
var secs = sec % 60;
|
||||
var label = mins > 0 ? $"{title} -- {mins}m {secs:00}s" : $"{title} -- {secs}s";
|
||||
await _proc.SendInputAsync($"title @a actionbar {{\"text\":\"{EscapeJson(label)}\"}}", ct);
|
||||
try { await Task.Delay(1000, ct); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clear the action bar AND restore feedback. Both best-effort: if MC
|
||||
// is stopping these'll fail and that's fine.
|
||||
try { await _proc.SendInputAsync("title @a actionbar {\"text\":\"\"}"); } catch { }
|
||||
try { await _proc.SendInputAsync("gamerule sendCommandFeedback true"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public Task TitleAsync(string message, CancellationToken ct = default)
|
||||
=> _proc.SendInputAsync($"title @a title {{\"text\":\"{EscapeJson(message)}\"}}", ct);
|
||||
|
||||
/// <summary>
|
||||
/// Show a draining boss bar at the top of every player's screen for
|
||||
/// <paramref name="durationSeconds"/>. Updates the bar's name with a
|
||||
/// "title -- Mm Ss" countdown each second. Returns when the bar is removed.
|
||||
/// Honours cancellation: bar is removed cleanly even on cancel.
|
||||
/// </summary>
|
||||
public async Task BossBarCountdownAsync(
|
||||
string title, int durationSeconds, string color = "yellow", CancellationToken ct = default)
|
||||
{
|
||||
if (durationSeconds <= 0) return;
|
||||
|
||||
// Silence /bossbar feedback for the same reason as ActionBarCountdownAsync.
|
||||
await _proc.SendInputAsync("gamerule sendCommandFeedback false", ct);
|
||||
await _proc.SendInputAsync($"bossbar add {BossBarId} {{\"text\":\"{EscapeJson(title)}\"}}", ct);
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} color {color}", ct);
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} max {durationSeconds}", ct);
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} value {durationSeconds}", ct);
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} players @a", ct);
|
||||
|
||||
try
|
||||
{
|
||||
for (int sec = durationSeconds; sec > 0; sec--)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var mins = sec / 60;
|
||||
var secs = sec % 60;
|
||||
var label = mins > 0 ? $"{title} -- {mins}m {secs:00}s" : $"{title} -- {secs}s";
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} name {{\"text\":\"{EscapeJson(label)}\"}}", ct);
|
||||
await _proc.SendInputAsync($"bossbar set {BossBarId} value {sec}", ct);
|
||||
try { await Task.Delay(1000, ct); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always remove the bar -- even on cancel, so a stuck bar isn't left
|
||||
// on every player's screen. Use CancellationToken.None for the cleanup.
|
||||
try { await _proc.SendInputAsync($"bossbar remove {BossBarId}"); } catch { }
|
||||
try { await _proc.SendInputAsync("gamerule sendCommandFeedback true"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeJson(string s) =>
|
||||
s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", " ");
|
||||
|
||||
private static string SingleLine(string s) =>
|
||||
s.Replace("\r", " ").Replace("\n", " ").Trim();
|
||||
}
|
||||
Reference in New Issue
Block a user