using HSchool.People; using HSchool.Schedule; using HSchool.Server.Game; using HSchool.Simulation; namespace HSchool.Server.Api; /// /// Diagnostic HTTP that exists only when HSchool:AllowSaveReload is on. Never mapped in /// production by default. /// internal static class DevEndpoints { private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(5); public static void MapDevEndpoints(this IEndpointRouteBuilder builder) { builder.MapGet("/api/dev/schools/{id:int}/dump", async ( int id, GameLoopService loop, GameCommandQueue commands, CancellationToken cancellationToken) => { var published = loop.FindPeople(id); if (published is null) { return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."); } var command = new GameCommand.DumpSchool(id, NewCompletion()); commands.Enqueue(command); var live = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken); if (live is null) { return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."); } return Results.Ok(MapDump(published, live)); }) .WithName("DumpSchool"); builder.MapPost("/api/dev/schools/{id:int}/notices", async ( int id, PostDevNoticeRequest? request, GameLoopService loop, GameCommandQueue commands, CancellationToken cancellationToken) => { if (loop.FindSchool(id) is null) { return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."); } var defName = request?.DefName?.Trim() ?? ""; if (defName.Length == 0) { return Problem(StatusCodes.Status400BadRequest, "invalid-query", "defName is required."); } var command = new GameCommand.PostSchoolNotice( id, defName, request?.Person?.Trim() ?? "", request?.Kind?.Trim() ?? "", NewCompletion()); commands.Enqueue(command); var posted = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken); if (posted is null) { return Problem( StatusCodes.Status409Conflict, "notice-rejected", "Unknown event def or the sticky notice ceiling is full."); } return Results.Ok(new { id = posted.Id, defName = posted.DefName, pause = posted.Pause }); }) .WithName("PostDevNotice"); } private static SchoolDumpResponse MapDump(PublishedSchoolPeople published, SchoolLiveDump live) { var byId = live.Presence.ToDictionary(row => row.PersonId, StringComparer.Ordinal); var people = published.Roster?.People .OrderBy(person => person.Id, StringComparer.Ordinal) .Select(person => Person(person, byId, live.Needs)) .ToArray() ?? []; var lessons = (published.Timetable?.Lessons ?? []) .Select(Lesson) .ToArray(); var now = live.Now.Select(Lesson).ToArray(); return new SchoolDumpResponse( published.School.Id, published.School.Name, live.GameTime, live.Running, people, now, lessons); } private static SchoolDumpPersonResponse Person( Person person, IReadOnlyDictionary presence, IReadOnlyDictionary> needs) { string? nodeId = null; if (presence.TryGetValue(person.Id, out var row)) { nodeId = row.NodeId; } IReadOnlyDictionary values = person.Needs; if (needs.TryGetValue(person.Id, out var live)) { values = live; } return new SchoolDumpPersonResponse(person.Id, person.Name.Full, nodeId, values); } private static SchoolDumpLessonResponse Lesson(LessonPlacement lesson) => new(lesson.ClassId, lesson.Subject, lesson.TeacherId, lesson.RoomId, lesson.Day, lesson.Period); private static TaskCompletionSource NewCompletion() => new(TaskCreationOptions.RunContinuationsAsynchronously); private static IResult Problem(int statusCode, string code, string detail) => Results.Problem(detail: detail, statusCode: statusCode, title: code, extensions: new Dictionary { ["code"] = code }); } internal sealed record SchoolDumpResponse( int Id, string Name, DateTime GameTime, bool Running, IReadOnlyList People, IReadOnlyList Now, IReadOnlyList Lessons); internal sealed record SchoolDumpPersonResponse( string Id, string FullName, string? NodeId, IReadOnlyDictionary Needs); internal sealed record SchoolDumpLessonResponse( string ClassId, string Subject, string TeacherId, string RoomId, int Day, int Period); internal sealed record PostDevNoticeRequest(string? DefName, uint PersonId = 0, string? Person = null, string? Kind = null);