Campaign FA slice FA1: pure parse functions + typed records only, UNWIRED (FA2 registers them against the new RuntimeFellowshipState/ RuntimeAllegianceState owners -- see docs/research/2026-08-11-fa-acdream-seams.md §2). Fellowship family (GameEvents.cs), field orders from lane B §3.8-§3.13, guid-first on 0x02C0 per the resolved Chorizite disagreement: FellowshipFullUpdate (0x02BE), FellowshipUpdateFellow (0x02C0), FellowshipQuitNotice/FellowshipDismissNotice (S->C 0x00A3/0x00A4), FellowshipDisband (0x02BF, empty body), and the dead FellowshipFellowUpdateDone/FellowshipFellowStatsDone (0x01C9/0x01CA, parse-and-ignore, must never fail per lane B §2.7). ShareLoot is modeled as a raw uint (D5) -- ACE encodes it two incompatible ways (0x10 in full updates, <<1 incremental), so `!= 0` is the only safe read, never `== 1`. Confirmation triple (D6): grepping the tree showed 0x0274/0x0276 already had typed parsers in Core.Net; 0x0275 (client-authored) already had a byte-correct builder but no typed representation. Added the ConfirmationType enum (1 SwearAllegiance, 4 Fellowship, matching retail's Handle_Character__ConfirmationRequest switch and ACE's enum verbatim) and ParseConfirmationResponse, completing Core.Net's typed coverage of all three legs and round-tripping against the existing ClientCommandRequests.BuildConfirmationResponse byte-for-byte. Allegiance small events (GameEvents.cs): AllegianceLoginNotification (0x027A), AllegianceUpdateDone (0x01C8), AllegianceUpdateAborted (0x0003, declared but never sent by ACE). The heavyweight AllegianceUpdate (0x0020) extends ClientCommandResponses.ParseAllegianceInfoResponse (0x027C) rather than a second parser, per lane C §7.2's explicit reuse verdict -- both messages now share ReadAllegianceProfileBody, which the discriminating leading u32 (targetGuid vs rank) is read around. That shared reader implements: - The ELEVEN AllegianceHierarchy::UnPack version gates (lane C §4.2) -- officers/spokesperson-skip, officer titles, the four broadcast counters, motd/motdSetBy, chatRoomId, bind point, allegianceName, isLocked, approvedVassal, each behind its own oldVersion threshold. AllegianceProfileVersionGateTests.cs pins all eleven with a boundary-crossing pair per gate (N-1 OFF vs N ON), including the negative proof that version 5 (BannedCharactersAdded) gates nothing in UnPack. - The §4.4 tree-assembly rules: a record whose treeParent is not already in the tree (orphan), equals its own id (self-parent), or duplicates an id already seen makes AllegianceHierarchy::Add fail, which the whole parse now mirrors by returning null for the ENTIRE message -- not a partial tree. Sibling order REVERSES on assembly (each new record is prepended to its parent's vassal list), so FindVassals now walks records in reverse wire order; both rules have dedicated tests. - AllegianceMemberRecord gained the panel-needed columns lane C §7.2 names (rank, level, loyalty, leadership, cpCached, cpTithed, gender, heritage, MayPassupExperience) with defaulted trailing parameters so existing 4-arg positional construction sites keep compiling. Officers/ officer titles/bind point are read (so every later field lands at the right offset) but deliberately left unsurfaced -- ACE always zeroes/ empties them anyway (lane C §5.1), and bind point is a 32-byte Position the retail chat renderer never uses either; a future panel slice can extend the record without re-deriving the parse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.5 KiB
C#
74 lines
2.5 KiB
C#
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Campaign FA slice FA1 (2026-08-11): the small fixed-shape allegiance
|
|
/// S→C events (everything except <c>0x0020 AllegianceUpdate</c> and
|
|
/// <c>0x027C AllegianceInfoResponse</c>, which reuse the extended
|
|
/// <c>ClientCommandResponses</c> profile reader — see
|
|
/// <c>AllegianceProfileVersionGateTests</c>). Field orders per
|
|
/// docs/research/2026-08-11-fa-allegiance-wire.md (lane C) §4.5. Unwired —
|
|
/// FA2 registers these against RuntimeAllegianceState.
|
|
/// </summary>
|
|
public sealed class AllegianceSmallEventsTests
|
|
{
|
|
[Fact]
|
|
public void ParseAllegianceLoginNotification_RoundTrips_LoggedIn()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0x50000042u).Write((uint)1).ToArray();
|
|
|
|
var notice = GameEvents.ParseAllegianceLoginNotification(wire);
|
|
|
|
Assert.NotNull(notice);
|
|
Assert.Equal(0x50000042u, notice.Value.CharacterGuid);
|
|
Assert.True(notice.Value.IsLoggedIn);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceLoginNotification_RoundTrips_LoggedOut()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0x50000042u).Write((uint)0).ToArray();
|
|
|
|
var notice = GameEvents.ParseAllegianceLoginNotification(wire);
|
|
|
|
Assert.NotNull(notice);
|
|
Assert.False(notice.Value.IsLoggedIn);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceLoginNotification_TruncatedPayload_ReturnsNull()
|
|
{
|
|
Assert.Null(GameEvents.ParseAllegianceLoginNotification(new byte[4]));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceUpdateDone_ReadsWeenieError()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0u).ToArray();
|
|
Assert.Equal(0u, GameEvents.ParseAllegianceUpdateDone(wire));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceUpdateDone_NonZeroErrorCode_RoundTrips()
|
|
{
|
|
byte[] wire = new AceWireWriter().Write(0x40Bu).ToArray(); // AlreadySworn-style code
|
|
Assert.Equal(0x40Bu, GameEvents.ParseAllegianceUpdateDone(wire));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceUpdateAborted_ReadsWeenieError()
|
|
{
|
|
// Never actually sent by ACE (lane C §5.4) — parsed for
|
|
// forward-compat/completeness only.
|
|
byte[] wire = new AceWireWriter().Write(0x40Cu).ToArray();
|
|
Assert.Equal(0x40Cu, GameEvents.ParseAllegianceUpdateAborted(wire));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseAllegianceUpdateDone_TruncatedPayload_ReturnsNull()
|
|
{
|
|
Assert.Null(GameEvents.ParseAllegianceUpdateDone(System.ReadOnlySpan<byte>.Empty));
|
|
}
|
|
}
|