Update wire protocol to version 7 and enhance presence management features
ci / server (push) Failing after 3m44s
ci / client (push) Successful in 15s

- Bumped the wire protocol version to 7, reflecting significant changes in the communication structure.
- Introduced a new presence message type for real-time occupancy updates, including node activity and individual presence states.
- Updated the API to include a directory endpoint for fetching short id→name mappings, improving client-side name resolution.
- Revised the map snapshot structure to be static, with people and current lessons now handled through the presence stream.
- Enhanced client-side handling of presence updates, including UI adjustments to display live occupancy and activity.
- Updated documentation to reflect the new protocol features and changes in presence management.
- Added tests to validate the new presence functionalities and ensure robust handling of real-time data.
This commit is contained in:
Leonid Pershin
2026-08-19 17:00:53 +03:00
parent 4137400621
commit 3c54f981b7
31 changed files with 1025 additions and 373 deletions
+98
View File
@@ -0,0 +1,98 @@
using HSchool.Content;
using HSchool.People;
using HSchool.Protocol;
using HSchool.Schedule;
using HSchool.Simulation;
namespace HSchool.Server.Game;
/// <summary>
/// Builds a presence frame from stored places plus the current lesson. Occupancy is state;
/// the lesson labels are still derived from the timetable so an empty classroom during a period
/// still shows what should be happening there.
/// </summary>
internal static class PresenceFrame
{
public static ServerPresenceMessage Build(School school, int weekDays, string locale)
{
var people = new List<PresencePerson>();
var counts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var row in school.CapturePresence())
{
if (row.NodeId is null)
{
continue;
}
var walking = row.Path.Count > 0 || row.RemainingMinutes > 0;
people.Add(new PresencePerson(
row.PersonId,
row.NodeId,
walking ? PresenceState.Walking : PresenceState.Here));
counts[row.NodeId] = counts.GetValueOrDefault(row.NodeId) + 1;
}
people.Sort((left, right) => StringComparer.Ordinal.Compare(left.Id, right.Id));
var activity = ActivityByRoom(school, weekDays, locale);
var nodeIds = counts.Keys
.Concat(activity.Keys)
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal);
var nodes = new List<PresenceNode>();
foreach (var id in nodeIds)
{
var count = (ushort)Math.Min(counts.GetValueOrDefault(id), ushort.MaxValue);
var subject = "";
var classLabel = "";
if (activity.TryGetValue(id, out var labels))
{
subject = labels.Subject;
classLabel = labels.Class;
}
nodes.Add(new PresenceNode(id, count, subject, classLabel));
}
return new ServerPresenceMessage(school.Id, nodes, people);
}
private static Dictionary<string, (string Subject, string Class)> ActivityByRoom(
School school,
int weekDays,
string locale)
{
var labels = new Dictionary<string, (string Subject, string Class)>(StringComparer.Ordinal);
if (school.Timetable is null || school.Roster is null || school.Catalog is null)
{
return labels;
}
var occurring = TimetableClock.OccurringAt(
school.Timetable,
school.Catalog,
school.Clock.Time,
weekDays);
if (occurring.Count == 0)
{
return labels;
}
var catalog = school.Catalog;
var classes = school.Roster.Classes.ToDictionary(item => item.Id, StringComparer.Ordinal);
foreach (var lesson in occurring)
{
classes.TryGetValue(lesson.ClassId, out var schoolClass);
var subjectLabel = catalog.Subjects.TryGetValue(lesson.Subject, out var subject)
? catalog.Label(locale, subject)
: lesson.Subject;
var classLabel = schoolClass is null
? lesson.ClassId
: $"{schoolClass.Year}{schoolClass.Letter}";
labels[lesson.RoomId] = (subjectLabel, classLabel);
}
return labels;
}
}