Merge branch 'phase/45-presence-talk'

# Conflicts:
#	docs/phases/README.md
#	src/HSchool.Server/Api/PeopleModels.cs
#	src/HSchool.Server/Game/PersonCardReader.cs
This commit is contained in:
Leonid Pershin
2026-08-20 11:27:58 +03:00
28 changed files with 679 additions and 41 deletions
@@ -246,7 +246,120 @@ public class ProtocolCodecTests
Assert.Equal("f0.c0", read.People[0].Id);
Assert.Equal("classroom-101", read.People[0].NodeId);
Assert.Equal(PresenceState.Here, read.People[0].State);
Assert.Empty(read.People[0].TalkMemberIds);
Assert.Equal("", read.People[0].TalkTopicId);
Assert.Equal(PresenceState.Walking, read.People[1].State);
Assert.Empty(read.People[1].TalkMemberIds);
}
[Fact]
public void Presence_PersonWithoutCircle_WritesEmptyMemberListAndEmptyTopic()
{
var message = new ServerPresenceMessage(
1,
[],
[new PresencePerson("f0.c0", "corridor-1", PresenceState.Here)]);
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
var length = ProtocolCodec.WritePresence(buffer, message);
Assert.Equal((byte)MessageType.ServerPresence, buffer[0]);
Assert.Equal(1, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
Assert.Equal((ushort)0, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(7, 2)));
var offset = 9;
offset = AssertWireString(buffer, offset, "f0.c0");
offset = AssertWireString(buffer, offset, "corridor-1");
Assert.Equal(PresenceState.Here, buffer[offset]);
offset += 1;
Assert.Equal(0, buffer[offset]);
offset += 1;
offset = AssertWireString(buffer, offset, "");
Assert.Equal(length, offset);
Assert.Equal(length, ProtocolCodec.PresenceSize(message));
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
Assert.Empty(read.People[0].TalkMemberIds);
Assert.Equal("", read.People[0].TalkTopicId);
Assert.Equal(message.People[0].Id, read.People[0].Id);
Assert.Equal(message.People[0].NodeId, read.People[0].NodeId);
Assert.Equal(message.People[0].State, read.People[0].State);
}
[Fact]
public void Presence_PersonWithCircle_RoundTripsMemberIdsAndTopic()
{
var members = new[] { "f0.c0", "f0.c1" };
var message = new ServerPresenceMessage(
1,
[new PresenceNode("corridor-1", 2)],
[new PresencePerson("f0.c0", "corridor-1", PresenceState.Here, members, "TopicSport")]);
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
var length = ProtocolCodec.WritePresence(buffer, message);
Assert.Equal((byte)MessageType.ServerPresence, buffer[0]);
Assert.Equal(1, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
var offset = 7;
offset = AssertWireString(buffer, offset, "corridor-1");
Assert.Equal((ushort)2, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
offset += 2;
Assert.Equal(0, buffer[offset]);
offset += 1;
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
offset += 2;
offset = AssertWireString(buffer, offset, "f0.c0");
offset = AssertWireString(buffer, offset, "corridor-1");
Assert.Equal(PresenceState.Here, buffer[offset]);
offset += 1;
Assert.Equal(2, buffer[offset]);
offset += 1;
offset = AssertWireString(buffer, offset, "f0.c0");
offset = AssertWireString(buffer, offset, "f0.c1");
offset = AssertWireString(buffer, offset, "TopicSport");
Assert.Equal(length, offset);
Assert.Equal(length, ProtocolCodec.PresenceSize(message));
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
Assert.Equal(["f0.c0", "f0.c1"], read.People[0].TalkMemberIds);
Assert.Equal("TopicSport", read.People[0].TalkTopicId);
Assert.Equal(message.SchoolId, read.SchoolId);
Assert.Equal(message.People[0].Id, read.People[0].Id);
}
[Fact]
public void Presence_DoesNotContainPersonDisplayName()
{
var message = new ServerPresenceMessage(
1,
[],
[new PresencePerson(
"f0.c0",
"corridor-1",
PresenceState.Here,
["f0.c0", "f0.c1"],
"TopicSport")]);
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
ProtocolCodec.WritePresence(buffer, message);
var text = System.Text.Encoding.UTF8.GetString(buffer);
Assert.DoesNotContain("Мария", text);
Assert.DoesNotContain("Иванова", text);
Assert.DoesNotContain("Маша", text);
Assert.Contains("f0.c0", text);
Assert.Contains("TopicSport", text);
}
private static int AssertWireString(byte[] buffer, int offset, string expected)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(expected);
Assert.Equal((ushort)bytes.Length, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
Assert.Equal(bytes, buffer.AsSpan(offset + 2, bytes.Length).ToArray());
return offset + 2 + bytes.Length;
}
[Fact]
@@ -26,6 +26,13 @@ public class TalkCircleTests
var rows = school.CapturePresence();
Assert.Equal(TalkActions.Chat, rows.Single(row => row.PersonId == first.Id).ActionId);
Assert.Equal(TalkActions.Chat, rows.Single(row => row.PersonId == second.Id).ActionId);
var circle = school.TalkCircleOf(first.Id);
Assert.NotNull(circle);
Assert.Contains(first.Id, circle.MemberIds);
Assert.Contains(second.Id, circle.MemberIds);
Assert.False(string.IsNullOrEmpty(circle.TopicId));
Assert.Equal(circle.MemberIds, school.TalkCircleOf(second.Id)?.MemberIds);
Assert.Null(school.TalkCircleOf("missing"));
}
}