From dt-brigid
.NET Generic Host patterns for headless game servers — BackgroundService tick loops, DI patterns, graceful shutdown, WebSocket hosting
npx claudepluginhub dreamteam-hq/brigid --plugin dt-brigidThis skill uses the workspace's default tool permissions.
Domain knowledge for .NET Generic Host patterns specific to headless game servers. NO MCP servers.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Domain knowledge for .NET Generic Host patterns specific to headless game servers. NO MCP servers.
Use WebApplication.CreateBuilder() + AddHostedService<T>() for background game logic. WebSocket endpoint handles client connections. Wire everything through DI.
CrystalMagica pattern: MapHub as singleton (owns all map state), SocketHandler as singleton (manages WebSocket connections), BackgroundService subclasses drive server-controlled entities (patrols, spawns, world ticks).
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MapHub>();
builder.Services.AddSingleton<SocketHandler>();
builder.Services.AddHostedService<GameLoopService>();
builder.Services.AddHostedService<PatrolService>();
ExecuteAsync with while (!stoppingToken.IsCancellationRequested). Use Task.Delay for tick cadence. Never spin-wait. Pass CancellationToken on every await. For physics-rate ticks: Stopwatch + sleep to maintain consistent tick rate regardless of work duration.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var sw = Stopwatch.StartNew();
while (!stoppingToken.IsCancellationRequested)
{
var tickStart = sw.ElapsedMilliseconds;
await DoTick(stoppingToken);
var elapsed = sw.ElapsedMilliseconds - tickStart;
var delay = Math.Max(0, TickIntervalMs - elapsed);
await Task.Delay((int)delay, stoppingToken);
}
}
AddSingleton for game state (MapHub, ConnectionService) — one instance, shared across all servicesAddHostedService for background loops — started/stopped by the hostCrystalMagica: MapHub registered as both concrete type and IMapHub interface for shared instance.
builder.Services.AddSingleton<MapHub>();
builder.Services.AddSingleton<IMapHub>(sp => sp.GetRequiredService<MapHub>());
app.UseWebSockets() + app.Map("/ws", handler.ProcessSocket). SocketHandler manages per-connection lifecycle. Each connection gets a ConnectedUser with an Outgoing channel. Use BoundedChannel with FullMode.DropOldest to prevent slow clients from blocking the server.
var channel = Channel.CreateBounded<ServerMessage>(
new BoundedChannelOptions(64) { FullMode = BoundedChannelFullMode.DropOldest });
Two tasks per connection: one reads from WebSocket, one writes from the channel. When either completes, the connection tears down.
ApplicationStopping token triggers shutdown sequence. BackgroundService.StopAsync called by the host. Drain player connections: send disconnect message, wait for outgoing channels to flush, close WebSockets with CloseAsync, timeout after 5s if clients don't disconnect cleanly.
lifetime.ApplicationStopping.Register(() =>
{
_ = connectionService.DisconnectAllAsync(TimeSpan.FromSeconds(5));
});
app.MapHealthChecks("/health") for container orchestration. Checks: can accept WebSocket connections, game loop is ticking (last tick within tolerance), memory pressure acceptable. Readiness probe: accepting new players? Liveness probe: process responsive?
appsettings.json for tuning (tick rate, max players, patrol parameters). IOptions<T> pattern. Environment variable overrides for containers. CrystalMagica today: all consts in code — move to config when there is more than one tunable per concern.
{ "GameServer": { "TickRateMs": 100, "MaxPlayers": 50, "PatrolSpeedMultiplier": 1.0 } }