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:
Erik 2026-04-18 17:12:05 +02:00
parent a28a69af71
commit 83e0e4f9ca
3 changed files with 304 additions and 0 deletions

View 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);
});
}
}