using HSchool.Content; using HSchool.Server.Game; namespace HSchool.Server.Api; /// /// Catalog for the create dialog. Loading defs here is safe: Content is immutable and this never /// touches a live School or its worker. /// internal static class ModEndpoints { public static void MapModEndpoints(this IEndpointRouteBuilder builder) { builder.MapGet("/api/mods", (ModContent mods) => new ModsResponse(mods.ListPacks().Select(pack => new ModInfoResponse(pack.Id, pack.Required)).ToArray())) .WithName("GetMods"); builder.MapGet("/api/catalog", (string? lang, string? mods, ModContent content) => { var extras = ParseModIds(mods); foreach (var packId in extras) { if (!ModContent.IsSafePackId(packId) || !content.PackExists(packId)) { return Problem(StatusCodes.Status400BadRequest, "unknown-mod", $"Unknown mod '{packId}'."); } } if (!content.PackExists(CatalogLoader.CorePackId)) { return Problem(StatusCodes.Status400BadRequest, "invalid-catalog", "The core pack is missing."); } var packIds = content.NormalizePackIds(extras); DefCatalog catalog; MapLayout map; try { catalog = content.LoadCatalog(packIds); map = content.LoadMap(packIds, saved: null); MapValidator.Validate(map, catalog); } catch (SchoolContentUnavailableException ex) { return Problem(StatusCodes.Status400BadRequest, "invalid-catalog", ex.Message); } catch (ContentLoadException ex) { return Problem(StatusCodes.Status400BadRequest, "invalid-catalog", ex.Message); } catch (MapValidationException ex) { return Problem(StatusCodes.Status400BadRequest, "invalid-map", ex.Message); } var locale = string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase) ? "en" : "ru"; return Results.Ok(CatalogResponse.From(catalog, map, locale)); }) .WithName("GetCatalog"); } private static IReadOnlyList ParseModIds(string? mods) { if (string.IsNullOrWhiteSpace(mods)) { return []; } return mods.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } 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 ModsResponse(IReadOnlyList Mods); internal sealed record ModInfoResponse(string Id, bool Required); internal sealed record CatalogResponse( IReadOnlyList Territories, IReadOnlyList Buildings, IReadOnlyList Floors, IReadOnlyList Rooms, IReadOnlyList Things, IReadOnlyList NameSets, IReadOnlyList Subjects, MapLayout DefaultMap) { public static CatalogResponse From(DefCatalog catalog, MapLayout map, string locale) => new( Placeable(catalog.Territories.Values, catalog, locale), Placeable(catalog.Buildings.Values, catalog, locale), Placeable(catalog.Floors.Values, catalog, locale), PlaceableRooms(catalog, locale), PlaceableThings(catalog, locale), Placeable(catalog.NameSets.Values, catalog, locale), PlaceableSubjects(catalog, locale), map); private static IReadOnlyList Placeable(IEnumerable defs, DefCatalog catalog, string locale) where T : Def => defs .Where(def => !def.Abstract) .OrderBy(def => def.DefName, StringComparer.Ordinal) .Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def))) .ToArray(); private static IReadOnlyList PlaceableThings(DefCatalog catalog, string locale) => catalog.Things.Values .Where(def => !def.Abstract) .OrderBy(def => def.DefName, StringComparer.Ordinal) .Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def), def.PupilSlots)) .ToArray(); private static IReadOnlyList PlaceableRooms(DefCatalog catalog, string locale) => catalog.Rooms.Values .Where(def => !def.Abstract) .OrderBy(def => def.DefName, StringComparer.Ordinal) .Select(def => new RoomInfoResponse( def.DefName, catalog.Label(locale, def), def.Homeroom, def.SeatThing, def.DefaultSeats, def.Slots.Select(slot => new RoomSlotInfo(slot.Key, slot.Thing, slot.Count)).ToArray(), def.Positions.ToArray())) .ToArray(); private static IReadOnlyList PlaceableSubjects(DefCatalog catalog, string locale) => catalog.Subjects.Values .Where(def => !def.Abstract) .OrderBy(def => def.DefName, StringComparer.Ordinal) .Select(def => new SubjectInfoResponse( def.DefName, catalog.Label(locale, def), def.Grades.Min, def.Grades.Max, def.HoursPerWeek, def.Skills.Select(share => new SubjectSkillInfo(share.Skill, share.Share)).ToArray())) .ToArray(); } internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0); internal sealed record RoomInfoResponse( string DefName, string Label, bool Homeroom, string? SeatThing, int DefaultSeats, IReadOnlyList Slots, IReadOnlyList Positions); internal sealed record RoomSlotInfo(string Key, string Thing, int Count); internal sealed record SubjectSkillInfo(string Skill, float Share); internal sealed record SubjectInfoResponse( string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek, IReadOnlyList Skills);