Client-side allegiance data model + outbound swear/break actions.
The inbound AllegianceUpdate blob (0x0020) is complex and is deferred;
the tree API here is designed so the handler can push nodes in when
the blob parser lands.
Wire layer:
- AllegianceRequests.BuildSwear (0x001D): single uint32 patronGuid.
- AllegianceRequests.BuildBreak (0x001E): single uint32 targetGuid
(works for both breaking from patron and breaking away a vassal;
the server picks behavior based on the relationship).
Core layer (AcDream.Core/Allegiance):
- AllegianceNode: Guid, Name, PatronGuid, Rank (clamped 0..10),
VassalGuids list.
- AllegianceTree: Dictionary-backed, events on TreeChanged.
- SetMonarch: registers the root (no patron).
- UpsertNode: adds/refreshes + auto-inserts into parent's vassal list.
- RemoveNode: removes from parent list too; descendants are left with
dangling patron pointers for the UI to hide (next AllegianceUpdate
refreshes).
- GetAncestors: walks up to monarch, cycle-detected for defense.
- GetDescendants: BFS-order flattening.
- AllegianceMath.ComputePassup: retail XP formula
(50+22.5×loyalty)/291 × (1+RT/730×IG/720) × earned,
clamped at 0.
Tests (11 new):
- Tree: SetMonarch fires TreeChanged, UpsertNode auto-populates parent
vassal list, rank clamp at 10, RemoveNode cleans parent list,
GetAncestors chain, cycle-safe walk, GetDescendants BFS order.
- Math: Passup known-value check (1000 XP, 10 loyalty, 100 RT/IG
days → ~963 XP), negative clamp.
- Wire: Swear + Break byte-exact encoding.
Build green, 613 tests pass (up from 602).
Next: wire inbound AllegianceUpdate (0x0020) + AllegianceInfoResponse
(0x027C) handlers once the blob parser lands. Chat "Allegiance"
Turbine channel joining (r11 §2.1 step 9) layers on top of
Phase H.1 chat infrastructure.
Ref: r11 §1 (tree structure + rank cap), §2 (swear/break wire),
§3.2 (XP passup formula).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
1.9 KiB
C#
57 lines
1.9 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Outbound allegiance GameActions. Both Swear and Break carry a
|
|
/// single <c>uint32</c> target-guid payload inside the standard
|
|
/// <c>0xF7B1</c> GameAction envelope.
|
|
///
|
|
/// <para>
|
|
/// Wire layout (r11 §2.1 / §2.3):
|
|
/// <code>
|
|
/// u32 0xF7B1
|
|
/// u32 gameActionSequence
|
|
/// u32 subOpcode (0x001D or 0x001E)
|
|
/// u32 targetGuid
|
|
/// </code>
|
|
/// </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 r11 §2.1 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>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).
|
|
/// </summary>
|
|
public static byte[] BuildBreak(uint gameActionSequence, uint targetGuid)
|
|
{
|
|
return Build(gameActionSequence, BreakOpcode, targetGuid);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|