Applies both MUST-FIX items and the code-facing SHOULD-FIX items from the dual-lens FA1 review (docs/research/2026-08-12-fa1-review-mechanism.md, docs/research/2026-08-12-fa1-review-blast.md): Mechanism MF-1 / blast SF-2: AllegianceHierarchy::Add @0x005B6E90 wraps its entire body in `if (_id != 0)` -- a record whose own id is zero discards the WHOLE message, for both the monarch and a child record, and this is also what makes treeParent == 0 unconditionally fatal for a non-monarch record. ReadAllegianceProfileBody now rejects CharacterId == 0 on both paths; four new boundary tests in AllegianceProfileVersionGateTests.cs (zero-id monarch, zero-id child, zero treeParent, plus the existing orphan/self-parent/duplicate trio). Mechanism MF-2: added the missing 0x001F AllegianceUpdateRequest builder -- the structural twin of the fellowship 0x00A6 this slice already repaired -- with golden-vector tests for both on/off. Mechanism SF-1 / blast SF-3: UnPack's last act before returning success forces the monarch's MayPassupExperience to false regardless of the wire bit or the HasPackedLevel-absent legacy-compat fallback. Ported at the end of the record loop; the pre-existing HasPackedLevel-absent test moved off the monarch record (which the new clear makes indistinguishable from "the fallback never fired") onto a vassal record, and a new test proves the monarch clear fires even when the wire bit explicitly asks for true. Mechanism SF-2: removed ParseFellowshipDisband's invented body-length validation -- retail's DispatchUI_Disband reads only the opcode and never inspects a trailing body. The parser now always succeeds; the matching test flips from asserting rejection to asserting acceptance. Mechanism SF-3: added the D5 `<<1` shareLoot-shape test at the 0x02C0 FellowshipUpdateFellow site -- previously only pinned at 0x02BE, so a future split of the shared ReadFellow helper could silently reintroduce a bool read on this leg undetected. Mechanism SF-5: renumbered the version-gate comments in ReadAllegianceProfileBody to the true AllegianceVersion enum values (1-11, matching acclient.h's SpokespersonAdded..ApprovedVassal) instead of wire-appearance order, which only reached 10 and silently dropped gate 5 (BannedCharactersAdded, which is real but gates nothing in UnPack -- now called out explicitly). Fixed the stale "lane B §12" citation in SocialActions.cs to the actual master-table row. Blast SF-1: pinned the two retail-faithful but user-visible behavior changes FA1 made to the ALREADY-LIVE `@allegiance info` command -- reversed vassal print order (3-vassal test through FormatAllegianceInfoLines) and malformed-tree silent-drop (test at the GameEventWiring registration layer, which is `if (info is null) return;`). Blast SF-4: fixed a doc comment citing a nonexistent `ConfirmationResponseTests` class; the actual class is `ConfirmationTripleTests`. Blast SF-5: cross-referenced the confirmation-triple discriminator's split representation (ConfirmationType on the response leg only; bare uint on the two inbound legs production actually reads) at both sites, so FA4 inherits a stated decision rather than an unexplained inconsistency. Full Release suite: 13,158 passed / 4 skipped / 0 failed (13,162 total), up from the pre-fix-round 13,149/4/0 (+9 tests this round). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
4.4 KiB
C#
128 lines
4.4 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using AcDream.Core.Net.Messages;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests.Messages;
|
|
|
|
public sealed class AllegianceRequestsTests
|
|
{
|
|
[Fact]
|
|
public void BuildSwear_EncodesOpcodeAndTarget()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildSwear(gameActionSequence: 3, patronGuid: 0xAAAAu);
|
|
|
|
Assert.Equal(16, body.Length);
|
|
Assert.Equal(AllegianceRequests.GameActionEnvelope,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body));
|
|
Assert.Equal(3u,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
|
|
Assert.Equal(AllegianceRequests.SwearOpcode,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
Assert.Equal(0xAAAAu,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBreak_EncodesOpcodeAndTarget()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildBreak(gameActionSequence: 5, targetGuid: 0xBBBBu);
|
|
Assert.Equal(AllegianceRequests.BreakOpcode,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
Assert.Equal(0xBBBBu,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
|
}
|
|
|
|
// FA1: golden byte vectors, hand-computed from lane C §3.2
|
|
// (`CM_Allegiance::Event_SwearAllegiance`/`Event_BreakAllegiance`),
|
|
// not generated by calling the builder under test.
|
|
[Fact]
|
|
public void BuildSwear_GoldenByteVector()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildSwear(gameActionSequence: 3, patronGuid: 0xAAAAu);
|
|
|
|
byte[] expected =
|
|
[
|
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
|
0x03, 0x00, 0x00, 0x00, // seq 3
|
|
0x1D, 0x00, 0x00, 0x00, // opcode 0x001D
|
|
0xAA, 0xAA, 0x00, 0x00, // targetGuid 0xAAAA
|
|
];
|
|
|
|
Assert.Equal(expected, body);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildBreak_GoldenByteVector()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildBreak(gameActionSequence: 5, targetGuid: 0xBBBBu);
|
|
|
|
byte[] expected =
|
|
[
|
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
|
0x05, 0x00, 0x00, 0x00, // seq 5
|
|
0x1E, 0x00, 0x00, 0x00, // opcode 0x001E
|
|
0xBB, 0xBB, 0x00, 0x00, // targetGuid 0xBBBB
|
|
];
|
|
|
|
Assert.Equal(expected, body);
|
|
}
|
|
|
|
// New builder — BuildKick is BuildBreak's opcode with a vassal target
|
|
// (lane C §1.4). Same golden vector shape as BuildBreak, proving the
|
|
// wire is identical while the call site reads distinctly for FA2.
|
|
[Fact]
|
|
public void BuildKick_GoldenByteVector_SameShapeAsBreak()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildKick(gameActionSequence: 6, vassalGuid: 0x50000042u);
|
|
|
|
byte[] expected =
|
|
[
|
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
|
0x06, 0x00, 0x00, 0x00, // seq 6
|
|
0x1E, 0x00, 0x00, 0x00, // opcode 0x001E — SAME as Break
|
|
0x42, 0x00, 0x00, 0x50, // vassalGuid 0x50000042
|
|
];
|
|
|
|
Assert.Equal(expected, body);
|
|
Assert.Equal(AllegianceRequests.BreakOpcode,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
|
}
|
|
|
|
// FA1 review round (mechanism MF-2): the missing 0x001F
|
|
// AllegianceUpdateRequest builder — the structural twin of the
|
|
// fellowship 0x00A6 this slice already repaired. Golden vector
|
|
// hand-derived from lane C §3.2: [u32 0xF7B1][u32 seq][u32 0x1F]
|
|
// [u32 on], total 0x10.
|
|
[Fact]
|
|
public void BuildAllegianceUpdateRequest_GoldenByteVector_On()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildAllegianceUpdateRequest(gameActionSequence: 4, on: true);
|
|
|
|
byte[] expected =
|
|
[
|
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
|
0x04, 0x00, 0x00, 0x00, // seq 4
|
|
0x1F, 0x00, 0x00, 0x00, // opcode 0x001F
|
|
0x01, 0x00, 0x00, 0x00, // on = 1
|
|
];
|
|
|
|
Assert.Equal(expected, body);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildAllegianceUpdateRequest_GoldenByteVector_Off()
|
|
{
|
|
byte[] body = AllegianceRequests.BuildAllegianceUpdateRequest(gameActionSequence: 7, on: false);
|
|
|
|
byte[] expected =
|
|
[
|
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
|
0x07, 0x00, 0x00, 0x00, // seq 7
|
|
0x1F, 0x00, 0x00, 0x00, // opcode 0x001F
|
|
0x00, 0x00, 0x00, 0x00, // on = 0
|
|
];
|
|
|
|
Assert.Equal(expected, body);
|
|
}
|
|
}
|