Files
h-school/src/HSchool.Server/Program.cs
T
Leonid PershinandCursor 2b4cd29bda
ci / server (push) Failing after 3m34s
ci / client (push) Successful in 13s
Merge phase 27 save format and dump.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 00:34:54 +03:00

109 lines
4.4 KiB
C#

using System.Net.WebSockets;
using HSchool.Server.Api;
using HSchool.Server.Game;
using HSchool.Server.Net;
using HSchool.Simulation;
using Microsoft.Extensions.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services
.AddOptions<SimulationOptions>()
.Bind(builder.Configuration.GetSection(SimulationOptions.SectionName))
.Validate(options => options.TickRate is > 0 and <= 120, "Simulation:TickRate must be between 1 and 120.")
.Validate(options => options.MaxSchools is > 0 and <= 255, "Simulation:MaxSchools must be between 1 and 255.")
.Validate(options => options.GameMinutesPerRealSecond > 0, "Simulation:GameMinutesPerRealSecond must be positive.")
.Validate(options => GameClock.IsValidStartDate(options.DefaultStartDate), "Simulation:DefaultStartDate is out of range.")
.Validate(options => !string.IsNullOrWhiteSpace(options.SavesDirectory), "Simulation:SavesDirectory must be set.")
.Validate(options => !string.IsNullOrWhiteSpace(options.ModsDirectory), "Simulation:ModsDirectory must be set.")
.Validate(options => options.SaveIntervalSeconds is > 0 and <= 3600, "Simulation:SaveIntervalSeconds must be between 1 and 3600.")
.Validate(options => options.MonthlyPayrollCap > 0, "Simulation:MonthlyPayrollCap must be positive.")
.Validate(options => options.SchoolWeekDays is >= 5 and <= 7, "Simulation:SchoolWeekDays must be between 5 and 7.")
.Validate(options => options.MaxDecisionsPerTick is > 0 and <= 10_000, "Simulation:MaxDecisionsPerTick must be between 1 and 10000.")
.ValidateOnStart();
builder.Services.AddSingleton<GameCommandQueue>();
builder.Services.AddSingleton<ClientRegistry>();
builder.Services.AddSingleton<GameMetrics>();
builder.Services.AddSingleton<SchoolStore>();
builder.Services.AddSingleton<ModContent>();
builder.Services.AddSingleton<GameSocketHandler>();
builder.Services.AddSingleton<GameLoopService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<GameLoopService>());
builder.Services.AddOpenTelemetry().WithMetrics(metrics => metrics.AddMeter(GameMetrics.MeterName));
var app = builder.Build();
app.UseExceptionHandler();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
});
app.MapSchoolEndpoints();
app.MapTimetableEndpoints();
app.MapModEndpoints();
app.MapGet("/api/status", (GameLoopService loop, ClientRegistry clients) =>
{
var state = loop.SchoolsState;
return new GameStatusResponse(loop.CurrentTick, loop.Options.TickRate, state.Schools.Count, state.MaxSchools, clients.Count);
})
.WithName("GetGameStatus");
if (app.Configuration.GetValue("HSchool:AllowSaveReload", false))
{
app.MapPost("/api/dev/reload-schools", async (GameLoopService loop, CancellationToken cancellationToken) =>
{
await loop.ReloadFromDiskAsync(cancellationToken);
return Results.NoContent();
})
.WithName("ReloadSchoolsFromDisk");
app.MapGet("/api/dev/saves-directory", (SchoolStore store) => Results.Json(new { path = store.DirectoryPath }))
.WithName("GetSavesDirectory");
app.MapGet("/api/dev/mods-directory", (ModContent mods) => Results.Json(new { path = mods.Root }))
.WithName("GetModsDirectory");
app.MapDevEndpoints();
}
// The realtime channel: one binary frame per protocol message, see docs/protocol.md.
app.Map("/ws/game", async (HttpContext context, GameSocketHandler handler) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("This endpoint expects a WebSocket upgrade.");
return;
}
using WebSocket socket = await context.WebSockets.AcceptWebSocketAsync();
await handler.HandleAsync(socket, context.RequestAborted);
});
app.MapDefaultEndpoints();
// In a published container the built client lands in wwwroot next to the server.
app.UseFileServer();
app.Run();
/// <summary>Loop health for dashboards and integration tests.</summary>
internal sealed record GameStatusResponse(uint Tick, int TickRate, int Schools, int MaxSchools, int Connections);
/// <summary>Exposed so <c>WebApplicationFactory</c>-style tests can reference the entry point.</summary>
public partial class Program;