158 lines
5.5 KiB
C#
158 lines
5.5 KiB
C#
using HSchool.People;
|
|
using HSchool.Schedule;
|
|
using HSchool.Server.Game;
|
|
using HSchool.Simulation;
|
|
|
|
namespace HSchool.Server.Api;
|
|
|
|
/// <summary>
|
|
/// Diagnostic HTTP that exists only when <c>HSchool:AllowSaveReload</c> is on. Never mapped in
|
|
/// production by default.
|
|
/// </summary>
|
|
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<SchoolLiveDump?>());
|
|
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<PostedNotice?>());
|
|
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<string, PresenceSnapshot> presence,
|
|
IReadOnlyDictionary<string, IReadOnlyDictionary<string, float>> needs)
|
|
{
|
|
string? nodeId = null;
|
|
if (presence.TryGetValue(person.Id, out var row))
|
|
{
|
|
nodeId = row.NodeId;
|
|
}
|
|
|
|
IReadOnlyDictionary<string, float> 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<T> NewCompletion<T>() =>
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
private static IResult Problem(int statusCode, string code, string detail) =>
|
|
Results.Problem(detail: detail, statusCode: statusCode, title: code, extensions: new Dictionary<string, object?> { ["code"] = code });
|
|
}
|
|
|
|
internal sealed record SchoolDumpResponse(
|
|
int Id,
|
|
string Name,
|
|
DateTime GameTime,
|
|
bool Running,
|
|
IReadOnlyList<SchoolDumpPersonResponse> People,
|
|
IReadOnlyList<SchoolDumpLessonResponse> Now,
|
|
IReadOnlyList<SchoolDumpLessonResponse> Lessons);
|
|
|
|
internal sealed record SchoolDumpPersonResponse(
|
|
string Id,
|
|
string FullName,
|
|
string? NodeId,
|
|
IReadOnlyDictionary<string, float> 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);
|