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);