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:
@@ -0,0 +1,179 @@
|
||||
using System.Globalization;
|
||||
using Arch.Core;
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Log row types the person card pages. Captions for morning issue are ready even while phase 34
|
||||
/// is the one that appends that row.
|
||||
/// </summary>
|
||||
public static class PersonLogTypes
|
||||
{
|
||||
public const string ActionStarted = "action-started";
|
||||
public const string ActionEnded = "action-ended";
|
||||
public const string ApparelReplaced = "apparel-replaced";
|
||||
public const string ApparelChanged = "apparel-changed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One thing that happened to a person today. Stored on the school worker, not in <c>people.json</c>.
|
||||
/// The day boundary is six in the morning — the same hour skip lands on.
|
||||
/// </summary>
|
||||
public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type, string? ThingDef)
|
||||
{
|
||||
public string Caption(DefCatalog catalog, string locale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
if (ThingDef is null)
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.ApparelReplaced, StringComparison.Ordinal))
|
||||
{
|
||||
var name = catalog.Things.TryGetValue(ThingDef, out var def)
|
||||
? catalog.Label(locale, def)
|
||||
: ThingDef;
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "ApparelReplaced"), name);
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.ActionStarted, StringComparison.Ordinal)
|
||||
|| Type.Equals(PersonLogTypes.ActionEnded, StringComparison.Ordinal))
|
||||
{
|
||||
var name = catalog.Actions.TryGetValue(ThingDef, out var action)
|
||||
? catalog.Label(locale, action)
|
||||
: ThingDef;
|
||||
var key = Type.Equals(PersonLogTypes.ActionStarted, StringComparison.Ordinal)
|
||||
? "ActionStarted"
|
||||
: "ActionEnded";
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||
}
|
||||
|
||||
return Type;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct PersonLogQuery(string? Search, bool Descending, int Page, int PageSize);
|
||||
|
||||
public sealed record PersonLogPage(int Total, int Page, int PageSize, IReadOnlyList<PersonLogRow> Entries);
|
||||
|
||||
public sealed record PersonLogRow(DateTime Time, string Type, string Label, string? ThingDef);
|
||||
|
||||
/// <summary>Filters, sorts and pages today's log for one person. Does not touch a World.</summary>
|
||||
public static class PersonLogBrowser
|
||||
{
|
||||
public const int DefaultPageSize = 20;
|
||||
|
||||
public const int MaxPageSize = 100;
|
||||
|
||||
public static PersonLogPage Apply(
|
||||
IReadOnlyList<PersonLogEvent> log,
|
||||
string personId,
|
||||
PersonLogQuery query,
|
||||
DefCatalog? catalog,
|
||||
string locale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(log);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(personId);
|
||||
|
||||
var page = Math.Max(1, query.Page);
|
||||
var pageSize = Math.Clamp(query.PageSize, 1, MaxPageSize);
|
||||
var search = string.IsNullOrWhiteSpace(query.Search) ? null : query.Search.Trim();
|
||||
var rows = new List<PersonLogRow>();
|
||||
foreach (var row in log)
|
||||
{
|
||||
if (!row.PersonId.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var label = catalog is null ? row.Type : row.Caption(catalog, locale);
|
||||
if (search is not null
|
||||
&& !label.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||||
&& !row.Type.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||||
&& (row.ThingDef is null || !row.ThingDef.Contains(search, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.Add(new PersonLogRow(row.Time, row.Type, label, row.ThingDef));
|
||||
}
|
||||
|
||||
rows.Sort((left, right) =>
|
||||
{
|
||||
var byTime = left.Time.CompareTo(right.Time);
|
||||
return query.Descending ? -byTime : byTime;
|
||||
});
|
||||
|
||||
var skip = (page - 1) * pageSize;
|
||||
var slice = skip >= rows.Count ? Array.Empty<PersonLogRow>() : rows.Skip(skip).Take(pageSize).ToArray();
|
||||
return new PersonLogPage(rows.Count, page, pageSize, slice);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Records action start/end and clears yesterday at six in the morning.</summary>
|
||||
internal static class PersonDayLog
|
||||
{
|
||||
private static readonly QueryDescription IdentityAndActivity =
|
||||
new QueryDescription().WithAll<PersonIdentity, PersonActivity>();
|
||||
|
||||
internal static bool CrossedDayStart(DateTime before, DateTime after)
|
||||
{
|
||||
if (after <= before)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cursor = DateTime.SpecifyKind(before.Date, DateTimeKind.Utc).Add(SchoolDay.DayStart.ToTimeSpan());
|
||||
if (before >= cursor)
|
||||
{
|
||||
cursor = cursor.AddDays(1);
|
||||
}
|
||||
|
||||
return after >= cursor;
|
||||
}
|
||||
|
||||
internal static void Sync(School school)
|
||||
{
|
||||
var world = school.World;
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
var query = IdentityAndActivity;
|
||||
world.Query(in query, (ref PersonIdentity identity, ref PersonActivity activity) =>
|
||||
{
|
||||
seen.Add(identity.Id);
|
||||
Record(school, identity.Id, activity.ActionId);
|
||||
});
|
||||
|
||||
foreach (var id in school.LoggedActivity.Keys.ToArray())
|
||||
{
|
||||
if (!seen.Contains(id))
|
||||
{
|
||||
Record(school, id, current: null);
|
||||
school.LoggedActivity.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Record(School school, string personId, string? current)
|
||||
{
|
||||
school.LoggedActivity.TryGetValue(personId, out var last);
|
||||
if (string.Equals(last, current, StringComparison.Ordinal))
|
||||
{
|
||||
school.LoggedActivity[personId] = current;
|
||||
return;
|
||||
}
|
||||
|
||||
if (last is not null)
|
||||
{
|
||||
school.AppendDayLog(new PersonLogEvent(personId, school.Clock.Time, PersonLogTypes.ActionEnded, last));
|
||||
}
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
school.AppendDayLog(new PersonLogEvent(personId, school.Clock.Time, PersonLogTypes.ActionStarted, current));
|
||||
}
|
||||
|
||||
school.LoggedActivity[personId] = current;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user