feat(runtime): FA4 -- fellowship roster enumeration + Social selection source

IRuntimeFellowshipView.GetMembers() gives the fellowship panel a way to
enumerate the whole roster (TryGetMember alone needs the guid first,
which a UI roster build doesn't have yet). Implemented on
RuntimeFellowshipState.FellowshipView as a materialized snapshot under
the same lock every other read there uses.

SelectionChangeSource.Social covers a fellowship-roster row click
(gmFellowshipUI's list-selection arm calls the same
ACCWeenieObject::SetSelectedObject primitive every other selection
origin uses -- lane B docs/research/2026-08-11-fa-panel-structure.md
§6.2/§2.8).

Runtime tests: +4 (RuntimeFellowshipStateTests.GetMembers_*).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 04:40:19 +02:00
parent afb3223c9b
commit 357d203202
4 changed files with 88 additions and 0 deletions

View file

@ -482,6 +482,58 @@ public sealed class RuntimeFellowshipStateTests
Assert.Equal(42u, self.CurrentHealth);
}
// ── Campaign FA slice FA4: GetMembers (the fellowship panel's roster
// enumeration — TryGetMember alone cannot build a roster without the
// guid first). ────────────────────────────────────────────────────────
[Fact]
public void GetMembers_ReturnsEveryCurrentMember()
{
var state = new RuntimeFellowshipState();
state.ApplyFullUpdate(FullUpdate(
Member(SelfGuid, "Self"), Member(LeaderGuid, "Leader"), Member(OtherGuid, "Other")));
var guids = state.View.GetMembers().Select(m => m.Guid).ToHashSet();
Assert.Equal(3, guids.Count);
Assert.Contains(SelfGuid, guids);
Assert.Contains(LeaderGuid, guids);
Assert.Contains(OtherGuid, guids);
}
[Fact]
public void GetMembers_EmptyWhenNotInAFellowship()
{
var state = new RuntimeFellowshipState();
Assert.Empty(state.View.GetMembers());
}
[Fact]
public void GetMembers_ReflectsAnIncrementalUpsert()
{
var state = new RuntimeFellowshipState();
state.ApplyFullUpdate(FullUpdate(Member(SelfGuid), Member(LeaderGuid)));
state.ApplyUpdateFellow(new GameEvents.FellowshipUpdateFellow(
OtherGuid, Member(OtherGuid, "Other"), UpdateType: 1u));
Assert.Equal(3, state.View.GetMembers().Count());
Assert.Contains(state.View.GetMembers(), m => m.Guid == OtherGuid && m.Name == "Other");
}
[Fact]
public void GetMembers_ReflectsRemovalAfterDismiss()
{
var state = new RuntimeFellowshipState();
state.ApplyFullUpdate(FullUpdate(Member(SelfGuid), Member(LeaderGuid), Member(OtherGuid)));
state.ApplyDismiss(OtherGuid, SelfGuid);
Assert.DoesNotContain(state.View.GetMembers(), m => m.Guid == OtherGuid);
Assert.Equal(2, state.View.GetMembers().Count());
}
private sealed class ManualTimeProvider : TimeProvider
{
private DateTimeOffset _now = new(2026, 8, 12, 0, 0, 0, TimeSpan.Zero);