refactor(net): own session wiring subscriptions
This commit is contained in:
parent
88fe1db37b
commit
7d452aa6a2
8 changed files with 577 additions and 61 deletions
|
|
@ -18,9 +18,10 @@ namespace AcDream.Core.Net;
|
|||
///
|
||||
/// <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.
|
||||
/// dispatcher + the state instances you want to feed. The returned
|
||||
/// subscription owns exactly these registrations and must be disposed
|
||||
/// before the session is replaced. A later legacy registration still
|
||||
/// overrides the corresponding default without becoming owned here.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -32,7 +33,7 @@ namespace AcDream.Core.Net;
|
|||
/// </summary>
|
||||
public static class GameEventWiring
|
||||
{
|
||||
public static void WireAll(
|
||||
public static IDisposable WireAll(
|
||||
GameEventDispatcher dispatcher,
|
||||
ClientObjectTable items,
|
||||
CombatState combat,
|
||||
|
|
@ -88,29 +89,31 @@ public static class GameEventWiring
|
|||
ArgumentNullException.ThrowIfNull(spellbook);
|
||||
ArgumentNullException.ThrowIfNull(chat);
|
||||
clientTime ??= static () => 0d;
|
||||
var registrar = new OwnedGameEventRegistrar(dispatcher);
|
||||
using var construction = new RegistrationBuildScope(registrar);
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.ChannelBroadcast, e =>
|
||||
registrar.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 =>
|
||||
registrar.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 =>
|
||||
registrar.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 =>
|
||||
registrar.Register(GameEventType.PopupString, e =>
|
||||
{
|
||||
var s = GameEvents.ParsePopupString(e.Payload.Span);
|
||||
if (s is not null) chat.OnPopup(s);
|
||||
});
|
||||
dispatcher.Register(GameEventType.QueryAgeResponse, e =>
|
||||
registrar.Register(GameEventType.QueryAgeResponse, e =>
|
||||
{
|
||||
var p = GameEvents.ParseQueryAgeResponse(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -121,7 +124,7 @@ public static class GameEventWiring
|
|||
});
|
||||
if (onConfirmationRequest is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.CharacterConfirmationRequest, e =>
|
||||
registrar.Register(GameEventType.CharacterConfirmationRequest, e =>
|
||||
{
|
||||
var request = GameEvents.ParseCharacterConfirmationRequest(e.Payload.Span);
|
||||
if (request is not null)
|
||||
|
|
@ -131,7 +134,7 @@ public static class GameEventWiring
|
|||
|
||||
if (onConfirmationDone is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.CharacterConfirmationDone, e =>
|
||||
registrar.Register(GameEventType.CharacterConfirmationDone, e =>
|
||||
{
|
||||
var done = GameEvents.ParseCharacterConfirmationDone(e.Payload.Span);
|
||||
if (done is not null)
|
||||
|
|
@ -141,7 +144,7 @@ public static class GameEventWiring
|
|||
|
||||
if (friends is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.FriendsListUpdate, e =>
|
||||
registrar.Register(GameEventType.FriendsListUpdate, e =>
|
||||
{
|
||||
FriendsUpdate? update = SocialStateMessages.ParseFriendsUpdate(e.Payload.Span);
|
||||
if (update is not null) friends.Apply(update);
|
||||
|
|
@ -150,7 +153,7 @@ public static class GameEventWiring
|
|||
|
||||
if (squelch is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.SetSquelchDB, e =>
|
||||
registrar.Register(GameEventType.SetSquelchDB, e =>
|
||||
{
|
||||
SquelchDatabase? database = SocialStateMessages.ParseSquelchDatabase(e.Payload.Span);
|
||||
if (database is not null) squelch.Replace(database);
|
||||
|
|
@ -167,7 +170,7 @@ public static class GameEventWiring
|
|||
// server-message handling pattern.
|
||||
if (turbineChat is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.SetTurbineChatChannels, e =>
|
||||
registrar.Register(GameEventType.SetTurbineChatChannels, e =>
|
||||
{
|
||||
var p = SetTurbineChatChannels.TryParse(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -202,26 +205,26 @@ public static class GameEventWiring
|
|||
// (GameEvents.ParseWeenieError(WithString)) but were never registered.
|
||||
// The server fires these for game-logic failures: "not enough mana",
|
||||
// "can't pick that up", "your spell fizzled". Routed to chat.
|
||||
dispatcher.Register(GameEventType.WeenieError, e =>
|
||||
registrar.Register(GameEventType.WeenieError, e =>
|
||||
{
|
||||
var code = GameEvents.ParseWeenieError(e.Payload.Span);
|
||||
if (code is not null) chat.OnWeenieError(code.Value, param: null);
|
||||
});
|
||||
dispatcher.Register(GameEventType.WeenieErrorWithString, e =>
|
||||
registrar.Register(GameEventType.WeenieErrorWithString, e =>
|
||||
{
|
||||
var p = GameEvents.ParseWeenieErrorWithString(e.Payload.Span);
|
||||
if (p is not null) chat.OnWeenieError(p.Value.ErrorCode, p.Value.Interpolation);
|
||||
});
|
||||
|
||||
// ── Combat ────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.UpdateHealth, e =>
|
||||
registrar.Register(GameEventType.UpdateHealth, e =>
|
||||
{
|
||||
var p = GameEvents.ParseUpdateHealth(e.Payload.Span);
|
||||
if (p is not null) combat.OnUpdateHealth(p.Value.TargetGuid, p.Value.HealthPercent);
|
||||
});
|
||||
if (itemMana is not null)
|
||||
{
|
||||
dispatcher.Register(GameEventType.QueryItemManaResponse, e =>
|
||||
registrar.Register(GameEventType.QueryItemManaResponse, e =>
|
||||
{
|
||||
var p = GameEvents.ParseQueryItemManaResponse(e.Payload.Span);
|
||||
if (p is not null)
|
||||
|
|
@ -229,67 +232,67 @@ public static class GameEventWiring
|
|||
p.Value.ItemGuid, p.Value.ManaPercent, p.Value.Valid);
|
||||
});
|
||||
}
|
||||
dispatcher.Register(GameEventType.VictimNotification, e =>
|
||||
registrar.Register(GameEventType.VictimNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseVictimNotification(e.Payload.Span);
|
||||
if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, CombatLineKind.Error);
|
||||
});
|
||||
dispatcher.Register(GameEventType.DefenderNotification, e =>
|
||||
registrar.Register(GameEventType.DefenderNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseDefenderNotification(e.Payload.Span);
|
||||
if (p is not null) combat.OnDefenderNotification(
|
||||
p.Value.AttackerName, 0u, p.Value.DamageType,
|
||||
p.Value.Damage, p.Value.HitQuadrant, p.Value.Critical);
|
||||
});
|
||||
dispatcher.Register(GameEventType.AttackerNotification, e =>
|
||||
registrar.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, (float)p.Value.HealthPercent);
|
||||
});
|
||||
dispatcher.Register(GameEventType.EvasionAttackerNotification, e =>
|
||||
registrar.Register(GameEventType.EvasionAttackerNotification, e =>
|
||||
{
|
||||
var name = GameEvents.ParseEvasionAttackerNotification(e.Payload.Span);
|
||||
if (name is not null) combat.OnEvasionAttackerNotification(name);
|
||||
});
|
||||
dispatcher.Register(GameEventType.EvasionDefenderNotification, e =>
|
||||
registrar.Register(GameEventType.EvasionDefenderNotification, e =>
|
||||
{
|
||||
var name = GameEvents.ParseEvasionDefenderNotification(e.Payload.Span);
|
||||
if (name is not null) combat.OnEvasionDefenderNotification(name);
|
||||
});
|
||||
dispatcher.Register(GameEventType.AttackDone, e =>
|
||||
registrar.Register(GameEventType.AttackDone, e =>
|
||||
{
|
||||
var p = GameEvents.ParseAttackDone(e.Payload.Span);
|
||||
if (p is not null) combat.OnAttackDone(p.Value.AttackSequence, p.Value.WeenieError);
|
||||
});
|
||||
dispatcher.Register(GameEventType.CombatCommenceAttack, e =>
|
||||
registrar.Register(GameEventType.CombatCommenceAttack, e =>
|
||||
{
|
||||
if (GameEvents.ParseCombatCommenceAttack(e.Payload.Span))
|
||||
combat.OnCombatCommenceAttack();
|
||||
});
|
||||
dispatcher.Register(GameEventType.KillerNotification, e =>
|
||||
registrar.Register(GameEventType.KillerNotification, e =>
|
||||
{
|
||||
var p = GameEvents.ParseKillerNotification(e.Payload.Span);
|
||||
if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, CombatLineKind.Info);
|
||||
});
|
||||
|
||||
// ── Spells ────────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.MagicUpdateSpell, e =>
|
||||
registrar.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 =>
|
||||
registrar.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 =>
|
||||
registrar.Register(GameEventType.MagicUpdateEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicUpdateEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentAdded(ToActiveEnchantment(p.Value, clientTime()));
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicUpdateMultipleEnchantments, e =>
|
||||
registrar.Register(GameEventType.MagicUpdateMultipleEnchantments, e =>
|
||||
{
|
||||
var entries = GameEvents.ParseMagicUpdateMultipleEnchantments(e.Payload.Span);
|
||||
if (entries is not null)
|
||||
|
|
@ -299,35 +302,35 @@ public static class GameEventWiring
|
|||
ToActiveEnchantment(entry, receivedAt)));
|
||||
}
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicRemoveEnchantment, e =>
|
||||
registrar.Register(GameEventType.MagicRemoveEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicRemoveEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.Layer, p.Value.SpellId);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicRemoveMultipleEnchantments, e =>
|
||||
registrar.Register(GameEventType.MagicRemoveMultipleEnchantments, e =>
|
||||
{
|
||||
var entries = GameEvents.ParseMagicLayeredSpellList(e.Payload.Span);
|
||||
if (entries is not null)
|
||||
spellbook.OnEnchantmentsRemoved(entries.Select(item => ((uint)item.SpellId, (uint)item.Layer)));
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicDispelEnchantment, e =>
|
||||
registrar.Register(GameEventType.MagicDispelEnchantment, e =>
|
||||
{
|
||||
var p = GameEvents.ParseMagicDispelEnchantment(e.Payload.Span);
|
||||
if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.Layer, p.Value.SpellId);
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicDispelMultipleEnchantments, e =>
|
||||
registrar.Register(GameEventType.MagicDispelMultipleEnchantments, e =>
|
||||
{
|
||||
var entries = GameEvents.ParseMagicLayeredSpellList(e.Payload.Span);
|
||||
if (entries is not null)
|
||||
spellbook.OnEnchantmentsRemoved(entries.Select(item => ((uint)item.SpellId, (uint)item.Layer)));
|
||||
});
|
||||
dispatcher.Register(GameEventType.MagicPurgeEnchantments,
|
||||
registrar.Register(GameEventType.MagicPurgeEnchantments,
|
||||
_ => spellbook.OnPurgeAll());
|
||||
dispatcher.Register(GameEventType.MagicPurgeBadEnchantments,
|
||||
registrar.Register(GameEventType.MagicPurgeBadEnchantments,
|
||||
_ => spellbook.OnPurgeBadEnchantments());
|
||||
|
||||
// ── Inventory ─────────────────────────────────────────────
|
||||
dispatcher.Register(GameEventType.WieldObject, e =>
|
||||
registrar.Register(GameEventType.WieldObject, e =>
|
||||
{
|
||||
var p = GameEvents.ParseWieldObject(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -338,7 +341,7 @@ public static class GameEventWiring
|
|||
wielderGuid,
|
||||
(AcDream.Core.Items.EquipMask)p.Value.EquipLoc);
|
||||
});
|
||||
dispatcher.Register(GameEventType.InventoryPutObjInContainer, e =>
|
||||
registrar.Register(GameEventType.InventoryPutObjInContainer, e =>
|
||||
{
|
||||
var p = GameEvents.ParsePutObjInContainer(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -355,7 +358,7 @@ public static class GameEventWiring
|
|||
// opened (Use 0x0036). Treat it as a full projection-only REPLACE: update membership without
|
||||
// inventing ContainerSlot values, then publish one ContainerContentsReplaced notification so
|
||||
// every UI consumer repaints from the same snapshot. Retail: ClientUISystem::OnViewContents.
|
||||
dispatcher.Register(GameEventType.ViewContents, e =>
|
||||
registrar.Register(GameEventType.ViewContents, e =>
|
||||
{
|
||||
var p = GameEvents.ParseViewContents(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -371,7 +374,7 @@ public static class GameEventWiring
|
|||
// B-Wire: InventoryPutObjectIn3D (0x019A) — server confirms an item dropped
|
||||
// to the world. Unparent it from its container (it's now a ground object) so
|
||||
// the inventory grid drops the cell; the object itself survives.
|
||||
dispatcher.Register(GameEventType.InventoryPutObjectIn3D, e =>
|
||||
registrar.Register(GameEventType.InventoryPutObjectIn3D, e =>
|
||||
{
|
||||
var guid = GameEvents.ParsePutObjectIn3D(e.Payload.Span);
|
||||
if (guid is not null)
|
||||
|
|
@ -386,7 +389,7 @@ public static class GameEventWiring
|
|||
// B-Drag: InventoryServerSaveFailed (0x00A0) — server rejected an optimistic move.
|
||||
// Snap the item back to its pre-move slot. Log only when there was no pending move
|
||||
// (a server-initiated failure on a non-optimistic path).
|
||||
dispatcher.Register(GameEventType.InventoryServerSaveFailed, e =>
|
||||
registrar.Register(GameEventType.InventoryServerSaveFailed, e =>
|
||||
{
|
||||
var p = GameEvents.ParseInventoryServerSaveFailed(e.Payload.Span);
|
||||
if (p is null) return;
|
||||
|
|
@ -404,7 +407,7 @@ public static class GameEventWiring
|
|||
// retail surfaces the string-table text as a chat line. Interim text map:
|
||||
// WeenieErrorText (#202 / register AP-74). Without this line a refused
|
||||
// kit-heal looks like "nothing happened" — the 2026-07-03 session bug.
|
||||
dispatcher.Register(GameEventType.UseDone, e =>
|
||||
registrar.Register(GameEventType.UseDone, e =>
|
||||
{
|
||||
uint? err = GameEvents.ParseUseDone(e.Payload.Span);
|
||||
if (err is null) return;
|
||||
|
|
@ -417,7 +420,7 @@ public static class GameEventWiring
|
|||
// CloseGroundContainer (0x0052): clear ClientUISystem::groundObject and
|
||||
// retire the root plus nested temporary ViewContents projections. Child
|
||||
// objects remain until authoritative move/delete wire says otherwise.
|
||||
dispatcher.Register(GameEventType.CloseGroundContainer, e =>
|
||||
registrar.Register(GameEventType.CloseGroundContainer, e =>
|
||||
{
|
||||
var guid = GameEvents.ParseCloseGroundContainer(e.Payload.Span);
|
||||
if (guid is null) return;
|
||||
|
|
@ -425,7 +428,7 @@ public static class GameEventWiring
|
|||
items.StopViewingContentsTree(guid.Value);
|
||||
});
|
||||
|
||||
dispatcher.Register(GameEventType.IdentifyObjectResponse, e =>
|
||||
registrar.Register(GameEventType.IdentifyObjectResponse, e =>
|
||||
{
|
||||
var p = AppraiseInfoParser.TryParse(e.Payload.Span);
|
||||
if (p is null || !p.Value.Success) return;
|
||||
|
|
@ -461,7 +464,7 @@ public static class GameEventWiring
|
|||
// SpellBook flag in IdentifyObjectResponse is for caster
|
||||
// items / scrolls only).
|
||||
bool dumpPd = Environment.GetEnvironmentVariable("ACDREAM_DUMP_VITALS") == "1";
|
||||
dispatcher.Register(GameEventType.PlayerDescription, e =>
|
||||
registrar.Register(GameEventType.PlayerDescription, e =>
|
||||
{
|
||||
var p = PlayerDescriptionParser.TryParse(e.Payload.Span);
|
||||
if (dumpPd)
|
||||
|
|
@ -652,6 +655,38 @@ public static class GameEventWiring
|
|||
// toolbar can read them without holding a parser reference.
|
||||
onShortcuts?.Invoke(p.Value.Shortcuts);
|
||||
});
|
||||
return construction.Complete();
|
||||
}
|
||||
|
||||
private sealed class OwnedGameEventRegistrar(
|
||||
GameEventDispatcher dispatcher) : IDisposable
|
||||
{
|
||||
private readonly SubscriptionSet _subscriptions = new();
|
||||
|
||||
public void Register(
|
||||
GameEventType type,
|
||||
GameEventDispatcher.EventHandler handler) =>
|
||||
_subscriptions.Add(dispatcher.RegisterOwned(type, handler));
|
||||
|
||||
public void Dispose() => _subscriptions.Dispose();
|
||||
}
|
||||
|
||||
private sealed class RegistrationBuildScope(
|
||||
OwnedGameEventRegistrar registration) : IDisposable
|
||||
{
|
||||
private bool _complete;
|
||||
|
||||
public IDisposable Complete()
|
||||
{
|
||||
_complete = true;
|
||||
return registration;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_complete)
|
||||
registration.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static ActiveEnchantmentRecord ToActiveEnchantment(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue