feat(net): FA2 -- fellowship/allegiance outbound wrappers + inbound wiring
Adds the missing WorldSession.Send* link for every FA1 fellowship/ allegiance builder (SendFellowshipCreate/Quit/Dismiss/Recruit/ UpdateRequest/AssignNewLeader/ChangeOpenness, SendAllegianceSwear/ Break/Kick/UpdateRequest) and 15 new GameEventWiring.WireAll delegate holes covering the 11 S->C fellowship/allegiance events. Delegate holes (not state-object params) because Core.Net cannot reference AcDream.Runtime, matching the onCharacterOptions/onConfirmationRequest precedent. Fixes a real bug found during implementation: GameEventDispatcher. Dispatch invokes only the single most-recently-registered handler per GameEventType (RegisterOwned REPLACES, it does not chain-invoke) -- contradicts the seam doc's "the dispatcher supports multiple owned handlers per type" claim. A literal second registrar.Register call for AllegianceInfoResponse would have silently killed the already-live `@allegiance info` chat-text output the moment a caller supplied the new self-gated Runtime callback. Both behaviors are folded into the ONE existing registration instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1bb707e248
commit
1c40104896
3 changed files with 437 additions and 1 deletions
|
|
@ -7,6 +7,7 @@ using AcDream.Core.Combat;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Net.Tests.Messages;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.Social;
|
||||
|
|
@ -1544,6 +1545,232 @@ public sealed class GameEventWiringTests
|
|||
Assert.Equal((0x50C4A54Au, 0x948700u, false), observed);
|
||||
}
|
||||
|
||||
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────
|
||||
// Wiring-plumbing tests: proves GameEventWiring.WireAll's NEW delegate
|
||||
// holes register against the SAME already-golden-vector-tested FA1
|
||||
// parsers (FellowshipEventsTests.cs / AllegianceSmallEventsTests.cs /
|
||||
// AllegianceProfileVersionGateTests.cs) and reach their callback. Not a
|
||||
// re-test of parser correctness — a proof the registration site is
|
||||
// wired at all.
|
||||
|
||||
[Fact]
|
||||
public void WireAll_FellowshipFullUpdate_ReachesTheCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
GameEvents.FellowshipFullUpdate? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onFellowshipFullUpdate: update => observed = update);
|
||||
|
||||
byte[] wire = new AceWireWriter()
|
||||
.Write((ushort)0).Write((ushort)16)
|
||||
.WriteString16L("TestFellowship")
|
||||
.Write((uint)0) // leaderGuid
|
||||
.Write((uint)1) // shareXp
|
||||
.Write((uint)0) // evenXpSplit
|
||||
.Write((uint)0) // openFellow
|
||||
.Write((uint)0) // locked
|
||||
.Write((ushort)0).Write((ushort)32)
|
||||
.ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.FellowshipFullUpdate, wire))!.Value);
|
||||
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal("TestFellowship", observed.Value.Name);
|
||||
Assert.True(observed.Value.ShareXp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_FellowshipUpdateFellow_ReachesTheCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
GameEvents.FellowshipUpdateFellow? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onFellowshipUpdateFellow: update => observed = update);
|
||||
|
||||
byte[] wire = new AceWireWriter()
|
||||
.Write(0x50000001u)
|
||||
.Write((uint)0).Write((uint)0).Write((uint)1)
|
||||
.Write((uint)100).Write((uint)100).Write((uint)100)
|
||||
.Write((uint)42).Write((uint)100).Write((uint)100)
|
||||
.Write((uint)0)
|
||||
.WriteString16L("Self")
|
||||
.Write((uint)3)
|
||||
.ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.FellowshipUpdateFellow, wire))!.Value);
|
||||
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal(0x50000001u, observed.Value.MemberGuid);
|
||||
Assert.Equal(42u, observed.Value.Member.CurrentHealth);
|
||||
Assert.Equal(3u, observed.Value.UpdateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_FellowshipQuit_ReachesTheCallbackWithQuitterGuid()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
uint? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onFellowshipQuit: guid => observed = guid);
|
||||
|
||||
byte[] wire = new AceWireWriter().Write(0x50000042u).ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.FellowshipQuit, wire))!.Value);
|
||||
|
||||
Assert.Equal(0x50000042u, observed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_FellowshipDismiss_ReachesTheCallbackWithDismissedGuid()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
uint? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onFellowshipDismiss: guid => observed = guid);
|
||||
|
||||
byte[] wire = new AceWireWriter().Write(0x50000043u).ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.FellowshipDismiss, wire))!.Value);
|
||||
|
||||
Assert.Equal(0x50000043u, observed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_FellowshipDisband_ReachesTheCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
bool fired = false;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onFellowshipDisband: () => fired = true);
|
||||
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.FellowshipDisband, []))!.Value);
|
||||
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
/// <summary>Minimal valid <c>AllegianceProfile</c> body at <c>oldVersion=0</c> — no gated fields, one monarch record only.</summary>
|
||||
private static byte[] BuildMinimalAllegianceProfile(
|
||||
uint leadingField,
|
||||
uint monarchGuid,
|
||||
string monarchName = "Monarch")
|
||||
{
|
||||
return new AceWireWriter()
|
||||
.Write(leadingField)
|
||||
.Write((uint)1) // totalMembers
|
||||
.Write((uint)0) // totalVassals
|
||||
.Write((ushort)1) // recordCount
|
||||
.Write((ushort)0) // oldVersion — no gates fire
|
||||
.Write(monarchGuid)
|
||||
.Write((uint)0).Write((uint)0) // cpCached, cpTithed
|
||||
.Write(0x4u | 0x8u) // bitfield: HasAllegianceAge | HasPackedLevel
|
||||
.Write((byte)0).Write((byte)0) // gender, heritageGroup
|
||||
.Write((ushort)1) // rank
|
||||
.Write((uint)10) // level (HasPackedLevel)
|
||||
.Write((ushort)0).Write((ushort)0) // loyalty, leadership
|
||||
.Write((uint)0).Write((uint)0) // timeOnline, allegianceAge (HasAllegianceAge)
|
||||
.WriteString16L(monarchName)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_AllegianceUpdate_ReachesTheCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
ClientCommandResponses.AllegianceUpdate? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onAllegianceUpdate: update => observed = update);
|
||||
|
||||
byte[] wire = BuildMinimalAllegianceProfile(leadingField: 5u, monarchGuid: 0x50000001u);
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.AllegianceUpdate, wire))!.Value);
|
||||
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal(5u, observed.Value.Rank);
|
||||
Assert.Equal(0x50000001u, observed.Value.Monarch!.Value.CharacterId);
|
||||
Assert.Equal("Monarch", observed.Value.Monarch.Value.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_AllegianceInfoResponse_SelfGated_FiresOnlyForOwnGuid()
|
||||
{
|
||||
const uint self = 0x50000001u;
|
||||
const uint other = 0x50000002u;
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
ClientCommandResponses.AllegianceInfoResponse? observed = null;
|
||||
int chatLines = 0;
|
||||
var chat = new ChatLog();
|
||||
chat.EntryAppended += _ => chatLines++;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), chat,
|
||||
playerGuid: () => self,
|
||||
onAllegianceInfoResponseSelf: response => observed = response);
|
||||
|
||||
// A response about ANOTHER player: the self-gated Runtime callback
|
||||
// must NOT fire, but the pre-existing `@allegiance info` chat-text
|
||||
// handler (unconditional) must still fire unchanged.
|
||||
byte[] otherWire = BuildMinimalAllegianceProfile(leadingField: other, monarchGuid: other, monarchName: "Other");
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.AllegianceInfoResponse, otherWire))!.Value);
|
||||
Assert.Null(observed);
|
||||
Assert.True(chatLines > 0);
|
||||
|
||||
chatLines = 0;
|
||||
byte[] selfWire = BuildMinimalAllegianceProfile(leadingField: self, monarchGuid: self, monarchName: "Self");
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.AllegianceInfoResponse, selfWire))!.Value);
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal(self, observed.Value.TargetGuid);
|
||||
// The chat-text handler fires for EVERY response, self or not.
|
||||
Assert.True(chatLines > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_AllegianceUpdateDoneAndAborted_ReachTheirCallbacks()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
uint? done = null;
|
||||
uint? aborted = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onAllegianceUpdateDone: code => done = code,
|
||||
onAllegianceUpdateAborted: code => aborted = code);
|
||||
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.AllegianceUpdateDone,
|
||||
new AceWireWriter().Write(0x0561u).ToArray()))!.Value);
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(WrapEnvelope(
|
||||
GameEventType.AllegianceUpdateAborted,
|
||||
new AceWireWriter().Write(0x0562u).ToArray()))!.Value);
|
||||
|
||||
Assert.Equal(0x0561u, done);
|
||||
Assert.Equal(0x0562u, aborted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_AllegianceLoginNotification_ReachesTheCallback()
|
||||
{
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
GameEvents.AllegianceLoginNotification? observed = null;
|
||||
GameEventWiring.WireAll(
|
||||
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
|
||||
onAllegianceLoginNotification: notice => observed = notice);
|
||||
|
||||
byte[] wire = new AceWireWriter().Write(0x50000042u).Write((uint)1).ToArray();
|
||||
dispatcher.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapEnvelope(GameEventType.AllegianceLoginNotification, wire))!.Value);
|
||||
|
||||
Assert.NotNull(observed);
|
||||
Assert.Equal(0x50000042u, observed.Value.CharacterGuid);
|
||||
Assert.True(observed.Value.IsLoggedIn);
|
||||
}
|
||||
|
||||
private static byte[] BuildEnchantment(
|
||||
ushort spellId,
|
||||
ushort layer,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue