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