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>
80 lines
3.4 KiB
C#
80 lines
3.4 KiB
C#
using System.Buffers.Binary;
|
|
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
/// <summary>
|
|
/// Campaign FA slice FA1 (2026-08-11), D6: completes Core.Net's typed
|
|
/// representation of the shared confirmation triple
|
|
/// (<c>0x0274</c>/<c>0x0275</c>/<c>0x0276</c>). Grepping the tree before
|
|
/// this slice showed <c>0x0274</c> (<see cref="GameEvents.ParseCharacterConfirmationRequest"/>)
|
|
/// and <c>0x0276</c> (<see cref="GameEvents.ParseCharacterConfirmationDone"/>)
|
|
/// already had typed record + parser pairs; <c>0x0275</c> — the CLIENT→
|
|
/// SERVER leg — already had a byte-correct builder
|
|
/// (<c>ClientCommandRequests.BuildConfirmationResponse</c>, lane C §3.3)
|
|
/// but no typed record, no <see cref="GameEvents.ConfirmationType"/>
|
|
/// enum, and no parser. This file pins the completed triple: the new
|
|
/// <see cref="GameEvents.ConfirmationType"/> enum (1 = SwearAllegiance,
|
|
/// 4 = Fellowship — D6), and <see cref="GameEvents.ParseConfirmationResponse"/>
|
|
/// round-tripping against the EXISTING builder byte-for-byte.
|
|
/// </summary>
|
|
public sealed class ConfirmationTripleTests
|
|
{
|
|
[Fact]
|
|
public void ConfirmationType_SwearAllegianceIsOne_FellowshipIsFour()
|
|
{
|
|
// Handle_Character__ConfirmationRequest @0x005640A0 switch vs ACE
|
|
// ConfirmationType.cs:5-12 — identical (lane B §3.15).
|
|
Assert.Equal(1u, (uint)GameEvents.ConfirmationType.SwearAllegiance);
|
|
Assert.Equal(4u, (uint)GameEvents.ConfirmationType.Fellowship);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseConfirmationResponse_RoundTripsAgainstExistingBuilder()
|
|
{
|
|
byte[] wire = ClientCommandRequests.BuildConfirmationResponse(
|
|
sequence: 5,
|
|
confirmationType: (uint)GameEvents.ConfirmationType.Fellowship,
|
|
contextId: 0x1234u,
|
|
accepted: true);
|
|
|
|
// Strip the 12-byte envelope/seq/opcode header the same way every
|
|
// other GameEvents.Parse* function receives its payload (header
|
|
// already stripped by the dispatcher) — here we strip the GameACTION
|
|
// header by hand since BuildConfirmationResponse is a C→S builder.
|
|
var response = GameEvents.ParseConfirmationResponse(wire.AsSpan(12));
|
|
|
|
Assert.NotNull(response);
|
|
Assert.Equal(GameEvents.ConfirmationType.Fellowship, response.Value.Type);
|
|
Assert.Equal(0x1234u, response.Value.ContextId);
|
|
Assert.True(response.Value.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseConfirmationResponse_GoldenByteVector_SwearAllegianceDeclined()
|
|
{
|
|
// Hand-computed from CM_Character::Event_ConfirmationResponse
|
|
// @0x006A1210 (lane B §3.15 / lane C §3.3):
|
|
// [u32 confirmType][u32 context][u32 accepted].
|
|
byte[] payload =
|
|
[
|
|
0x01, 0x00, 0x00, 0x00, // confirmType = 1 (SwearAllegiance)
|
|
0x99, 0x00, 0x00, 0x00, // context = 0x99
|
|
0x00, 0x00, 0x00, 0x00, // accepted = 0 (declined)
|
|
];
|
|
|
|
var response = GameEvents.ParseConfirmationResponse(payload);
|
|
|
|
Assert.NotNull(response);
|
|
Assert.Equal(GameEvents.ConfirmationType.SwearAllegiance, response.Value.Type);
|
|
Assert.Equal(0x99u, response.Value.ContextId);
|
|
Assert.False(response.Value.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseConfirmationResponse_TruncatedPayload_ReturnsNull()
|
|
{
|
|
Assert.Null(GameEvents.ParseConfirmationResponse(new byte[8]));
|
|
}
|
|
}
|