feat(net): GameEventWiring — one-call glue from dispatcher to Core state
Central registration helper that wires every parsed GameEvent from the Phase F.1 dispatcher into the appropriate Core state class: - ChannelBroadcast / Tell / CommunicationTransientString / PopupString → ChatLog (H.1) - UpdateHealth / Victim / Defender / Attacker / EvasionAttacker / EvasionDefender / AttackDone → CombatState (E.4) - MagicUpdateSpell / MagicRemoveSpell / MagicUpdateEnchantment / MagicRemoveEnchantment / MagicDispelEnchantment / MagicPurgeEnchantments → Spellbook (E.5) - WieldObject / InventoryPutObjInContainer → ItemRepository (F.2) This is the piece that makes the dispatcher go from "thing that routes opcodes" to "thing that populates state the UI can redraw from". Before this, every handler had to be wired at each call site; now one call at startup (or per-reconnect) does the whole map. Project graph: added AcDream.Core.Net → AcDream.Core ProjectReference so the wiring can see both the dispatcher (Net) and the state classes (Core). Net's own tests already pull in Core indirectly, so test scope is unchanged. Tests (6 new, in Core.Net.Tests): verify round-trip via the actual dispatcher. Build envelope → dispatch → assert the correct Core state change. Covers ChannelBroadcast, UpdateHealth, MagicUpdateSpell, WieldObject, PopupString, MagicPurgeEnchantments. Build green, 602 tests pass (up from 596). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a28a69af71
commit
83e0e4f9ca
3 changed files with 304 additions and 0 deletions
155
src/AcDream.Core.Net/GameEventWiring.cs
Normal file
155
src/AcDream.Core.Net/GameEventWiring.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
using System;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Central registration point that wires every parsed GameEvent from
|
||||
/// <see cref="GameEventDispatcher"/> into the appropriate Core state
|
||||
/// class (<see cref="ItemRepository"/>, <see cref="CombatState"/>,
|
||||
/// <see cref="Spellbook"/>, <see cref="ChatLog"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Call once at startup (or on reconnect) passing the session's
|
||||
/// dispatcher + the state instances you want to feed. The wiring is
|
||||
/// additive — if you want to add a custom handler for a specific
|
||||
/// event, register it AFTER this helper so it overrides the default.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This is the piece that makes Phase F.1's dispatcher go from "a
|
||||
/// thing that routes opcodes" to "a thing that actually populates
|
||||
/// client state so the UI can redraw". Without this glue every
|
||||
/// dispatcher handler had to be written by hand at each call site.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class GameEventWiring
|
||||
{
|
||||
public static void WireAll(
|
||||
GameEventDispatcher dispatcher,
|
||||
ItemRepository items,
|
||||
CombatState combat,
|
||||
Spellbook spellbook,
|
||||
ChatLog chat)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
ArgumentNullException.ThrowIfNull(combat);
|
||||
ArgumentNullException.ThrowIfNull(spellbook);
|
||||
ArgumentNullException.ThrowIfNull(chat);
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.ChannelBroadcast, e =>
|
||||
{
|
||||
var p = GameEvents.ParseChannelBroadcast(e.Payload.Span);
|
||||
if (p is not null) chat.OnChannelBroadcast(p.Value.ChannelId, p.Value.SenderName, p.Value.Message);
|
||||
});
|
||||
dispatcher.Register(GameEventType.Tell, e =>
|
||||
{
|
||||
var p = GameEvents.ParseTell(e.Payload.Span);
|
||||
if (p is not null) chat.OnTellReceived(p.Value.SenderName, p.Value.Message, p.Value.SenderGuid);
|
||||
});
|
||||
dispatcher.Register(GameEventType.CommunicationTransientString, e =>
|
||||
{
|
||||
var p = GameEvents.ParseTransient(e.Payload.Span);
|
||||
if (p is not null) chat.OnSystemMessage(p.Value.Message, p.Value.ChatType);
|
||||
});
|
||||
dispatcher.Register(GameEventType.PopupString, e =>
|
||||
{
|
||||
var s = GameEvents.ParsePopupString(e.Payload.Span);
|
||||
if (s is not null) chat.OnPopup(s);
|
||||
});
|
||||
|
||||
// ── Combat ────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.UpdateHealth, e =>
|
||||
{
|
||||
var p = GameEvents.ParseUpdateHealth(e.Payload.Span);
|
||||
if (p is not null) combat.OnUpdateHealth(p.Value.TargetGuid, p.Value.HealthPercent);
|
||||
});
|
||||
dispatcher.Register(GameEventType.VictimNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseVictimNotification(e.Payload.Span);
|
||||
if (p is not null) combat.OnVictimNotification(
|
||||
p.Value.AttackerName, p.Value.AttackerGuid, p.Value.DamageType,
|
||||
p.Value.Damage, p.Value.HitQuadrant, p.Value.Critical, p.Value.AttackType);
|
||||
});
|
||||
dispatcher.Register(GameEventType.DefenderNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseDefenderNotification(e.Payload.Span);
|
||||
if (p is not null) combat.OnDefenderNotification(
|
||||
p.Value.AttackerName, p.Value.AttackerGuid, p.Value.DamageType,
|
||||
p.Value.Damage, p.Value.HitQuadrant, p.Value.Critical);
|
||||
});
|
||||
dispatcher.Register(GameEventType.AttackerNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseAttackerNotification(e.Payload.Span);
|
||||
if (p is not null) combat.OnAttackerNotification(
|
||||
p.Value.DefenderName, p.Value.DamageType, p.Value.Damage, p.Value.DamagePercent);
|
||||
});
|
||||
dispatcher.Register(GameEventType.EvasionAttackerNotification, e =>
|
||||
{
|
||||
var name = GameEvents.ParseEvasionAttackerNotification(e.Payload.Span);
|
||||
if (name is not null) combat.OnEvasionAttackerNotification(name);
|
||||
});
|
||||
dispatcher.Register(GameEventType.EvasionDefenderNotification, e =>
|
||||
{
|
||||
var name = GameEvents.ParseEvasionDefenderNotification(e.Payload.Span);
|
||||
if (name is not null) combat.OnEvasionDefenderNotification(name);
|
||||
});
|
||||
dispatcher.Register(GameEventType.AttackDone, e =>
|
||||
{
|
||||
var p = GameEvents.ParseAttackDone(e.Payload.Span);
|
||||
if (p is not null) combat.OnAttackDone(p.Value.AttackSequence, p.Value.WeenieError);
|
||||
});
|
||||
|
||||
// ── Spells ────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.MagicUpdateSpell, e =>
|
||||
{
|
||||
var spellId = GameEvents.ParseMagicUpdateSpell(e.Payload.Span);
|
||||
if (spellId is not null) spellbook.OnSpellLearned(spellId.Value);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicRemoveSpell, e =>
|
||||
{
|
||||
var spellId = GameEvents.ParseMagicRemoveSpell(e.Payload.Span);
|
||||
if (spellId is not null) spellbook.OnSpellForgotten(spellId.Value);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicUpdateEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicUpdateEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentAdded(
|
||||
p.Value.SpellId, p.Value.LayerId, p.Value.Duration, p.Value.CasterGuid);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicRemoveEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicRemoveEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.LayerId, p.Value.SpellId);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicDispelEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicDispelEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.LayerId, p.Value.SpellId);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicPurgeEnchantments,
|
||||
_ => spellbook.OnPurgeAll());
|
||||
|
||||
// ── Inventory ─────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.WieldObject, e =>
|
||||
{
|
||||
var p = GameEvents.ParseWieldObject(e.Payload.Span);
|
||||
if (p is not null) items.MoveItem(
|
||||
p.Value.ItemGuid,
|
||||
newContainerId: p.Value.WielderGuid,
|
||||
newEquipLocation: (AcDream.Core.Items.EquipMask)p.Value.EquipLoc);
|
||||
});
|
||||
dispatcher.Register(GameEventType.InventoryPutObjInContainer, e =>
|
||||
{
|
||||
var p = GameEvents.ParsePutObjInContainer(e.Payload.Span);
|
||||
if (p is not null) items.MoveItem(p.Value.ItemGuid, p.Value.ContainerGuid,
|
||||
newSlot: (int)p.Value.Placement);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue