Split the person card into overview, clothes, carry and now tabs.
Today's history is a paged HTTP log on the worker, not on the card or in people.json. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,16 @@ internal enum PersonLookupError
|
||||
|
||||
internal sealed record PersonCardResult(PersonCardResponse? Card, PersonLookupError Error);
|
||||
|
||||
internal sealed record PersonLogResult(PersonLogResponse? Page, PersonLookupError Error);
|
||||
|
||||
internal sealed record PersonLogResponse(
|
||||
int Total,
|
||||
int Page,
|
||||
int PageSize,
|
||||
IReadOnlyList<PersonLogEntryResponse> Entries);
|
||||
|
||||
internal sealed record PersonLogEntryResponse(DateTime Time, string Type, string Label, string? ThingDef);
|
||||
|
||||
internal sealed record PeopleListResponse(
|
||||
int Total,
|
||||
int Page,
|
||||
@@ -65,14 +75,18 @@ internal sealed record PersonCardResponse(
|
||||
IReadOnlyList<WornItemResponse> Worn,
|
||||
IReadOnlyList<CarriedItemResponse> Carried,
|
||||
float CarryMass,
|
||||
float CarryCapacity);
|
||||
float CarryCapacity,
|
||||
bool HasLocker,
|
||||
int HomeCount);
|
||||
|
||||
internal sealed record WornItemResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
string? Color,
|
||||
string? ColorLabel,
|
||||
IReadOnlyList<DefLabelResponse> Layers);
|
||||
IReadOnlyList<DefLabelResponse> Layers,
|
||||
float Condition,
|
||||
string? ConditionLabel);
|
||||
|
||||
internal sealed record CarriedItemResponse(
|
||||
string DefName,
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace HSchool.Server.Api;
|
||||
|
||||
/// <summary>
|
||||
/// The main menu talks to these: list, create, delete. People list and staffing read published
|
||||
/// snapshots; the person card, hire and subject changes go through the school's mailbox.
|
||||
/// snapshots; the person card, the personal log, hire and subject changes go through the school's mailbox.
|
||||
/// The timetable is a published snapshot; pin/unpin go through the mailbox.
|
||||
/// </summary>
|
||||
internal static class SchoolEndpoints
|
||||
@@ -194,6 +194,41 @@ internal static class SchoolEndpoints
|
||||
})
|
||||
.WithName("GetSchoolPerson");
|
||||
|
||||
schools.MapGet("/{id:int}/people/{personId}/log", async (
|
||||
int id,
|
||||
string personId,
|
||||
string? q,
|
||||
string? sort,
|
||||
string? dir,
|
||||
int? page,
|
||||
int? pageSize,
|
||||
string? lang,
|
||||
GameCommandQueue commands,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(personId) || personId.Length > 64)
|
||||
{
|
||||
return Problem(StatusCodes.Status400BadRequest, "invalid-query", "The person id is not valid.");
|
||||
}
|
||||
|
||||
if (!TryParsePersonLogQuery(q, sort, dir, page, pageSize, out var query, out var error))
|
||||
{
|
||||
return Problem(StatusCodes.Status400BadRequest, "invalid-query", error);
|
||||
}
|
||||
|
||||
var command = new GameCommand.GetPersonLog(id, personId, ParseLocale(lang), query, NewCompletion<PersonLogResult>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
|
||||
return outcome.Error switch
|
||||
{
|
||||
PersonLookupError.None when outcome.Page is not null => Results.Ok(outcome.Page),
|
||||
PersonLookupError.UnknownPerson => Problem(StatusCodes.Status404NotFound, "unknown-person", "That person is not in the school."),
|
||||
_ => Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."),
|
||||
};
|
||||
})
|
||||
.WithName("GetSchoolPersonLog");
|
||||
|
||||
schools.MapGet("/{id:int}/staffing", (
|
||||
int id,
|
||||
string? lang,
|
||||
@@ -396,6 +431,66 @@ internal static class SchoolEndpoints
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParsePersonLogQuery(
|
||||
string? search,
|
||||
string? sort,
|
||||
string? dir,
|
||||
int? page,
|
||||
int? pageSize,
|
||||
out PersonLogQuery query,
|
||||
out string error)
|
||||
{
|
||||
query = default;
|
||||
error = string.Empty;
|
||||
|
||||
if (search is { Length: > 128 })
|
||||
{
|
||||
error = "q must be at most 128 characters.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sort) && !sort.Equals("time", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = "sort must be time.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var descending = true;
|
||||
if (!string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
if (dir.Equals("asc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
descending = false;
|
||||
}
|
||||
else if (!dir.Equals("desc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = "dir must be asc or desc.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var parsedPage = page ?? 1;
|
||||
if (parsedPage < 1)
|
||||
{
|
||||
error = "page must be 1 or greater.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsedPageSize = pageSize ?? PersonLogBrowser.DefaultPageSize;
|
||||
if (parsedPageSize < 1 || parsedPageSize > PersonLogBrowser.MaxPageSize)
|
||||
{
|
||||
error = $"pageSize must be between 1 and {PersonLogBrowser.MaxPageSize}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
query = new PersonLogQuery(
|
||||
string.IsNullOrWhiteSpace(search) ? null : search.Trim(),
|
||||
descending,
|
||||
parsedPage,
|
||||
parsedPageSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static StaffingResponse MapStaffing(PublishedSchoolPeople published, float allocated, string locale) =>
|
||||
StaffingMapper.From(
|
||||
published.Roster,
|
||||
|
||||
@@ -59,6 +59,14 @@ internal abstract record GameCommand
|
||||
string Locale,
|
||||
TaskCompletionSource<PersonCardResult> Result) : GameCommand;
|
||||
|
||||
/// <summary>Today's history for one person. Completes on that school's worker; not a snapshot.</summary>
|
||||
internal sealed record GetPersonLog(
|
||||
int SchoolId,
|
||||
string PersonId,
|
||||
string Locale,
|
||||
PersonLogQuery Query,
|
||||
TaskCompletionSource<PersonLogResult> Result) : GameCommand;
|
||||
|
||||
internal sealed record HireStaff(
|
||||
int SchoolId,
|
||||
string PersonId,
|
||||
|
||||
@@ -187,6 +187,10 @@ internal sealed class GameLoopService(
|
||||
HandleGetPerson(getPerson);
|
||||
break;
|
||||
|
||||
case GameCommand.GetPersonLog getLog:
|
||||
HandleGetPersonLog(getLog);
|
||||
break;
|
||||
|
||||
case GameCommand.HireStaff hire:
|
||||
HandleStaffing(
|
||||
hire.SchoolId,
|
||||
@@ -242,6 +246,15 @@ internal sealed class GameLoopService(
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGetPersonLog(GameCommand.GetPersonLog command)
|
||||
{
|
||||
if (!_workers.TryGetValue(command.SchoolId, out var worker)
|
||||
|| !worker.Post(new WorkerCommand.GetPersonLog(command.PersonId, command.Locale, command.Query, command.Result)))
|
||||
{
|
||||
command.Result.TrySetResult(new PersonLogResult(null, PersonLookupError.UnknownSchool));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStaffing(int schoolId, WorkerCommand command, TaskCompletionSource<StaffingOutcome> result)
|
||||
{
|
||||
if (!_workers.TryGetValue(schoolId, out var worker) || !worker.Post(command))
|
||||
|
||||
@@ -85,7 +85,9 @@ internal static class PersonCardReader
|
||||
Worn(person, catalog, locale),
|
||||
Carried(person, catalog, locale),
|
||||
catalog is null ? 0f : CarryMass.Held(catalog, person.Items),
|
||||
Capacity(person, skills, catalog));
|
||||
Capacity(person, skills, catalog),
|
||||
person.Items.Any(item => item.Location.Equals(ItemLocations.Locker, StringComparison.Ordinal)),
|
||||
person.Items.Count(item => item.Location.Equals(ItemLocations.Home, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, float>? LiveNeeds(World world, string personId)
|
||||
@@ -288,7 +290,9 @@ internal static class PersonCardReader
|
||||
ThingLabel(catalog, locale, item.Def),
|
||||
item.Color,
|
||||
ColorLabel(catalog, locale, item.Color),
|
||||
layers));
|
||||
layers,
|
||||
item.Condition,
|
||||
ConditionLabel: null));
|
||||
}
|
||||
|
||||
return rows;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using HSchool.Server.Api;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
/// <summary>Pages today's log on the worker thread. The list is not a published snapshot.</summary>
|
||||
internal static class PersonLogReader
|
||||
{
|
||||
public static PersonLogResponse? Read(School school, string personId, PersonLogQuery query, string locale)
|
||||
{
|
||||
var roster = school.Roster;
|
||||
if (roster is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var known = roster.People.Any(person => person.Id.Equals(personId, StringComparison.Ordinal))
|
||||
|| (school.Applicants?.Applicants.Any(applicant => applicant.Person.Id.Equals(personId, StringComparison.Ordinal)) ?? false);
|
||||
if (!known)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var page = PersonLogBrowser.Apply(school.DayLog, personId, query, school.Catalog, locale);
|
||||
var entries = page.Entries
|
||||
.Select(row => new PersonLogEntryResponse(row.Time, row.Type, row.Label, row.ThingDef))
|
||||
.ToArray();
|
||||
return new PersonLogResponse(page.Total, page.Page, page.PageSize, entries);
|
||||
}
|
||||
}
|
||||
@@ -429,6 +429,14 @@ internal sealed class SchoolWorker
|
||||
: new PersonCardResult(card, PersonLookupError.None));
|
||||
break;
|
||||
|
||||
case WorkerCommand.GetPersonLog getLog:
|
||||
var log = PersonLogReader.Read(school, getLog.PersonId, getLog.Query, getLog.Locale);
|
||||
getLog.Result.TrySetResult(
|
||||
log is null
|
||||
? new PersonLogResult(null, PersonLookupError.UnknownPerson)
|
||||
: new PersonLogResult(log, PersonLookupError.None));
|
||||
break;
|
||||
|
||||
case WorkerCommand.HireStaff hire:
|
||||
hire.Result.TrySetResult(ApplyHire(school, hire.PersonId, hire.Position));
|
||||
break;
|
||||
@@ -485,6 +493,9 @@ internal sealed class SchoolWorker
|
||||
case WorkerCommand.GetPerson getPerson:
|
||||
getPerson.Result.TrySetResult(new PersonCardResult(null, PersonLookupError.UnknownSchool));
|
||||
break;
|
||||
case WorkerCommand.GetPersonLog getLog:
|
||||
getLog.Result.TrySetResult(new PersonLogResult(null, PersonLookupError.UnknownSchool));
|
||||
break;
|
||||
case WorkerCommand.HireStaff hire:
|
||||
hire.Result.TrySetResult(Staffing.UnknownSchool());
|
||||
break;
|
||||
@@ -513,6 +524,9 @@ internal sealed class SchoolWorker
|
||||
case WorkerCommand.GetPerson getPerson:
|
||||
getPerson.Result.TrySetException(exception);
|
||||
break;
|
||||
case WorkerCommand.GetPersonLog getLog:
|
||||
getLog.Result.TrySetException(exception);
|
||||
break;
|
||||
case WorkerCommand.HireStaff hire:
|
||||
hire.Result.TrySetException(exception);
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using HSchool.People;
|
||||
using HSchool.Server.Api;
|
||||
using HSchool.Server.Net;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
@@ -27,6 +28,12 @@ internal abstract record WorkerCommand
|
||||
string Locale,
|
||||
TaskCompletionSource<PersonCardResult> Result) : WorkerCommand;
|
||||
|
||||
internal sealed record GetPersonLog(
|
||||
string PersonId,
|
||||
string Locale,
|
||||
PersonLogQuery Query,
|
||||
TaskCompletionSource<PersonLogResult> Result) : WorkerCommand;
|
||||
|
||||
internal sealed record HireStaff(
|
||||
string PersonId,
|
||||
string Position,
|
||||
|
||||
@@ -159,5 +159,8 @@
|
||||
"WinterBreak": "Winter break",
|
||||
"SpringBreak": "Spring break",
|
||||
"SummerBreak": "Summer break",
|
||||
"ActionStarted": "started: {0}",
|
||||
"ActionEnded": "finished: {0}",
|
||||
"ApparelReplaced": "got a new {0}",
|
||||
"core": "Core",
|
||||
}
|
||||
|
||||
@@ -159,5 +159,8 @@
|
||||
"WinterBreak": "Зимние каникулы",
|
||||
"SpringBreak": "Весенние каникулы",
|
||||
"SummerBreak": "Летние каникулы",
|
||||
"ActionStarted": "начал: {0}",
|
||||
"ActionEnded": "закончил: {0}",
|
||||
"ApparelReplaced": "получил новую {0}",
|
||||
"core": "Базовая игра",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user