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>
169 lines
8 KiB
C#
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);
|
|
});
|
|
}
|
|
}
|