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:
parent
bb48d2c89d
commit
7be86f47f6
4 changed files with 264 additions and 35 deletions
|
|
@ -33,12 +33,24 @@ public static class SocialActions
|
|||
public const uint QueryItemManaOpcode = 0x0263u; // u32 itemGuid; zero cancels
|
||||
public const uint PingRequestOpcode = 0x01E9u; // no payload
|
||||
|
||||
// Fellowship
|
||||
public const uint FellowshipCreateOpcode = 0x00A2u; // string16L name, bool openness, bool shareXP
|
||||
public const uint FellowshipQuitOpcode = 0x00A3u; // bool disband
|
||||
// Fellowship — field orders byte-verified against
|
||||
// docs/research/2026-08-11-fa-fellowship-wire.md §3.1-§3.7 (lane B),
|
||||
// three-way agreed with ACE + holtburger there. Campaign FA slice FA1
|
||||
// (2026-08-11) repaired two latent defects here (lane B §5.2): (a)
|
||||
// FellowshipCreateOpcode's body never had an "openness" field — the
|
||||
// prior builder invented one and silently sent it as the low byte of
|
||||
// shareXP, which ACE would read as an INVERTED shareXP value; (b)
|
||||
// FellowshipUpdateOpcode (0x00A6) is FellowshipUpdateRequest — panel
|
||||
// VISIBILITY, not fellowship openness (that's the new 0x0291 below) —
|
||||
// the prior "open" parameter name/doc was wrong even though its single
|
||||
// bool byte happened to be in the right wire position.
|
||||
public const uint FellowshipCreateOpcode = 0x00A2u; // string16L name, u32 shareXP
|
||||
public const uint FellowshipQuitOpcode = 0x00A3u; // u32 disband (0/1)
|
||||
public const uint FellowshipDismissOpcode = 0x00A4u; // u32 guid
|
||||
public const uint FellowshipRecruitOpcode = 0x00A5u; // u32 guid
|
||||
public const uint FellowshipUpdateOpcode = 0x00A6u; // bool open
|
||||
public const uint FellowshipUpdateRequestOpcode = 0x00A6u; // u32 panelOpen (0/1) — panel visibility, NOT openness
|
||||
public const uint FellowshipAssignNewLeaderOpcode = 0x0290u; // u32 newLeaderGuid
|
||||
public const uint FellowshipChangeOpennessOpcode = 0x0291u; // u32 isOpen (0/1) — the REAL openness toggle
|
||||
|
||||
// Character options
|
||||
// CH3 (2026-08-09): the full-blob SetCharacterOptions (0x01A1) builder
|
||||
|
|
@ -119,21 +131,26 @@ public static class SocialActions
|
|||
return body;
|
||||
}
|
||||
|
||||
/// <summary>Create a fellowship with a chosen name + options.</summary>
|
||||
public static byte[] BuildFellowshipCreate(
|
||||
uint seq, string fellowshipName, bool openness, bool shareXp)
|
||||
/// <summary>
|
||||
/// Create a fellowship with a chosen name. There is no "openness"
|
||||
/// field on this message — retail's <c>gmFellowshipUI::CreateFellowship
|
||||
/// @0x0048F730</c> writes exactly <c>[str16L name][u32 shareXP]</c>
|
||||
/// (lane B §2.2/§3.1). <paramref name="shareXp"/> is the
|
||||
/// <c>FellowshipShareXP</c> character option (<c>0x0F</c>), not a
|
||||
/// dialog checkbox — retail reads
|
||||
/// <c>PlayerModule::FellowshipShareXP</c> at create time and sends its
|
||||
/// current value.
|
||||
/// </summary>
|
||||
public static byte[] BuildFellowshipCreate(uint seq, string fellowshipName, bool shareXp)
|
||||
{
|
||||
byte[] name = PackString16L(fellowshipName);
|
||||
// 2 bools consume 2 bytes + alignment pad to 4.
|
||||
int boolBlock = 2;
|
||||
int pad = (4 - ((name.Length + boolBlock) & 3)) & 3;
|
||||
byte[] body = new byte[12 + name.Length + boolBlock + pad];
|
||||
byte[] body = new byte[12 + name.Length + 4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipCreateOpcode);
|
||||
Array.Copy(name, 0, body, 12, name.Length);
|
||||
body[12 + name.Length] = openness ? (byte)1 : (byte)0;
|
||||
body[12 + name.Length + 1] = shareXp ? (byte)1 : (byte)0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
body.AsSpan(12 + name.Length), shareXp ? 1u : 0u);
|
||||
return body;
|
||||
}
|
||||
|
||||
|
|
@ -156,14 +173,59 @@ public static class SocialActions
|
|||
public static byte[] BuildFellowshipRecruit(uint seq, uint targetGuid)
|
||||
=> SingleGuid(seq, FellowshipRecruitOpcode, targetGuid);
|
||||
|
||||
/// <summary>Toggle fellowship open / closed recruiting.</summary>
|
||||
public static byte[] BuildFellowshipUpdate(uint seq, bool open)
|
||||
/// <summary>
|
||||
/// Declare the fellowship panel's visibility to the server
|
||||
/// (<c>0x00A6 FellowshipUpdateRequest</c>). This is NOT fellowship
|
||||
/// openness — retail sends it from
|
||||
/// <c>gmFellowshipUI::OnVisibilityChanged @0x0048E460</c> on panel
|
||||
/// show/hide, and ACE gates the whole <c>0x02C0</c> member-vitals
|
||||
/// stream on it (<c>Fellowship.cs:723</c>): a client that never sends
|
||||
/// this sees names/levels/max-vitals from <c>0x02BE</c> but a roster
|
||||
/// frozen at join time (lane B §4.5, §12). See
|
||||
/// <see cref="BuildFellowshipChangeOpenness"/> for the real openness
|
||||
/// toggle, which is a separate opcode this builder used to be
|
||||
/// (incorrectly) named for.
|
||||
/// </summary>
|
||||
public static byte[] BuildFellowshipUpdateRequest(uint seq, bool panelOpen)
|
||||
{
|
||||
byte[] body = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateOpcode);
|
||||
body[12] = open ? (byte)1 : (byte)0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateRequestOpcode);
|
||||
body[12] = panelOpen ? (byte)1 : (byte)0;
|
||||
return body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer fellowship leadership to another member
|
||||
/// (<c>0x0290 FellowshipAssignNewLeader</c>). Retail's Quit button, when
|
||||
/// pressed by the current leader, sends this FIRST (targeting
|
||||
/// <c>Fellowship::GetNonLeaderFellowID</c>) and only then sends
|
||||
/// <c>0x00A3</c> disband=0 —
|
||||
/// <c>gmFellowshipUI</c> Quit button case 8 @0x0049034B (lane B
|
||||
/// §2.5/§3.6). <c>AssignLeadershipToFellow @0x0048F1E0</c> is the
|
||||
/// direct (non-quit) caller.
|
||||
/// </summary>
|
||||
public static byte[] BuildFellowshipAssignNewLeader(uint seq, uint newLeaderGuid)
|
||||
=> SingleGuid(seq, FellowshipAssignNewLeaderOpcode, newLeaderGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Toggle fellowship open / closed recruiting — the REAL openness
|
||||
/// action (<c>0x0291 FellowshipChangeOpenness</c>). Retail's Open
|
||||
/// button (case 9) calls
|
||||
/// <c>Event_ChangeFellowOpeness @0x006A6040</c> after pre-toggling its
|
||||
/// own <c>_open_fellow</c> optimistically (lane B §2.5/§3.7) — the
|
||||
/// bytes are identical in shape to the old (misnamed)
|
||||
/// <see cref="BuildFellowshipUpdateRequest"/>, but this is a distinct
|
||||
/// opcode acdream did not previously send at all.
|
||||
/// </summary>
|
||||
public static byte[] BuildFellowshipChangeOpenness(uint seq, bool isOpen)
|
||||
{
|
||||
byte[] body = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipChangeOpennessOpcode);
|
||||
body[12] = isOpen ? (byte)1 : (byte)0;
|
||||
return body;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue