feat(runtime): FA2 -- RuntimeFellowshipState + RuntimeAllegianceState sibling J-owners

Two new sibling Runtime owners under GameRuntime per the Slice-J
pattern (D2): RuntimeFellowshipState is session-scoped (a new
RuntimeGenerationResetStage.Fellowship clears it at every generation
reset, matching the ExternalContainer precedent); RuntimeAllegianceState
survives reconnect behind a HasServerSeed-style one-way latch and
participates in NO reset stage (its data persists like a real
disconnect does not sever allegiance membership).

Fellowship: full-update REPLACE, incremental-fellow UPSERT, self-vs-
other quit/dismiss removal (self clears the whole snapshot), disband
clear, and retail's leader hand-off rule for the Quit button
(RequiresLeaderHandoffBeforeQuit -- the current leader quitting WITHOUT
disbanding must send 0x0290 AssignNewLeader before 0x00A3, lane B
§2.5/§3.6).

Allegiance: seeded by AllegianceUpdate (0x0020, always self) and,
self-gated on TargetGuid == playerGuid(), by AllegianceInfoResponse
(0x027C); wraps the FA1-assembled flat AllegianceMemberRecord list
directly (AllegianceTree was deleted at FA1 -- nothing left to wrap).

Both apply the full 8-edit J-owner template: construction + fault
points + Owner/View properties + CaptureOwnership + a new
GameRuntimeTeardownStage pair (FellowshipDisposed/AllegianceDisposed,
stage count 11->13) + RuntimeGameplayOwnershipSnapshot inclusion +
RuntimeStateCheckpoint/trace fields. IRuntimeFellowshipCommands/
IRuntimeAllegianceCommands added to IGameRuntimeCommands and
implemented on DirectGameRuntimeCommandAdapter. No IRuntimeEventObserver
member added (D2) -- consumers poll Snapshot.Revision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 01:31:54 +02:00
parent 1c40104896
commit 369729f06a
19 changed files with 1949 additions and 35 deletions

View file

@ -108,3 +108,87 @@ public interface IRuntimeSocialView
bool TryGetFriend(uint characterId, out RuntimeFriendSnapshot friend);
}
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────────
// D2: two sibling J-owners, session-scoped Fellowship vs reconnect-surviving
// Allegiance (docs/research/2026-08-11-fa-acdream-seams.md §1.3). Consumers
// poll Snapshot.Revision — no IRuntimeEventObserver member is added (would
// break all 5 bot policies + the trace recorder, per the same doc §1.3).
public readonly record struct RuntimeFellowMemberSnapshot(
uint Guid,
string Name,
uint Level,
uint MaxHealth,
uint MaxStamina,
uint MaxMana,
uint CurrentHealth,
uint CurrentStamina,
uint CurrentMana,
bool ShareLoot);
public readonly record struct RuntimeFellowshipSnapshot(
long Revision,
bool IsInFellowship,
string Name,
uint LeaderGuid,
bool ShareXp,
bool EvenXpSplit,
bool IsOpen,
bool Locked,
int MemberCount);
public interface IRuntimeFellowshipView
{
RuntimeFellowshipSnapshot Snapshot { get; }
bool TryGetMember(uint guid, out RuntimeFellowMemberSnapshot member);
}
public readonly record struct RuntimeAllegianceMemberSnapshot(
uint CharacterId,
uint ParentGuid,
bool IsLoggedIn,
string Name,
ushort Rank,
uint Level,
ushort Loyalty,
ushort Leadership,
uint CpCached,
uint CpTithed,
byte Gender,
byte HeritageGroup,
bool MayPassupExperience);
public readonly record struct RuntimeAllegianceSnapshot(
long Revision,
bool HasServerSeed,
bool HasProfile,
uint Rank,
uint TotalMembers,
uint TotalVassals,
string AllegianceName,
uint MonarchGuid,
int RecordCount)
{
public bool HasMonarch => MonarchGuid != 0u;
}
public interface IRuntimeAllegianceView
{
RuntimeAllegianceSnapshot Snapshot { get; }
bool TryGetMonarch(out RuntimeAllegianceMemberSnapshot monarch);
bool TryGetMember(uint guid, out RuntimeAllegianceMemberSnapshot member);
bool TryGetPatron(uint guid, out RuntimeAllegianceMemberSnapshot patron);
/// <summary>
/// Port of retail's <c>GetFirstVassal</c>/<c>GetNextVassal</c> walk —
/// reverse wire order, exactly like <c>AllegianceProfileLookups.
/// FindVassals</c> (lane C §4.4 point 3;
/// <c>ClientCommandResponses.cs:280-294</c>).
/// </summary>
IEnumerable<RuntimeAllegianceMemberSnapshot> GetVassals(uint guid);
}