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
|
|
@ -3,25 +3,32 @@ using System.Buffers.Binary;
|
||||||
namespace AcDream.Core.Net.Messages;
|
namespace AcDream.Core.Net.Messages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Outbound allegiance GameActions. Both Swear and Break carry a
|
/// Outbound allegiance GameActions. Swear, Break, and Kick all carry a
|
||||||
/// single <c>uint32</c> target-guid payload inside the standard
|
/// single <c>uint32</c> target-guid payload inside the standard
|
||||||
/// <c>0xF7B1</c> GameAction envelope.
|
/// <c>0xF7B1</c> GameAction envelope.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Wire layout (r11 §2.1 / §2.3):
|
/// 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>
|
/// <code>
|
||||||
/// u32 0xF7B1
|
/// u32 0xF7B1
|
||||||
/// u32 gameActionSequence
|
/// u32 gameActionSequence
|
||||||
/// u32 subOpcode (0x001D or 0x001E)
|
/// u32 subOpcode (0x001D, 0x001E)
|
||||||
/// u32 targetGuid
|
/// u32 targetGuid
|
||||||
/// </code>
|
/// </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>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Server replies with <c>GameEventAllegianceUpdate</c> (0x0020) and
|
/// Server replies with <c>GameEventAllegianceUpdate</c> (0x0020) and
|
||||||
/// <c>GameEventAllegianceAllegianceUpdateDone</c> (0x01C8) on success,
|
/// <c>GameEventAllegianceAllegianceUpdateDone</c> (0x01C8) on success,
|
||||||
/// or a <c>WeenieError</c> on failure (already sworn, already maxed
|
/// or a <c>WeenieError</c> on failure (already sworn, already maxed
|
||||||
/// vassals, target not online, etc — see r11 §2.1 for the full list).
|
/// vassals, target not online, etc — see lane C §2 master table for the
|
||||||
|
/// full list).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AllegianceRequests
|
public static class AllegianceRequests
|
||||||
|
|
@ -39,12 +46,32 @@ public static class AllegianceRequests
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Break your pledge to <paramref name="targetGuid"/>. Target can be
|
/// Break your pledge to <paramref name="targetGuid"/>. Target can be
|
||||||
/// your patron (breaking from) OR your vassal (breaking them away).
|
/// 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>
|
/// </summary>
|
||||||
public static byte[] BuildBreak(uint gameActionSequence, uint targetGuid)
|
public static byte[] BuildBreak(uint gameActionSequence, uint targetGuid)
|
||||||
{
|
{
|
||||||
return Build(gameActionSequence, BreakOpcode, 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);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] Build(uint seq, uint sub, uint targetGuid)
|
private static byte[] Build(uint seq, uint sub, uint targetGuid)
|
||||||
{
|
{
|
||||||
byte[] body = new byte[16];
|
byte[] body = new byte[16];
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,24 @@ public static class SocialActions
|
||||||
public const uint QueryItemManaOpcode = 0x0263u; // u32 itemGuid; zero cancels
|
public const uint QueryItemManaOpcode = 0x0263u; // u32 itemGuid; zero cancels
|
||||||
public const uint PingRequestOpcode = 0x01E9u; // no payload
|
public const uint PingRequestOpcode = 0x01E9u; // no payload
|
||||||
|
|
||||||
// Fellowship
|
// Fellowship — field orders byte-verified against
|
||||||
public const uint FellowshipCreateOpcode = 0x00A2u; // string16L name, bool openness, bool shareXP
|
// docs/research/2026-08-11-fa-fellowship-wire.md §3.1-§3.7 (lane B),
|
||||||
public const uint FellowshipQuitOpcode = 0x00A3u; // bool disband
|
// 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 FellowshipDismissOpcode = 0x00A4u; // u32 guid
|
||||||
public const uint FellowshipRecruitOpcode = 0x00A5u; // 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
|
// Character options
|
||||||
// CH3 (2026-08-09): the full-blob SetCharacterOptions (0x01A1) builder
|
// CH3 (2026-08-09): the full-blob SetCharacterOptions (0x01A1) builder
|
||||||
|
|
@ -119,21 +131,26 @@ public static class SocialActions
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Create a fellowship with a chosen name + options.</summary>
|
/// <summary>
|
||||||
public static byte[] BuildFellowshipCreate(
|
/// Create a fellowship with a chosen name. There is no "openness"
|
||||||
uint seq, string fellowshipName, bool openness, bool shareXp)
|
/// 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);
|
byte[] name = PackString16L(fellowshipName);
|
||||||
// 2 bools consume 2 bytes + alignment pad to 4.
|
byte[] body = new byte[12 + name.Length + 4];
|
||||||
int boolBlock = 2;
|
|
||||||
int pad = (4 - ((name.Length + boolBlock) & 3)) & 3;
|
|
||||||
byte[] body = new byte[12 + name.Length + boolBlock + pad];
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipCreateOpcode);
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipCreateOpcode);
|
||||||
Array.Copy(name, 0, body, 12, name.Length);
|
Array.Copy(name, 0, body, 12, name.Length);
|
||||||
body[12 + name.Length] = openness ? (byte)1 : (byte)0;
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
body[12 + name.Length + 1] = shareXp ? (byte)1 : (byte)0;
|
body.AsSpan(12 + name.Length), shareXp ? 1u : 0u);
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,14 +173,59 @@ public static class SocialActions
|
||||||
public static byte[] BuildFellowshipRecruit(uint seq, uint targetGuid)
|
public static byte[] BuildFellowshipRecruit(uint seq, uint targetGuid)
|
||||||
=> SingleGuid(seq, FellowshipRecruitOpcode, targetGuid);
|
=> SingleGuid(seq, FellowshipRecruitOpcode, targetGuid);
|
||||||
|
|
||||||
/// <summary>Toggle fellowship open / closed recruiting.</summary>
|
/// <summary>
|
||||||
public static byte[] BuildFellowshipUpdate(uint seq, bool open)
|
/// 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];
|
byte[] body = new byte[16];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateOpcode);
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateRequestOpcode);
|
||||||
body[12] = open ? (byte)1 : (byte)0;
|
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;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,60 @@ public sealed class AllegianceRequestsTests
|
||||||
Assert.Equal(0xBBBBu,
|
Assert.Equal(0xBBBBu,
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FA1: golden byte vectors, hand-computed from lane C §3.2
|
||||||
|
// (`CM_Allegiance::Event_SwearAllegiance`/`Event_BreakAllegiance`),
|
||||||
|
// not generated by calling the builder under test.
|
||||||
|
[Fact]
|
||||||
|
public void BuildSwear_GoldenByteVector()
|
||||||
|
{
|
||||||
|
byte[] body = AllegianceRequests.BuildSwear(gameActionSequence: 3, patronGuid: 0xAAAAu);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
||||||
|
0x03, 0x00, 0x00, 0x00, // seq 3
|
||||||
|
0x1D, 0x00, 0x00, 0x00, // opcode 0x001D
|
||||||
|
0xAA, 0xAA, 0x00, 0x00, // targetGuid 0xAAAA
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildBreak_GoldenByteVector()
|
||||||
|
{
|
||||||
|
byte[] body = AllegianceRequests.BuildBreak(gameActionSequence: 5, targetGuid: 0xBBBBu);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
||||||
|
0x05, 0x00, 0x00, 0x00, // seq 5
|
||||||
|
0x1E, 0x00, 0x00, 0x00, // opcode 0x001E
|
||||||
|
0xBB, 0xBB, 0x00, 0x00, // targetGuid 0xBBBB
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builder — BuildKick is BuildBreak's opcode with a vassal target
|
||||||
|
// (lane C §1.4). Same golden vector shape as BuildBreak, proving the
|
||||||
|
// wire is identical while the call site reads distinctly for FA2.
|
||||||
|
[Fact]
|
||||||
|
public void BuildKick_GoldenByteVector_SameShapeAsBreak()
|
||||||
|
{
|
||||||
|
byte[] body = AllegianceRequests.BuildKick(gameActionSequence: 6, vassalGuid: 0x50000042u);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
||||||
|
0x06, 0x00, 0x00, 0x00, // seq 6
|
||||||
|
0x1E, 0x00, 0x00, 0x00, // opcode 0x001E — SAME as Break
|
||||||
|
0x42, 0x00, 0x00, 0x50, // vassalGuid 0x50000042
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
Assert.Equal(AllegianceRequests.BreakOpcode,
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,22 +49,48 @@ public sealed class SocialActionsTests
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FA1 (Campaign FA slice FA1, 2026-08-11): re-pinned per
|
||||||
|
// docs/research/2026-08-11-fa-fellowship-wire.md §3.1/§5.2(a). The
|
||||||
|
// PRIOR version of this test asserted a wrong shape — a fabricated
|
||||||
|
// "openness" byte at offset 20 that does not exist on the wire at all
|
||||||
|
// — pinning the SAME bug lane B found: ACE reads that byte as the low
|
||||||
|
// byte of a u32 shareXP, silently inverting it. The golden vector
|
||||||
|
// below is HAND-COMPUTED field-by-field from
|
||||||
|
// `CM_Fellowship::Event_Create @0x006A67A0` (`[u32 0xA2][str16L
|
||||||
|
// name][u32 shareXP]`, buffer size strPackSize+0x10), not generated by
|
||||||
|
// calling the builder under test — the OP1 convention
|
||||||
|
// (SocialActionsTests.cs:136-142 states the rule this file already
|
||||||
|
// follows for BuildSetCharacterOptions).
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildFellowshipCreate_StringThenBools()
|
public void BuildFellowshipCreate_GoldenByteVector_StringThenShareXpU32()
|
||||||
{
|
{
|
||||||
byte[] body = SocialActions.BuildFellowshipCreate(
|
byte[] body = SocialActions.BuildFellowshipCreate(
|
||||||
seq: 1, fellowshipName: "Team", openness: true, shareXp: false);
|
seq: 7, fellowshipName: "Team", shareXp: true);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
|
||||||
|
0x07, 0x00, 0x00, 0x00, // seq 7
|
||||||
|
0xA2, 0x00, 0x00, 0x00, // opcode 0x00A2
|
||||||
|
0x04, 0x00, 0x54, 0x65, 0x61, 0x6D, 0x00, 0x00, // str16L "Team": u16 len=4, "Team", pad 2
|
||||||
|
0x01, 0x00, 0x00, 0x00, // shareXP = 1 (true) — a full u32, NOT a byte
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
Assert.Equal(SocialActions.FellowshipCreateOpcode,
|
Assert.Equal(SocialActions.FellowshipCreateOpcode,
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
// String at offset 12: u16 length = 4
|
}
|
||||||
ushort len = BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12));
|
|
||||||
Assert.Equal(4, len);
|
[Fact]
|
||||||
Assert.Equal("Team", Encoding.ASCII.GetString(body.AsSpan(14, 4)));
|
public void BuildFellowshipCreate_ShareXpFalse_EncodesZeroU32()
|
||||||
// string16L record = 2+4=6, pad 2 → advance by 8.
|
{
|
||||||
// Then 2 bools at offset 20,21.
|
byte[] body = SocialActions.BuildFellowshipCreate(
|
||||||
Assert.Equal(1, body[20]); // openness true
|
seq: 1, fellowshipName: "X", shareXp: false);
|
||||||
Assert.Equal(0, body[21]); // shareXp false
|
|
||||||
|
// str16L "X": len=1, "X", pad to 4 => 4 bytes total (2+1+1 pad).
|
||||||
|
Assert.Equal(12 + 4 + 4, body.Length);
|
||||||
|
Assert.Equal(0u,
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(body.Length - 4)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -94,13 +120,71 @@ public sealed class SocialActionsTests
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FA1: renamed from BuildFellowshipUpdate/"open" — this opcode (0x00A6
|
||||||
|
// FellowshipUpdateRequest) is the panel-VISIBILITY declaration, not
|
||||||
|
// fellowship openness (lane B §3.5). The pinned byte shape is
|
||||||
|
// unchanged (SocialActionsTests.cs:98-105 pre-FA1 pinned the byte, not
|
||||||
|
// the meaning, so it survived the rename per lane B §5.2(b)) — this
|
||||||
|
// test just carries the corrected name.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildFellowshipUpdate_HasOpenBool()
|
public void BuildFellowshipUpdateRequest_GoldenByteVector_PanelOpenBool()
|
||||||
{
|
{
|
||||||
byte[] body = SocialActions.BuildFellowshipUpdate(seq: 1, open: true);
|
byte[] body = SocialActions.BuildFellowshipUpdateRequest(seq: 9, panelOpen: true);
|
||||||
Assert.Equal(SocialActions.FellowshipUpdateOpcode,
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope
|
||||||
|
0x09, 0x00, 0x00, 0x00, // seq 9
|
||||||
|
0xA6, 0x00, 0x00, 0x00, // opcode 0x00A6
|
||||||
|
0x01, 0x00, 0x00, 0x00, // panelOpen = 1
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
Assert.Equal(SocialActions.FellowshipUpdateRequestOpcode,
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builder — 0x0290 FellowshipAssignNewLeader (lane B §2.5/§3.6).
|
||||||
|
// Golden vector hand-computed from `Event_AssignNewLeader
|
||||||
|
// @0x006A5F70`: `[u32 0x290][u32 newLeaderGuid]`, total 0x10.
|
||||||
|
[Fact]
|
||||||
|
public void BuildFellowshipAssignNewLeader_GoldenByteVector()
|
||||||
|
{
|
||||||
|
byte[] body = SocialActions.BuildFellowshipAssignNewLeader(seq: 2, newLeaderGuid: 0x50001234u);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope
|
||||||
|
0x02, 0x00, 0x00, 0x00, // seq 2
|
||||||
|
0x90, 0x02, 0x00, 0x00, // opcode 0x0290
|
||||||
|
0x34, 0x12, 0x00, 0x50, // newLeaderGuid 0x50001234
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
Assert.Equal(SocialActions.FellowshipAssignNewLeaderOpcode,
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builder — 0x0291 FellowshipChangeOpenness, the REAL openness
|
||||||
|
// toggle (lane B §2.5/§3.7). Golden vector hand-computed from
|
||||||
|
// `Event_ChangeFellowOpeness @0x006A6040`: `[u32 0x291][u32 isOpen]`,
|
||||||
|
// total 0x10.
|
||||||
|
[Fact]
|
||||||
|
public void BuildFellowshipChangeOpenness_GoldenByteVector()
|
||||||
|
{
|
||||||
|
byte[] body = SocialActions.BuildFellowshipChangeOpenness(seq: 4, isOpen: false);
|
||||||
|
|
||||||
|
byte[] expected =
|
||||||
|
[
|
||||||
|
0xB1, 0xF7, 0x00, 0x00, // envelope
|
||||||
|
0x04, 0x00, 0x00, 0x00, // seq 4
|
||||||
|
0x91, 0x02, 0x00, 0x00, // opcode 0x0291
|
||||||
|
0x00, 0x00, 0x00, 0x00, // isOpen = 0 (false)
|
||||||
|
];
|
||||||
|
|
||||||
|
Assert.Equal(expected, body);
|
||||||
|
Assert.Equal(SocialActions.FellowshipChangeOpennessOpcode,
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||||
Assert.Equal(1, body[12]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue