fix(net): FA1 -- repair fellowship builders, add 0x0290/0x0291, allegiance Kick

Campaign FA slice FA1 (lane B field-order sections §3.1-§3.7, lane C §3.2,
§1.4). Two latent acdream builder defects repaired, both cited in lane B
§5.2:

- BuildFellowshipCreate (0x00A2) invented a nonexistent "openness" byte
  and silently sent it as the low byte of shareXP -- ACE would read an
  INVERTED shareXP value. Corrected to retail's real shape: [str16L
  name][u32 shareXP]. shareXP is the FellowshipShareXP character option,
  not a dialog checkbox.
- BuildFellowshipUpdate(open:) mislabeled 0x00A6 as fellowship openness;
  it is FellowshipUpdateRequest -- panel VISIBILITY. Renamed to
  BuildFellowshipUpdateRequest(panelOpen:); the wire bytes were already
  correct, only the name/doc were wrong. ACE gates the whole 0x02C0
  member-vitals stream on this message (lane B §4.5) -- a prerequisite
  for live vitals once FA4 wires the panel.

Two builders added that acdream never had at all:

- BuildFellowshipAssignNewLeader (0x0290) -- retail's leader-Quit path
  sends this before 0x00A3 disband=0 (lane B §2.5).
- BuildFellowshipChangeOpenness (0x0291) -- the REAL openness toggle.
- AllegianceRequests.BuildKick -- wire-identical to BuildBreak (both are
  Event_BreakAllegiance 0x001E); named separately so FA2's panel command
  surface can distinguish "break from patron" from "kick a vassal" (lane
  C §1.4). AllegianceInfoRequest (0x027B) was already live via
  ClientCommandRequests.BuildAllegianceInfoRequest -- not duplicated.

Wrong-shape tests at SocialActionsTests.cs:53-105 re-pinned with
hand-computed golden byte vectors deriving each field from the cited
lane-B sections (not generated by calling the builder under test, per the
OP1 convention this file already follows for BuildSetCharacterOptions).
AllegianceRequestsTests.cs gained golden vectors for the existing
Swear/Break builders plus the new Kick alias.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 00:12:47 +02:00
parent bb48d2c89d
commit 7be86f47f6
4 changed files with 264 additions and 35 deletions

View file

@ -32,4 +32,60 @@ public sealed class AllegianceRequestsTests
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)));
}
}

View file

@ -49,22 +49,48 @@ public sealed class SocialActionsTests
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
// FA1 (Campaign FA slice FA1, 2026-08-11): re-pinned per
// docs/research/2026-08-11-fa-fellowship-wire.md §3.1/§5.2(a). The
// PRIOR version of this test asserted a wrong shape — a fabricated
// "openness" byte at offset 20 that does not exist on the wire at all
// — pinning the SAME bug lane B found: ACE reads that byte as the low
// byte of a u32 shareXP, silently inverting it. The golden vector
// below is HAND-COMPUTED field-by-field from
// `CM_Fellowship::Event_Create @0x006A67A0` (`[u32 0xA2][str16L
// name][u32 shareXP]`, buffer size strPackSize+0x10), not generated by
// calling the builder under test — the OP1 convention
// (SocialActionsTests.cs:136-142 states the rule this file already
// follows for BuildSetCharacterOptions).
[Fact]
public void BuildFellowshipCreate_StringThenBools()
public void BuildFellowshipCreate_GoldenByteVector_StringThenShareXpU32()
{
byte[] body = SocialActions.BuildFellowshipCreate(
seq: 1, fellowshipName: "Team", openness: true, shareXp: false);
seq: 7, fellowshipName: "Team", shareXp: true);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
0x07, 0x00, 0x00, 0x00, // seq 7
0xA2, 0x00, 0x00, 0x00, // opcode 0x00A2
0x04, 0x00, 0x54, 0x65, 0x61, 0x6D, 0x00, 0x00, // str16L "Team": u16 len=4, "Team", pad 2
0x01, 0x00, 0x00, 0x00, // shareXP = 1 (true) — a full u32, NOT a byte
];
Assert.Equal(expected, body);
Assert.Equal(SocialActions.FellowshipCreateOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
// String at offset 12: u16 length = 4
ushort len = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12));
Assert.Equal(4, len);
Assert.Equal("Team", Encoding.ASCII.GetString(body.AsSpan(14, 4)));
// string16L record = 2+4=6, pad 2 → advance by 8.
// Then 2 bools at offset 20,21.
Assert.Equal(1, body[20]); // openness true
Assert.Equal(0, body[21]); // shareXp false
}
[Fact]
public void BuildFellowshipCreate_ShareXpFalse_EncodesZeroU32()
{
byte[] body = SocialActions.BuildFellowshipCreate(
seq: 1, fellowshipName: "X", shareXp: false);
// str16L "X": len=1, "X", pad to 4 => 4 bytes total (2+1+1 pad).
Assert.Equal(12 + 4 + 4, body.Length);
Assert.Equal(0u,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(body.Length - 4)));
}
[Fact]
@ -94,13 +120,71 @@ public sealed class SocialActionsTests
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
// FA1: renamed from BuildFellowshipUpdate/"open" — this opcode (0x00A6
// FellowshipUpdateRequest) is the panel-VISIBILITY declaration, not
// fellowship openness (lane B §3.5). The pinned byte shape is
// unchanged (SocialActionsTests.cs:98-105 pre-FA1 pinned the byte, not
// the meaning, so it survived the rename per lane B §5.2(b)) — this
// test just carries the corrected name.
[Fact]
public void BuildFellowshipUpdate_HasOpenBool()
public void BuildFellowshipUpdateRequest_GoldenByteVector_PanelOpenBool()
{
byte[] body = SocialActions.BuildFellowshipUpdate(seq: 1, open: true);
Assert.Equal(SocialActions.FellowshipUpdateOpcode,
byte[] body = SocialActions.BuildFellowshipUpdateRequest(seq: 9, panelOpen: true);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope
0x09, 0x00, 0x00, 0x00, // seq 9
0xA6, 0x00, 0x00, 0x00, // opcode 0x00A6
0x01, 0x00, 0x00, 0x00, // panelOpen = 1
];
Assert.Equal(expected, body);
Assert.Equal(SocialActions.FellowshipUpdateRequestOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
// New builder — 0x0290 FellowshipAssignNewLeader (lane B §2.5/§3.6).
// Golden vector hand-computed from `Event_AssignNewLeader
// @0x006A5F70`: `[u32 0x290][u32 newLeaderGuid]`, total 0x10.
[Fact]
public void BuildFellowshipAssignNewLeader_GoldenByteVector()
{
byte[] body = SocialActions.BuildFellowshipAssignNewLeader(seq: 2, newLeaderGuid: 0x50001234u);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope
0x02, 0x00, 0x00, 0x00, // seq 2
0x90, 0x02, 0x00, 0x00, // opcode 0x0290
0x34, 0x12, 0x00, 0x50, // newLeaderGuid 0x50001234
];
Assert.Equal(expected, body);
Assert.Equal(SocialActions.FellowshipAssignNewLeaderOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
// New builder — 0x0291 FellowshipChangeOpenness, the REAL openness
// toggle (lane B §2.5/§3.7). Golden vector hand-computed from
// `Event_ChangeFellowOpeness @0x006A6040`: `[u32 0x291][u32 isOpen]`,
// total 0x10.
[Fact]
public void BuildFellowshipChangeOpenness_GoldenByteVector()
{
byte[] body = SocialActions.BuildFellowshipChangeOpenness(seq: 4, isOpen: false);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope
0x04, 0x00, 0x00, 0x00, // seq 4
0x91, 0x02, 0x00, 0x00, // opcode 0x0291
0x00, 0x00, 0x00, 0x00, // isOpen = 0 (false)
];
Assert.Equal(expected, body);
Assert.Equal(SocialActions.FellowshipChangeOpennessOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(1, body[12]);
}
[Fact]