feat: port retail magic lifecycle and retained spell UI

Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 10:55:22 +02:00
parent 7b7ffcd278
commit 07be994d97
84 changed files with 17822 additions and 1051 deletions

View file

@ -42,37 +42,92 @@ public sealed class CastSpellTests
public void ParseMagicUpdateSpell_RoundTrip()
{
byte[] payload = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x3E1u);
Assert.Equal(0x3E1u, GameEvents.ParseMagicUpdateSpell(payload));
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x123403E1u);
var parsed = GameEvents.ParseMagicUpdateSpell(payload);
Assert.Equal(0x123403E1u, parsed);
}
[Fact]
public void ParseMagicUpdateEnchantment_RoundTrip()
{
byte[] payload = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 42u); // spellId
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 7u); // layerId
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(8), 300.0f); // duration
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 0xBEEFu); // caster
byte[] payload = BuildEnchantment(spellId: 42, layer: 7, duration: 300, caster: 0xBEEF);
var parsed = GameEvents.ParseMagicUpdateEnchantment(payload);
Assert.NotNull(parsed);
Assert.Equal(42u, parsed!.Value.SpellId);
Assert.Equal(7u, parsed.Value.LayerId);
Assert.Equal(300f, parsed.Value.Duration, 4);
Assert.Equal((ushort)42, parsed!.Value.SpellId);
Assert.Equal((ushort)7, parsed.Value.Layer);
Assert.Equal(300d, parsed.Value.Duration, 4);
Assert.Equal(0xBEEFu, parsed.Value.CasterGuid);
Assert.Equal(0x20u, parsed.Value.StatModType);
Assert.Equal(7u, parsed.Value.StatModKey);
Assert.Equal(1.25f, parsed.Value.StatModValue);
}
[Fact]
public void ParseMagicRemoveEnchantment_RoundTrip()
{
byte[] payload = new byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 7u); // layerId
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 42u); // spellId
byte[] payload = new byte[4];
BinaryPrimitives.WriteUInt16LittleEndian(payload, 42);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(2), 7);
var parsed = GameEvents.ParseMagicRemoveEnchantment(payload);
Assert.NotNull(parsed);
Assert.Equal(7u, parsed!.Value.LayerId);
Assert.Equal(42u, parsed.Value.SpellId);
Assert.Equal((ushort)7, parsed!.Value.Layer);
Assert.Equal((ushort)42, parsed.Value.SpellId);
}
[Fact]
public void ParseMagicUpdateMultipleEnchantments_ReadsEveryRecord()
{
byte[] first = BuildEnchantment(42, 1, 60, 0xAA);
byte[] second = BuildEnchantment(43, 2, 120, 0xBB);
byte[] payload = new byte[4 + first.Length + second.Length];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 2);
first.CopyTo(payload, 4);
second.CopyTo(payload, 4 + first.Length);
var parsed = GameEvents.ParseMagicUpdateMultipleEnchantments(payload);
Assert.NotNull(parsed);
Assert.Equal(2, parsed!.Count);
Assert.Equal((ushort)42, parsed[0].SpellId);
Assert.Equal((ushort)43, parsed[1].SpellId);
}
[Fact]
public void ParseMagicUpdateEnchantment_RejectsTruncatedOptionalSpellSetId()
{
byte[] payload = BuildEnchantment(42, 1, 60, 0xAA);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(6), 1);
Assert.Null(GameEvents.ParseMagicUpdateEnchantment(payload));
}
[Fact]
public void ParseMagicLayeredSpellList_RejectsTruncation()
{
byte[] payload = new byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 2);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(4), 42);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(6), 1);
Assert.Null(GameEvents.ParseMagicLayeredSpellList(payload));
}
private static byte[] BuildEnchantment(
ushort spellId, ushort layer, double duration, uint caster)
{
byte[] payload = new byte[60];
int offset = 0;
WriteU16(spellId); WriteU16(layer); WriteU16(3); WriteU16(0);
WriteU32(8); WriteF64(10); WriteF64(duration); WriteU32(caster);
WriteF32(0.1f); WriteF32(-1f); WriteF64(0);
WriteU32(0x20); WriteU32(7); WriteF32(1.25f);
return payload;
void WriteU16(ushort value) { BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(offset), value); offset += 2; }
void WriteU32(uint value) { BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(offset), value); offset += 4; }
void WriteF32(float value) { BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(offset), value); offset += 4; }
void WriteF64(double value) { BinaryPrimitives.WriteDoubleLittleEndian(payload.AsSpan(offset), value); offset += 8; }
}
}