Split SchoolWorker and the person card along existing seams without a second thread or public API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 13:51:39 +03:00
co-authored by Cursor
parent 8d07b3606d
commit b48bbbcf7c
18 changed files with 2307 additions and 2193 deletions
+1 -475
View File
@@ -9,7 +9,7 @@ namespace HSchool.Server.Game;
/// <summary>
/// Builds a person card on the school's worker thread so live need values come from the World.
/// </summary>
internal static class PersonCardReader
internal static partial class PersonCardReader
{
private static readonly QueryDescription IdentityAndNeeds =
new QueryDescription().WithAll<PersonIdentity, PersonNeeds>();
@@ -136,478 +136,4 @@ internal static class PersonCardReader
});
return found;
}
private static IReadOnlyList<LabeledStatResponse> Body(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<LabeledStatResponse>();
if (catalog is not null)
{
foreach (var def in catalog.BodyAttributes.Values)
{
if (def.Abstract)
{
continue;
}
if (def.Kind == BodyAttributeKind.Number && person.Numbers.TryGetValue(def.DefName, out var number))
{
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), number.ToString()));
}
else if (def.Kind == BodyAttributeKind.Choice && person.Choices.TryGetValue(def.DefName, out var choice))
{
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), catalog.Text(locale, choice)));
}
}
}
else
{
foreach (var (id, number) in person.Numbers)
{
rows.Add(new LabeledStatResponse(id, id, number.ToString()));
}
foreach (var (id, choice) in person.Choices)
{
rows.Add(new LabeledStatResponse(id, id, choice));
}
}
if (person.Choices.TryGetValue(BodyBuilds.Attribute, out var build)
&& rows.TrueForAll(row => row.Id != BodyBuilds.Attribute))
{
var label = catalog?.Text(locale, BodyBuilds.Attribute) ?? BodyBuilds.Attribute;
var value = catalog?.Text(locale, build) ?? build;
rows.Add(new LabeledStatResponse(BodyBuilds.Attribute, label, value));
}
return rows;
}
private static IReadOnlyList<LabeledStatResponse> Skills(
Person person,
IReadOnlyDictionary<string, float>? live,
DefCatalog? catalog,
string locale)
{
if (catalog is null)
{
if (live is not null)
{
return live
.Select(pair => new LabeledStatResponse(pair.Key, pair.Key, FormatSkill(pair.Value)))
.ToArray();
}
return person.Skills
.Select(pair => new LabeledStatResponse(pair.Key, pair.Key, pair.Value.ToString()))
.ToArray();
}
var rows = new List<LabeledStatResponse>();
foreach (var def in catalog.Skills.Values)
{
if (def.Abstract)
{
continue;
}
if (live is not null)
{
if (!live.TryGetValue(def.DefName, out var liveValue))
{
continue;
}
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), FormatSkill(liveValue)));
continue;
}
if (!person.Skills.TryGetValue(def.DefName, out var value))
{
continue;
}
rows.Add(new LabeledStatResponse(def.DefName, catalog.Label(locale, def), value.ToString()));
}
return rows;
}
private static string FormatSkill(float value) => Math.Round(value, 2).ToString("0.##");
private static IReadOnlyList<DefLabelResponse> Traits(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<DefLabelResponse>(person.Traits.Count);
foreach (var id in person.Traits)
{
var label = catalog is not null && catalog.Traits.TryGetValue(id, out var def)
? catalog.Label(locale, def)
: id;
rows.Add(new DefLabelResponse(id, label));
}
return rows;
}
private static IReadOnlyList<NeedStatResponse> Needs(
IReadOnlyDictionary<string, float> values,
DefCatalog? catalog,
string locale)
{
if (catalog is null)
{
return values.Select(pair => new NeedStatResponse(pair.Key, pair.Key, pair.Value)).ToArray();
}
var rows = new List<NeedStatResponse>();
foreach (var def in catalog.Needs.Values)
{
if (def.Abstract || !values.TryGetValue(def.DefName, out var value))
{
continue;
}
rows.Add(new NeedStatResponse(def.DefName, catalog.Label(locale, def), value));
}
return rows;
}
private static IReadOnlyList<WornItemResponse> Worn(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<WornItemResponse>();
foreach (var item in person.Items)
{
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal))
{
continue;
}
IReadOnlyList<string> layerIds = [];
IReadOnlyList<string> fullyCovers = [];
if (catalog is not null && catalog.Things.TryGetValue(item.Def, out var def))
{
layerIds = def.Layers;
fullyCovers = def.FullyCoversLayers;
}
var layers = layerIds
.Select(layer => new DefLabelResponse(layer, catalog?.Text(locale, layer) ?? layer))
.ToArray();
rows.Add(new WornItemResponse(
item.Def,
ThingLabel(catalog, locale, item.Def),
item.Color,
ColorLabel(catalog, locale, item.Color),
layers,
item.Condition,
catalog is null
? ApparelCondition.BandId(null, item.Condition)
: ApparelCondition.Label(catalog, locale, item.Condition),
fullyCovers));
}
return rows;
}
private static IReadOnlyList<CarriedItemResponse> Carried(Person person, DefCatalog? catalog, string locale)
{
var rows = new List<CarriedItemResponse>();
foreach (var item in person.Items)
{
if (!item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal))
{
continue;
}
var mass = catalog is not null && catalog.Things.TryGetValue(item.Def, out var def) ? def.Mass : 0f;
string? subjectLabel = null;
if (item.Subject is not null)
{
subjectLabel = catalog is not null && catalog.Subjects.TryGetValue(item.Subject, out var subject)
? catalog.Label(locale, subject)
: item.Subject;
}
rows.Add(new CarriedItemResponse(
item.Def,
ThingLabel(catalog, locale, item.Def),
item.Color,
ColorLabel(catalog, locale, item.Color),
item.Subject,
subjectLabel,
mass));
}
return rows;
}
private static string ThingLabel(DefCatalog? catalog, string locale, string defName)
{
if (catalog is not null && catalog.Things.TryGetValue(defName, out var def))
{
return catalog.Label(locale, def);
}
return defName;
}
private static string? ColorLabel(DefCatalog? catalog, string locale, string? color) =>
color is null ? null : catalog?.Text(locale, color) ?? color;
private static float Capacity(
Person person,
IReadOnlyDictionary<string, float>? live,
DefCatalog? catalog)
{
if (catalog is null)
{
return 0f;
}
if (live is not null)
{
return CarryMass.Capacity(catalog, live);
}
return CarryMass.Capacity(catalog, person.Skills);
}
private static PersonFamilyResponse Family(Roster roster, Person person)
{
var family = roster.Families.FirstOrDefault(candidate => candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
if (family is null)
{
return new PersonFamilyResponse([], [], [], []);
}
var people = roster.People.ToDictionary(member => member.Id, StringComparer.Ordinal);
var inParents = InFamily(family.ParentIds, person.Id);
var inChildren = InFamily(family.ChildIds, person.Id);
return new PersonFamilyResponse(
inChildren ? Relatives(family.ParentIds, people, except: person.Id) : [],
inParents ? Relatives(family.ChildIds, people, except: person.Id) : [],
inChildren ? Relatives(family.ChildIds, people, except: person.Id) : [],
inParents ? Relatives(family.ParentIds, people, except: person.Id) : []);
}
private static bool InFamily(IReadOnlyList<string> ids, string id)
{
foreach (var candidate in ids)
{
if (candidate.Equals(id, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static IReadOnlyList<PersonRelResponse> Relatives(
IReadOnlyList<string> ids,
IReadOnlyDictionary<string, Person> people,
string except)
{
var rows = new List<PersonRelResponse>();
foreach (var id in ids)
{
if (id.Equals(except, StringComparison.Ordinal) || !people.TryGetValue(id, out var relative))
{
continue;
}
rows.Add(new PersonRelResponse(relative.Id, relative.Name.Full, relative.Female));
}
return rows;
}
private static PersonConnectionsResponse Connections(
Roster roster,
Person person,
DefCatalog? catalog,
string locale)
{
var familyIds = OpinionStore.FamilyMemberIds(roster, person);
var people = roster.People.ToDictionary(member => member.Id, StringComparer.Ordinal);
var family = FamilyWithOpinions(roster, person, people, catalog, locale);
var others = new List<PersonOpinionLinkResponse>();
foreach (var (targetId, value) in person.Opinions)
{
if (familyIds.Contains(targetId) || !people.TryGetValue(targetId, out var target))
{
continue;
}
others.Add(Link(target, value, catalog, locale));
}
others.Sort((left, right) =>
{
var byAbs = Math.Abs(right.Opinion).CompareTo(Math.Abs(left.Opinion));
return byAbs != 0
? byAbs
: string.Compare(left.FullName, right.FullName, StringComparison.Ordinal);
});
var top = catalog?.BehaviorRules?.OpinionTopCount ?? 5;
var friends = others.Where(row => row.Opinion > 0).Take(top).ToArray();
var enemies = others.Where(row => row.Opinion < 0).Take(top).ToArray();
var crushes = CrushesOf(person, people, catalog, locale);
var admirers = AdmirersOf(roster, person, people, catalog, locale);
var pair = PairOf(person, people, catalog, locale);
return new PersonConnectionsResponse(family, friends, enemies, others, crushes, admirers, pair);
}
private static DefLabelResponse? OrientationOf(Person person, DefCatalog? catalog, string locale)
{
if (person.Orientation is null)
{
return null;
}
var label = person.Orientation;
if (catalog is not null && catalog.Orientations.TryGetValue(person.Orientation, out var def))
{
label = catalog.Label(locale, def);
}
return new DefLabelResponse(person.Orientation, label);
}
private static IReadOnlyList<PersonOpinionLinkResponse> CrushesOf(
Person person,
IReadOnlyDictionary<string, Person> people,
DefCatalog? catalog,
string locale)
{
if (person.Bonds is null)
{
return [];
}
var rows = new List<PersonOpinionLinkResponse>();
foreach (var id in person.Bonds.Crushes)
{
if (!people.TryGetValue(id, out var target))
{
continue;
}
var opinion = OpinionStore.Get(person, id) ?? 0;
rows.Add(Link(target, opinion, catalog, locale));
}
return rows;
}
private static IReadOnlyList<PersonOpinionLinkResponse> AdmirersOf(
Roster roster,
Person person,
IReadOnlyDictionary<string, Person> people,
DefCatalog? catalog,
string locale)
{
var rows = new List<PersonOpinionLinkResponse>();
foreach (var other in roster.People)
{
if (other.Id.Equals(person.Id, StringComparison.Ordinal) || other.Bonds is null)
{
continue;
}
if (!other.Bonds.Crushes.Contains(person.Id, StringComparer.Ordinal))
{
continue;
}
var opinion = OpinionStore.Get(other, person.Id) ?? 0;
rows.Add(Link(other, opinion, catalog, locale));
}
return rows;
}
private static PersonRelResponse? PairOf(
Person person,
IReadOnlyDictionary<string, Person> people,
DefCatalog? catalog,
string locale)
{
if (person.Bonds?.PartnerId is not { } partnerId || !people.TryGetValue(partnerId, out var partner))
{
return null;
}
int? opinion = null;
string? label = null;
if (person.Opinions.TryGetValue(partnerId, out var value))
{
opinion = value;
label = catalog is null ? OpinionLabels.BandId(null, value) : OpinionLabels.Label(catalog, locale, value);
}
return new PersonRelResponse(partner.Id, partner.Name.Full, partner.Female, opinion, label);
}
private static PersonFamilyResponse FamilyWithOpinions(
Roster roster,
Person person,
IReadOnlyDictionary<string, Person> people,
DefCatalog? catalog,
string locale)
{
var family = roster.Families.FirstOrDefault(candidate => candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
if (family is null)
{
return new PersonFamilyResponse([], [], [], []);
}
var inParents = InFamily(family.ParentIds, person.Id);
var inChildren = InFamily(family.ChildIds, person.Id);
return new PersonFamilyResponse(
inChildren ? RelativesWithOpinions(family.ParentIds, people, person, except: person.Id, catalog, locale) : [],
inParents ? RelativesWithOpinions(family.ChildIds, people, person, except: person.Id, catalog, locale) : [],
inChildren ? RelativesWithOpinions(family.ChildIds, people, person, except: person.Id, catalog, locale) : [],
inParents ? RelativesWithOpinions(family.ParentIds, people, person, except: person.Id, catalog, locale) : []);
}
private static IReadOnlyList<PersonRelResponse> RelativesWithOpinions(
IReadOnlyList<string> ids,
IReadOnlyDictionary<string, Person> people,
Person person,
string except,
DefCatalog? catalog,
string locale)
{
var rows = new List<PersonRelResponse>();
foreach (var id in ids)
{
if (id.Equals(except, StringComparison.Ordinal) || !people.TryGetValue(id, out var relative))
{
continue;
}
int? opinion = null;
string? label = null;
if (person.Opinions.TryGetValue(id, out var value))
{
opinion = value;
label = catalog is null ? OpinionLabels.BandId(null, value) : OpinionLabels.Label(catalog, locale, value);
}
rows.Add(new PersonRelResponse(relative.Id, relative.Name.Full, relative.Female, opinion, label));
}
return rows;
}
private static PersonOpinionLinkResponse Link(Person target, int opinion, DefCatalog? catalog, string locale) =>
new(
target.Id,
target.Name.Full,
target.Female,
opinion,
catalog is null ? OpinionLabels.BandId(null, opinion) : OpinionLabels.Label(catalog, locale, opinion));
}