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>
146 lines
5.2 KiB
C#
146 lines
5.2 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Spells;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Net.Tests;
|
|
|
|
public sealed class GameEventWiringTests
|
|
{
|
|
private static byte[] MakeString16L(string s)
|
|
{
|
|
byte[] data = Encoding.ASCII.GetBytes(s);
|
|
int recordSize = 2 + data.Length;
|
|
int padding = (4 - (recordSize & 3)) & 3;
|
|
byte[] result = new byte[recordSize + padding];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(result, (ushort)data.Length);
|
|
Array.Copy(data, 0, result, 2, data.Length);
|
|
return result;
|
|
}
|
|
|
|
private static byte[] WrapEnvelope(GameEventType type, byte[] payload)
|
|
{
|
|
byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameEventEnvelope.Opcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), 0u);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), 0u);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), (uint)type);
|
|
Array.Copy(payload, 0, body, GameEventEnvelope.HeaderSize, payload.Length);
|
|
return body;
|
|
}
|
|
|
|
private static (GameEventDispatcher, ItemRepository, CombatState, Spellbook, ChatLog) MakeAll()
|
|
{
|
|
var dispatcher = new GameEventDispatcher();
|
|
var items = new ItemRepository();
|
|
var combat = new CombatState();
|
|
var spellbook = new Spellbook();
|
|
var chat = new ChatLog();
|
|
GameEventWiring.WireAll(dispatcher, items, combat, spellbook, chat);
|
|
return (dispatcher, items, combat, spellbook, chat);
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_ChannelBroadcast_RoutesToChatLog()
|
|
{
|
|
var (d, _, _, _, chat) = MakeAll();
|
|
|
|
byte[] payload = new byte[4 + MakeString16L("Alice").Length + MakeString16L("hi").Length];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload, 42);
|
|
int p = 4;
|
|
var senderBytes = MakeString16L("Alice");
|
|
Array.Copy(senderBytes, 0, payload, p, senderBytes.Length); p += senderBytes.Length;
|
|
var msgBytes = MakeString16L("hi");
|
|
Array.Copy(msgBytes, 0, payload, p, msgBytes.Length);
|
|
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.ChannelBroadcast, payload));
|
|
d.Dispatch(env!.Value);
|
|
|
|
Assert.Equal(1, chat.Count);
|
|
var entry = chat.Snapshot()[0];
|
|
Assert.Equal(ChatKind.Channel, entry.Kind);
|
|
Assert.Equal("Alice", entry.Sender);
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_UpdateHealth_RoutesToCombatState()
|
|
{
|
|
var (d, _, combat, _, _) = MakeAll();
|
|
|
|
byte[] payload = new byte[8];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0xCAFE);
|
|
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(4), 0.42f);
|
|
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.UpdateHealth, payload));
|
|
d.Dispatch(env!.Value);
|
|
|
|
Assert.Equal(0.42f, combat.GetHealthPercent(0xCAFE), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_MagicUpdateSpell_RoutesToSpellbook()
|
|
{
|
|
var (d, _, _, book, _) = MakeAll();
|
|
|
|
byte[] payload = new byte[4];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x3E1);
|
|
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.MagicUpdateSpell, payload));
|
|
d.Dispatch(env!.Value);
|
|
|
|
Assert.True(book.Knows(0x3E1));
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_WieldObject_RoutesToItemRepository()
|
|
{
|
|
var (d, items, _, _, _) = MakeAll();
|
|
items.AddOrUpdate(new ItemInstance { ObjectId = 0x1000, WeenieClassId = 1 });
|
|
|
|
byte[] payload = new byte[12];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x1000);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)EquipMask.MeleeWeapon);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0x2000);
|
|
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.WieldObject, payload));
|
|
d.Dispatch(env!.Value);
|
|
|
|
var item = items.GetItem(0x1000);
|
|
Assert.NotNull(item);
|
|
Assert.Equal(EquipMask.MeleeWeapon, item!.CurrentlyEquippedLocation);
|
|
Assert.Equal(0x2000u, item.ContainerId);
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_PopupString_RoutesToChatLog()
|
|
{
|
|
var (d, _, _, _, chat) = MakeAll();
|
|
|
|
byte[] payload = MakeString16L("A modal message");
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.PopupString, payload));
|
|
d.Dispatch(env!.Value);
|
|
|
|
Assert.Equal(1, chat.Count);
|
|
Assert.Equal(ChatKind.Popup, chat.Snapshot()[0].Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void WireAll_MagicPurgeEnchantments_CallsOnPurgeAll()
|
|
{
|
|
var (d, _, _, book, _) = MakeAll();
|
|
book.OnEnchantmentAdded(1, 1, 100f, 0);
|
|
book.OnEnchantmentAdded(2, 2, 100f, 0);
|
|
Assert.Equal(2, book.ActiveCount);
|
|
|
|
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.MagicPurgeEnchantments, Array.Empty<byte>()));
|
|
d.Dispatch(env!.Value);
|
|
|
|
Assert.Equal(0, book.ActiveCount);
|
|
}
|
|
}
|