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>
113 lines
4.9 KiB
C#
113 lines
4.9 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Outbound allegiance GameActions. Swear, Break, and Kick all carry a
|
|
/// single <c>uint32</c> target-guid payload inside the standard
|
|
/// <c>0xF7B1</c> GameAction envelope.
|
|
///
|
|
/// <para>
|
|
/// Wire layout — byte-verified against
|
|
/// <c>docs/research/2026-08-11-fa-allegiance-wire.md</c> §3.2 (lane C),
|
|
/// three-way agreed with ACE + Chorizite there (the retail anchor is the
|
|
/// opcode literal store inside each <c>CM_Allegiance::Event_*</c> sender,
|
|
/// the strongest possible provenance):
|
|
/// <code>
|
|
/// u32 0xF7B1
|
|
/// u32 gameActionSequence
|
|
/// u32 subOpcode (0x001D, 0x001E)
|
|
/// u32 targetGuid
|
|
/// </code>
|
|
/// total 0x10 bytes for both. No change was needed here in Campaign FA
|
|
/// slice FA1 (2026-08-11) — the pre-existing shape already matched.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Server replies with <c>GameEventAllegianceUpdate</c> (0x0020) and
|
|
/// <c>GameEventAllegianceAllegianceUpdateDone</c> (0x01C8) on success,
|
|
/// or a <c>WeenieError</c> on failure (already sworn, already maxed
|
|
/// vassals, target not online, etc — see lane C §2 master table for the
|
|
/// full list).
|
|
/// </para>
|
|
/// </summary>
|
|
public static class AllegianceRequests
|
|
{
|
|
public const uint GameActionEnvelope = 0xF7B1u;
|
|
public const uint SwearOpcode = 0x001Du;
|
|
public const uint BreakOpcode = 0x001Eu;
|
|
|
|
/// <summary>
|
|
/// <c>0x001F AllegianceUpdateRequest</c> — the allegiance panel's
|
|
/// subscribe/unsubscribe toggle, the structural twin of the fellowship
|
|
/// <c>0x00A6 FellowshipUpdateRequest</c> repaired elsewhere in this
|
|
/// slice. Retail's <c>CM_Allegiance::Event_UpdateRequest(u32)</c>
|
|
/// (<c>@0x006A72BA</c>, lane C §1.2) is NOT a one-shot query — send
|
|
/// <c>1</c> on <c>gmAllegianceUI::PostInit</c>'s tail
|
|
/// (<c>@0x004911C6</c>) and on <c>RecvNotice_PlayerDescReceived</c>
|
|
/// (<c>@0x00490D59</c>), <c>1</c> on <c>OnVisibilityChanged</c>'s
|
|
/// visible branch (<c>@0x004912DD</c>), <c>0</c> on its hidden branch
|
|
/// (<c>@0x00491311</c>). ACE's own handler
|
|
/// (<c>GameActionAllegianceUpdateRequest.cs:12</c>) reads the value
|
|
/// and ignores it — always replying once regardless — but retail
|
|
/// servers do not, so this must be sent correctly (lane C §7.1
|
|
/// message #3).
|
|
/// </summary>
|
|
public const uint AllegianceUpdateRequestOpcode = 0x001Fu;
|
|
|
|
/// <summary>Pledge yourself to the given patron.</summary>
|
|
public static byte[] BuildSwear(uint gameActionSequence, uint patronGuid)
|
|
{
|
|
return Build(gameActionSequence, SwearOpcode, patronGuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Break your pledge to <paramref name="targetGuid"/>. Target can be
|
|
/// your patron (breaking from) OR your vassal (breaking them away).
|
|
/// Retail's Break button (element <c>0x10000264</c>) always targets
|
|
/// your own patron — see <see cref="BuildKick"/> for the vassal-facing
|
|
/// sibling button.
|
|
/// </summary>
|
|
public static byte[] BuildBreak(uint gameActionSequence, uint targetGuid)
|
|
{
|
|
return Build(gameActionSequence, BreakOpcode, targetGuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kick a vassal out of your allegiance. Wire-identical to
|
|
/// <see cref="BuildBreak"/> — both are <c>Event_BreakAllegiance(u32)
|
|
/// @0x006A69DA</c> (opcode <c>0x001E</c>); retail's Kick button
|
|
/// (<c>gmAllegianceUI::CloseKickConfirmationDialog @0x00490B00</c>)
|
|
/// targets the selected vassal (<c>m_iidPossibleKickedVassal</c>)
|
|
/// where Break targets <c>AllegianceProfile::GetPatron(self)</c> (lane
|
|
/// C §1.4). Named separately purely so FA2's panel command surface can
|
|
/// distinguish "break from patron" from "kick a vassal" without both
|
|
/// call sites reading like the same action; the golden vector is
|
|
/// identical to <see cref="BuildBreak"/>'s.
|
|
/// </summary>
|
|
public static byte[] BuildKick(uint gameActionSequence, uint vassalGuid)
|
|
{
|
|
return Build(gameActionSequence, BreakOpcode, vassalGuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Declare the allegiance panel's visibility/subscription state to the
|
|
/// server (<c>0x001F AllegianceUpdateRequest</c>). See
|
|
/// <see cref="AllegianceUpdateRequestOpcode"/> for the four retail send
|
|
/// sites and their arguments.
|
|
/// </summary>
|
|
public static byte[] BuildAllegianceUpdateRequest(uint gameActionSequence, bool on)
|
|
{
|
|
return Build(gameActionSequence, AllegianceUpdateRequestOpcode, on ? 1u : 0u);
|
|
}
|
|
|
|
private static byte[] Build(uint seq, uint sub, uint targetGuid)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), sub);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid);
|
|
return body;
|
|
}
|
|
}
|