Show director summons as info toasts and presence queue labels.

Corridor waiters need a summon-phase byte on presence (v11); EventDef DirectorSummoned is info-only so clocks keep running.
This commit is contained in:
Leonid Pershin
2026-08-21 13:07:47 +03:00
parent d3c2327e23
commit 9e429d0cd7
29 changed files with 453 additions and 28 deletions
@@ -33,6 +33,15 @@ public class EventDefTests
Assert.Equal(EventActions.GenerateImage, catalog.Events["GenerationFailed"].Action);
Assert.Equal("Начало дня", catalog.Label("ru", catalog.Events["DayStarted"]));
Assert.Equal("The day has started", catalog.Label("en", catalog.Events["DayStarted"]));
Assert.True(catalog.Events.ContainsKey("DirectorSummoned"));
Assert.Equal(EventSeverities.Info, catalog.Events["DirectorSummoned"].Severity);
Assert.False(catalog.Events["DirectorSummoned"].Pause);
Assert.Equal(8000, catalog.Events["DirectorSummoned"].TtlMs);
Assert.Equal(EventTriggers.DirectorSummon, catalog.Events["DirectorSummoned"].Trigger);
Assert.Equal(EventActions.None, catalog.Events["DirectorSummoned"].Action);
Assert.Equal("Ученика вызвали к директору", catalog.Label("ru", catalog.Events["DirectorSummoned"]));
Assert.Equal("A pupil was summoned to the principal", catalog.Label("en", catalog.Events["DirectorSummoned"]));
}
[Fact]
@@ -285,8 +285,10 @@ public class ProtocolCodecTests
Assert.Equal(PresenceState.Here, read.People[0].State);
Assert.Empty(read.People[0].TalkMemberIds);
Assert.Equal("", read.People[0].TalkTopicId);
Assert.Equal(PresenceSummonPhase.None, read.People[0].SummonPhase);
Assert.Equal(PresenceState.Walking, read.People[1].State);
Assert.Empty(read.People[1].TalkMemberIds);
Assert.Equal(PresenceSummonPhase.None, read.People[1].SummonPhase);
}
[Fact]
@@ -313,12 +315,15 @@ public class ProtocolCodecTests
Assert.Equal(0, buffer[offset]);
offset += 1;
offset = AssertWireString(buffer, offset, "");
Assert.Equal(PresenceSummonPhase.None, buffer[offset]);
offset += 1;
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(PresenceSummonPhase.None, read.People[0].SummonPhase);
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);
@@ -357,16 +362,52 @@ public class ProtocolCodecTests
offset = AssertWireString(buffer, offset, "f0.c0");
offset = AssertWireString(buffer, offset, "f0.c1");
offset = AssertWireString(buffer, offset, "TopicSport");
Assert.Equal(PresenceSummonPhase.None, buffer[offset]);
offset += 1;
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(PresenceSummonPhase.None, read.People[0].SummonPhase);
Assert.Equal(message.SchoolId, read.SchoolId);
Assert.Equal(message.People[0].Id, read.People[0].Id);
}
[Fact]
public void Presence_SummonPhase_RoundTripsAndMatchesByteLayout()
{
var message = new ServerPresenceMessage(
1,
[],
[new PresencePerson(
"f0.c0",
"corridor-1",
PresenceState.Here,
[],
"",
PresenceSummonPhase.Waiting)]);
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
var length = ProtocolCodec.WritePresence(buffer, message);
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(PresenceSummonPhase.Waiting, buffer[offset]);
offset += 1;
Assert.Equal(length, offset);
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
Assert.Equal(PresenceSummonPhase.Waiting, Assert.Single(read.People).SummonPhase);
}
[Fact]
public void Presence_DoesNotContainPersonDisplayName()
{
@@ -95,6 +95,18 @@ public class NoticeBoardTests
Assert.Equal("avatar", sticky.Kind);
}
[Fact]
public void DirectorSummonedInfo_IsNotStickyInSave()
{
var board = new NoticeBoard(maxSticky: 8);
Assert.True(board.TryPost(DirectorSummoned(), personKey: "a0.p0", kind: "", out var info));
Assert.Equal("DirectorSummoned", info.DefName);
Assert.False(info.Pause);
Assert.Empty(board.Sticky);
Assert.DoesNotContain(board.ToSave(), row => row.DefName == "DirectorSummoned");
}
private static EventDef Info() => new()
{
DefName = "DayStarted",
@@ -105,6 +117,16 @@ public class NoticeBoardTests
Action = EventActions.None,
};
private static EventDef DirectorSummoned() => new()
{
DefName = "DirectorSummoned",
Severity = EventSeverities.Info,
Pause = false,
TtlMs = 8000,
Trigger = EventTriggers.DirectorSummon,
Action = EventActions.None,
};
private static EventDef Pausing() => new()
{
DefName = "GenerationFailed",
@@ -112,7 +112,16 @@ public class DirectorSummonTests
name => name.Contains("Summon", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
typeof(School).GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public),
method => method.Name.Contains("Summon", StringComparison.OrdinalIgnoreCase));
method => method.Name.StartsWith("Set", StringComparison.Ordinal)
&& method.Name.Contains("Summon", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
typeof(School).GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public),
method => method.Name.StartsWith("Send", StringComparison.Ordinal)
&& method.Name.Contains("Summon", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(
typeof(School).GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public),
method => method.Name.StartsWith("Enqueue", StringComparison.Ordinal)
&& method.Name.Contains("Summon", StringComparison.OrdinalIgnoreCase));
}
[Fact]
@@ -123,6 +132,67 @@ public class DirectorSummonTests
Assert.Equal("corridor-1", DirectorSummons.WaitNodeId(map));
}
[Fact]
public void QualifyingSummon_RaisesDirectorSummonWorldEvent()
{
var (school, first, _, _) = TwoPupilsPrincipalAndTeacherOnBreak();
using (school)
{
school.DrainWorldEvents();
Assert.True(DirectorSummonSystem.TryEnqueue(school, first.Id, chance: 1f));
var facts = school.DrainWorldEvents();
var fact = Assert.Single(facts, row => row.Trigger.Equals(EventTriggers.DirectorSummon, StringComparison.Ordinal));
Assert.Equal(first.Id, fact.PersonKey);
}
}
[Fact]
public void QueueAdvancesWithoutOpenClient()
{
var (school, first, second, _) = TwoPupilsPrincipalAndTeacherOnBreak();
using (school)
{
Assert.True(DirectorSummonSystem.TryEnqueue(school, first.Id, chance: 1f));
Assert.True(DirectorSummonSystem.TryEnqueue(school, second.Id, chance: 1f));
DirectorSummonSystem.Apply(school);
var head = school.DirectorSummons.OrderBy(row => row.Order).First();
var next = school.DirectorSummons.OrderBy(row => row.Order).Skip(1).First();
PlaceAt(school, head.PersonId, "principals-office");
DirectorSummonSystem.Apply(school);
Assert.True(ActivitySystem.IsPrincipalHearing(ActivityOf(school, head.PersonId)));
DirectorSummonSystem.CompleteHearing(school, head.PersonId);
DirectorSummonSystem.Apply(school);
Assert.False(DirectorSummonSystem.IsSummoned(school, head.PersonId));
Assert.True(DirectorSummonSystem.IsSummoned(school, next.PersonId));
PlaceAt(school, next.PersonId, "corridor-1");
Assert.Equal(DirectorSummons.PresenceWaiting, school.DirectorSummonPresencePhase(next.PersonId));
}
}
[Fact]
public void SummonPresencePhase_WaitingInCorridor_HearingInOffice()
{
var (school, first, second, _) = TwoPupilsPrincipalAndTeacherOnBreak();
using (school)
{
Assert.True(DirectorSummonSystem.TryEnqueue(school, first.Id, chance: 1f));
Assert.True(DirectorSummonSystem.TryEnqueue(school, second.Id, chance: 1f));
DirectorSummonSystem.Apply(school);
var ordered = school.DirectorSummons.OrderBy(row => row.Order).ToArray();
PlaceAt(school, ordered[0].PersonId, "principals-office");
PlaceAt(school, ordered[1].PersonId, "corridor-1");
DirectorSummonSystem.Apply(school);
Assert.Equal(DirectorSummons.PresenceHearing, school.DirectorSummonPresencePhase(ordered[0].PersonId));
Assert.Equal(DirectorSummons.PresenceWaiting, school.DirectorSummonPresencePhase(ordered[1].PersonId));
}
}
private static IReadOnlyList<string> QueueOrderFromFight(int seed)
{
var (school, first, second, _) = TwoPupilsPrincipalAndTeacherOnBreak(seed);