Files
h-school/src/HSchool.Server/Api/ModEndpoints.cs
T
2026-08-18 23:51:10 +03:00

169 lines
6.4 KiB
C#

using HSchool.Content;
using HSchool.Server.Game;
namespace HSchool.Server.Api;
/// <summary>
/// Catalog for the create dialog. Loading defs here is safe: Content is immutable and this never
/// touches a live <c>School</c> or its worker.
/// </summary>
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<string> 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<string, object?>
{
["code"] = code,
});
}
internal sealed record ModsResponse(IReadOnlyList<ModInfoResponse> Mods);
internal sealed record ModInfoResponse(string Id, bool Required);
internal sealed record CatalogResponse(
IReadOnlyList<DefInfoResponse> Territories,
IReadOnlyList<DefInfoResponse> Buildings,
IReadOnlyList<DefInfoResponse> Floors,
IReadOnlyList<RoomInfoResponse> Rooms,
IReadOnlyList<DefInfoResponse> Things,
IReadOnlyList<DefInfoResponse> NameSets,
IReadOnlyList<SubjectInfoResponse> 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<DefInfoResponse> Placeable<T>(IEnumerable<T> 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<DefInfoResponse> 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<RoomInfoResponse> 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<SubjectInfoResponse> 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<RoomSlotInfo> Slots,
IReadOnlyList<string> 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<SubjectSkillInfo> Skills);