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
+38 -13
View File
@@ -31,12 +31,16 @@ public readonly record struct ServerPongMessage(long ClientTimeMs, uint ServerTi
/// State of the open school's calendar, sent every tick.
/// <paramref name="GameTimeUnixMs"/> is the in-game date as milliseconds since the Unix epoch,
/// interpreted as UTC — the game calendar has no time zone.
/// <paramref name="SkipAllowed"/> is the server's verdict; the client must not recompute it.
/// <paramref name="SkipTargetUnixMs"/> is 0 when skip is refused.
/// </summary>
public readonly record struct ServerClockMessage(
int SchoolId,
long GameTimeUnixMs,
bool Running,
byte SpeedIndex);
byte SpeedIndex,
bool SkipAllowed = false,
long SkipTargetUnixMs = 0);
/// <summary>The open school no longer exists (deleted from another tab); the client returns to the menu.</summary>
public readonly record struct ServerSchoolGoneMessage(int SchoolId);
@@ -45,9 +49,7 @@ public readonly record struct ServerSchoolGoneMessage(int SchoolId);
/// Tree node in a map snapshot. Kind is <c>0</c> territory, <c>1</c> building, <c>2</c> floor, <c>3</c> room.
/// <paramref name="ParentId"/> is empty for the yard.
/// <paramref name="PupilSlots"/> is how many pupils can take a lesson here — summed from things
/// on the server, not by the client.
/// <paramref name="ActivitySubject"/> and <paramref name="ActivityClass"/> are empty when the
/// room is free. <paramref name="Characters"/> are the people the timetable puts there right now.
/// on the server, not by the client. Live occupancy lives on <see cref="ServerPresenceMessage"/>.
/// </summary>
public sealed record MapSnapshotNode(
byte Kind,
@@ -56,19 +58,42 @@ public sealed record MapSnapshotNode(
string Name,
ushort PupilSlots,
IReadOnlyList<MapSnapshotItem> Items,
IReadOnlyList<string> Positions,
string ActivitySubject = "",
string ActivityClass = "",
IReadOnlyList<string>? Characters = null)
{
public IReadOnlyList<string> Present => Characters ?? [];
}
IReadOnlyList<string> Positions);
/// <summary>One stacked thing in a room. <paramref name="Count"/> is 1255.</summary>
public sealed record MapSnapshotItem(string Name, byte Count);
/// <summary>
/// One school's map, labelled in the Hello locale. Sent when that school is opened and again
/// when the current lesson slot changes. Occupancy is computed from the timetable and the clock.
/// One school's map, labelled in the Hello locale. Sent once when that school is opened.
/// Structure only — people and the current lesson ride the presence stream.
/// </summary>
public sealed record ServerMapSnapshotMessage(int SchoolId, IReadOnlyList<MapSnapshotNode> Nodes);
/// <summary>Jump empty nights, weekends and holidays. The server re-checks the conditions.</summary>
public readonly record struct ClientSkipEmptyMessage;
/// <summary>Where people are: 1 in a node, 2 walking through it. Off campus is omitted.</summary>
public static class PresenceState
{
public const byte Here = 1;
public const byte Walking = 2;
}
/// <summary>One occupied (or currently taught) map node in a presence frame.</summary>
public sealed record PresenceNode(
string Id,
ushort Count,
string ActivitySubject = "",
string ActivityClass = "");
/// <summary>One on-campus person. Names are resolved over HTTP, not on this frame.</summary>
public sealed record PresencePerson(string Id, string NodeId, byte State);
/// <summary>
/// Live occupancy of an open school, about twice a second. Counts and people cover the whole
/// map; the client filters to the selected tree node.
/// </summary>
public sealed record ServerPresenceMessage(
int SchoolId,
IReadOnlyList<PresenceNode> Nodes,
IReadOnlyList<PresencePerson> People);