271 lines
9.8 KiB
C#
271 lines
9.8 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", (string? lang, ModContent mods) =>
|
|
{
|
|
var locale = string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase) ? "en" : "ru";
|
|
return new ModsResponse(mods.ListPacks(locale).Select(pack => new ModInfoResponse(
|
|
pack.Id,
|
|
pack.Required,
|
|
pack.Label,
|
|
pack.Version,
|
|
pack.Requires)).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.");
|
|
}
|
|
|
|
IReadOnlyList<string> packIds;
|
|
try
|
|
{
|
|
packIds = content.ResolveSelectedPacks(extras);
|
|
}
|
|
catch (PackDependencyException ex)
|
|
{
|
|
return PackProblem(ex);
|
|
}
|
|
catch (ContentLoadException ex)
|
|
{
|
|
return Problem(StatusCodes.Status400BadRequest, "invalid-catalog", ex.Message);
|
|
}
|
|
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 PackProblem(PackDependencyException ex) =>
|
|
Results.Problem(
|
|
detail: ex.Message,
|
|
statusCode: StatusCodes.Status400BadRequest,
|
|
title: ex.Code,
|
|
extensions: new Dictionary<string, object?>
|
|
{
|
|
["code"] = ex.Code,
|
|
["missing"] = ex.MissingPackId,
|
|
});
|
|
|
|
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,
|
|
string Label,
|
|
string Version,
|
|
IReadOnlyList<string> Requires);
|
|
|
|
internal sealed record CatalogResponse(
|
|
IReadOnlyList<DefInfoResponse> Territories,
|
|
IReadOnlyList<DefInfoResponse> Buildings,
|
|
IReadOnlyList<DefInfoResponse> Floors,
|
|
IReadOnlyList<RoomInfoResponse> Rooms,
|
|
IReadOnlyList<DefInfoResponse> Things,
|
|
IReadOnlyList<NameSetInfoResponse> NameSets,
|
|
IReadOnlyList<SubjectInfoResponse> Subjects,
|
|
DayFrameResponse? DayFrame,
|
|
IReadOnlyList<HolidayInfoResponse> Holidays,
|
|
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),
|
|
PlaceableNameSets(catalog, locale),
|
|
PlaceableSubjects(catalog, locale),
|
|
MapDayFrame(catalog, locale),
|
|
PlaceableHolidays(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<NameSetInfoResponse> PlaceableNameSets(DefCatalog catalog, string locale) =>
|
|
catalog.NameSets.Values
|
|
.Where(def => !def.Abstract)
|
|
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
|
.Select(def => new NameSetInfoResponse(
|
|
def.DefName,
|
|
catalog.Label(locale, def),
|
|
def.Spoken
|
|
.Select(skill => new DefInfoResponse(
|
|
skill,
|
|
catalog.Skills.TryGetValue(skill, out var language)
|
|
? catalog.Label(locale, language)
|
|
: skill))
|
|
.ToArray()))
|
|
.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(),
|
|
def.Room))
|
|
.ToArray();
|
|
|
|
private static DayFrameResponse? MapDayFrame(DefCatalog catalog, string locale)
|
|
{
|
|
var frame = catalog.DayFrame;
|
|
return frame is null
|
|
? null
|
|
: new DayFrameResponse(
|
|
frame.DefName,
|
|
catalog.Label(locale, frame),
|
|
frame.FirstLesson,
|
|
frame.LessonCount,
|
|
frame.LessonMinutes,
|
|
frame.BreakMinutes,
|
|
frame.LongBreakAfter,
|
|
frame.LongBreakMinutes);
|
|
}
|
|
|
|
private static IReadOnlyList<HolidayInfoResponse> PlaceableHolidays(DefCatalog catalog, string locale) =>
|
|
catalog.Holidays.Values
|
|
.Where(def => !def.Abstract)
|
|
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
|
.Select(def => new HolidayInfoResponse(
|
|
def.DefName,
|
|
catalog.Label(locale, def),
|
|
def.Start,
|
|
def.End))
|
|
.ToArray();
|
|
}
|
|
|
|
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
|
|
|
|
internal sealed record NameSetInfoResponse(
|
|
string DefName,
|
|
string Label,
|
|
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
|
|
|
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,
|
|
string? Room);
|
|
|
|
internal sealed record DayFrameResponse(
|
|
string DefName,
|
|
string Label,
|
|
string FirstLesson,
|
|
int LessonCount,
|
|
int LessonMinutes,
|
|
int BreakMinutes,
|
|
int LongBreakAfter,
|
|
int LongBreakMinutes);
|
|
|
|
internal sealed record HolidayInfoResponse(string DefName, string Label, MonthDay Start, MonthDay End);
|