The bar was live and drove dressing; a frozen pupil in class still learned as if it were May. Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
7.2 KiB
C#
202 lines
7.2 KiB
C#
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";
|
|
public const string LessonNoTeacher = "lesson-no-teacher";
|
|
public const string LessonCold = "lesson-cold";
|
|
}
|
|
|
|
/// <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.ApparelChanged, StringComparison.Ordinal))
|
|
{
|
|
var name = catalog.Things.TryGetValue(ThingDef, out var def)
|
|
? catalog.Label(locale, def)
|
|
: ThingDef;
|
|
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "ApparelChanged"), 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);
|
|
}
|
|
|
|
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|
|
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal))
|
|
{
|
|
var name = catalog.Subjects.TryGetValue(ThingDef, out var subject)
|
|
? catalog.Label(locale, subject)
|
|
: ThingDef;
|
|
var key = Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
|
? "LessonCold"
|
|
: "LessonNoTeacher";
|
|
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;
|
|
}
|
|
}
|