fix(net): FA1 review round -- zero-id tree rejection, monarch clear, 0x001F builder
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>
This commit is contained in:
parent
bc693728a6
commit
ed30808720
8 changed files with 385 additions and 34 deletions
|
|
@ -314,6 +314,82 @@ public sealed class ClientCommandResponsesTests
|
|||
lines);
|
||||
}
|
||||
|
||||
// FA1 review round (blast SF-1): FA1's tree-assembly rules flipped
|
||||
// vassal print order (FindVassals now walks in REVERSE wire order —
|
||||
// see AllegianceProfileVersionGateTests.TreeAssembly_SiblingOrder_
|
||||
// ReversesOnAssembly) at the LIVE @allegiance info surface. The
|
||||
// pre-existing full-tree test above has exactly ONE vassal, which
|
||||
// cannot distinguish forward from reverse order — this pins the
|
||||
// reversal through FormatAllegianceInfoLines itself, not just through
|
||||
// FindVassals, with three vassals so the ordering is unambiguous.
|
||||
[Fact]
|
||||
public void ParseAndFormatAllegianceInfoResponse_ThreeVassals_PrintsInReverseWireOrder()
|
||||
{
|
||||
const uint monarchGuid = 0x50000001u;
|
||||
const uint vassalAGuid = 0x50000010u;
|
||||
const uint vassalBGuid = 0x50000011u;
|
||||
const uint vassalCGuid = 0x50000012u;
|
||||
|
||||
byte[] wire = BuildAllegianceWire(
|
||||
monarchGuid,
|
||||
new()
|
||||
{
|
||||
(monarchGuid, 0u, true, "Monarch"),
|
||||
(vassalAGuid, monarchGuid, true, "VassalA"),
|
||||
(vassalBGuid, monarchGuid, true, "VassalB"),
|
||||
(vassalCGuid, monarchGuid, true, "VassalC"),
|
||||
});
|
||||
|
||||
var response = ClientCommandResponses.ParseAllegianceInfoResponse(wire);
|
||||
Assert.NotNull(response);
|
||||
|
||||
var lines = ClientCommandResponses.FormatAllegianceInfoLines(response.Value).ToArray();
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"Note: An asterisk (*) indicates that the character is currently online.",
|
||||
"Allegiance information for Monarch *:",
|
||||
" Vassals: ",
|
||||
" VassalC *",
|
||||
" VassalB *",
|
||||
" VassalA *",
|
||||
},
|
||||
lines);
|
||||
}
|
||||
|
||||
// FA1 review round (blast SF-1): the §4.4 tree-assembly rules turned a
|
||||
// malformed tree (orphan / self-parent / duplicate treeParent) into a
|
||||
// whole-message discard (ParseAllegianceInfoResponse returns null),
|
||||
// and GameEventWiring's registration is `if (info is null) return;` —
|
||||
// so the live @allegiance info command now goes from "prints a
|
||||
// possibly-garbled roster" to "prints nothing at all" on a malformed
|
||||
// tree. Pin that silent-drop behavior at the wiring layer that shows
|
||||
// it to the user.
|
||||
[Fact]
|
||||
public void WireAll_AllegianceInfoResponse_MalformedTree_PrintsNothing()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
var chat = new ChatLog();
|
||||
GameEventWiring.WireAll(dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), chat);
|
||||
|
||||
const uint monarchGuid = 0x50000001u;
|
||||
byte[] payload = BuildAllegianceWire(
|
||||
monarchGuid,
|
||||
new()
|
||||
{
|
||||
(monarchGuid, 0u, true, "Monarch"),
|
||||
(0x50000005u, 0x5000FFFFu /* never-seen parent — orphan */, true, "Orphan"),
|
||||
});
|
||||
|
||||
GameEventEnvelope? envelope = GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.AllegianceInfoResponse, payload));
|
||||
Assert.NotNull(envelope);
|
||||
dispatcher.Dispatch(envelope.Value);
|
||||
|
||||
Assert.Empty(chat.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatAllegianceInfoLines_NoAllegiance_PrintsNothing()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue