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;
}
}