Update wire protocol to version 6 and enhance timetable functionality
- Bumped the wire protocol version to 6, reflecting changes in the communication structure.
- Expanded the timetable API with new endpoints for fetching and managing lesson schedules, including `GET /api/schools/{id}/timetable` and `POST /api/schools/{id}/timetable/pin`.
- Updated the protocol documentation to include detailed descriptions of the new timetable features and message structures.
- Enhanced the client-side implementation to support the new timetable functionalities, including lesson pinning and unpinning.
- Revised server-side logic to handle timetable operations and ensure proper integration with existing school management features.
- Added tests to validate the new timetable functionalities and ensure robustness in handling lesson data.
This commit is contained in:
@@ -46,6 +46,8 @@ public readonly record struct ServerSchoolGoneMessage(int SchoolId);
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public sealed record MapSnapshotNode(
|
||||
byte Kind,
|
||||
@@ -54,13 +56,19 @@ public sealed record MapSnapshotNode(
|
||||
string Name,
|
||||
ushort PupilSlots,
|
||||
IReadOnlyList<MapSnapshotItem> Items,
|
||||
IReadOnlyList<string> Positions);
|
||||
IReadOnlyList<string> Positions,
|
||||
string ActivitySubject = "",
|
||||
string ActivityClass = "",
|
||||
IReadOnlyList<string>? Characters = null)
|
||||
{
|
||||
public IReadOnlyList<string> Present => Characters ?? [];
|
||||
}
|
||||
|
||||
/// <summary>One stacked thing in a room. <paramref name="Count"/> is 1–255.</summary>
|
||||
public sealed record MapSnapshotItem(string Name, byte Count);
|
||||
|
||||
/// <summary>
|
||||
/// One school's map, labelled in the Hello locale. Sent once when that school is opened, not every tick.
|
||||
/// People and in-place activities are omitted — the client keeps those sections empty.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed record ServerMapSnapshotMessage(int SchoolId, IReadOnlyList<MapSnapshotNode> Nodes);
|
||||
|
||||
@@ -129,6 +129,18 @@ public static class ProtocolCodec
|
||||
{
|
||||
size += StringSize(position);
|
||||
}
|
||||
|
||||
size += sizeof(byte);
|
||||
if (HasActivity(node))
|
||||
{
|
||||
size += StringSize(node.ActivitySubject) + StringSize(node.ActivityClass);
|
||||
}
|
||||
|
||||
size += sizeof(byte);
|
||||
foreach (var person in node.Present)
|
||||
{
|
||||
size += StringSize(person);
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
@@ -148,9 +160,9 @@ public static class ProtocolCodec
|
||||
|
||||
foreach (var node in message.Nodes)
|
||||
{
|
||||
if (node.Items.Count > byte.MaxValue || node.Positions.Count > byte.MaxValue)
|
||||
if (node.Items.Count > byte.MaxValue || node.Positions.Count > byte.MaxValue || node.Present.Count > byte.MaxValue)
|
||||
{
|
||||
throw new ProtocolException($"Map node '{node.Id}' has too many items or positions for a u8 count.");
|
||||
throw new ProtocolException($"Map node '{node.Id}' has too many items, positions or people for a u8 count.");
|
||||
}
|
||||
|
||||
writer.WriteByte(node.Kind);
|
||||
@@ -170,6 +182,23 @@ public static class ProtocolCodec
|
||||
{
|
||||
writer.WriteString(position);
|
||||
}
|
||||
|
||||
if (HasActivity(node))
|
||||
{
|
||||
writer.WriteByte(1);
|
||||
writer.WriteString(node.ActivitySubject);
|
||||
writer.WriteString(node.ActivityClass);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteByte(0);
|
||||
}
|
||||
|
||||
writer.WriteByte((byte)node.Present.Count);
|
||||
foreach (var person in node.Present)
|
||||
{
|
||||
writer.WriteString(person);
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Position;
|
||||
@@ -283,12 +312,41 @@ public static class ProtocolCodec
|
||||
positions[position] = reader.ReadString();
|
||||
}
|
||||
|
||||
nodes[i] = new MapSnapshotNode(kind, id, parentId, name, pupilSlots, items, positions);
|
||||
var hasActivity = reader.ReadByte() != 0;
|
||||
var activitySubject = "";
|
||||
var activityClass = "";
|
||||
if (hasActivity)
|
||||
{
|
||||
activitySubject = reader.ReadString();
|
||||
activityClass = reader.ReadString();
|
||||
}
|
||||
|
||||
var characterCount = reader.ReadByte();
|
||||
var characters = new string[characterCount];
|
||||
for (var person = 0; person < characterCount; person++)
|
||||
{
|
||||
characters[person] = reader.ReadString();
|
||||
}
|
||||
|
||||
nodes[i] = new MapSnapshotNode(
|
||||
kind,
|
||||
id,
|
||||
parentId,
|
||||
name,
|
||||
pupilSlots,
|
||||
items,
|
||||
positions,
|
||||
activitySubject,
|
||||
activityClass,
|
||||
characters);
|
||||
}
|
||||
|
||||
return new ServerMapSnapshotMessage(schoolId, nodes);
|
||||
}
|
||||
|
||||
private static bool HasActivity(MapSnapshotNode node) =>
|
||||
node.ActivitySubject.Length > 0 || node.ActivityClass.Length > 0;
|
||||
|
||||
/// <summary>Matches <see cref="PacketWriter.WriteString"/>: a <c>u16</c> length plus UTF-8.</summary>
|
||||
private static int StringSize(string value) => sizeof(ushort) + Encoding.UTF8.GetByteCount(value);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace HSchool.Protocol;
|
||||
public static class ProtocolConstants
|
||||
{
|
||||
/// <summary>Bumped on every breaking change to the binary layout.</summary>
|
||||
public const byte Version = 5;
|
||||
public const byte Version = 6;
|
||||
|
||||
/// <summary>Upper bound for a single WebSocket frame accepted by the server.</summary>
|
||||
public const int MaxMessageSize = 8 * 1024;
|
||||
|
||||
Reference in New Issue
Block a user