acdream/src/AcDream.Core.Net/GameEventWiring.cs
Erik 4d96156e05 feat(net): wire IdentifyObjectResponse into ItemRepository + Spellbook
GameEventWiring now registers a handler for
GameEventType.IdentifyObjectResponse (0x00C9) that:

1. Runs AppraiseInfoParser.TryParse to extract the full property bundle.
2. If the item is in the repository, merges the bundle into its
   Properties via ItemRepository.UpdateProperties (fires
   ItemPropertiesUpdated).
3. Merges any SpellBook entries into Spellbook.OnSpellLearned (caster
   weapons list their cast-on-use spells; PlayerDescription reuses the
   same container for the player's learned set).

Effect: when the player clicks "Appraise" on an item, the tooltip
panel can read full property detail from ItemInstance.Properties
immediately after the server replies.

Build + 628 tests still green. No new test file needed — existing
AppraiseInfoParser tests cover the parse path; GameEventWiring round-
trip tests cover the dispatch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:22:31 +02:00

169 lines
8 KiB
C#

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);
});
dispatcher.Register(GameEventType.IdentifyObjectResponse, e =>
{
var p = AppraiseInfoParser.TryParse(e.Payload.Span);
if (p is null || !p.Value.Success) return;
// Merge parsed properties into the item if we know about it.
if (items.GetItem(p.Value.Guid) is not null)
items.UpdateProperties(p.Value.Guid, p.Value.Properties);
// Spell book from appraise: for ITEMS (caster / scrolls) this
// lists cast-on-use effects; for players (PlayerDescription)
// it's the whole learned spellbook. Both mutate the spellbook
// by adding any not-yet-known ids.
foreach (uint sid in p.Value.SpellBook)
spellbook.OnSpellLearned(sid);
});
}
}