fix(net): Phase L.1c conform combat wire events
This commit is contained in:
parent
460f95cb42
commit
29afc94b94
8 changed files with 241 additions and 177 deletions
|
|
@ -241,28 +241,32 @@ public sealed class GameEventWiringTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_KillerNotification_FiresKillLandedOnCombatState()
|
||||
public void WireAll_KillerNotification_AppendsCombatLine()
|
||||
{
|
||||
// Issue #10 — orphan parser at GameEvents.ParseKillerNotification
|
||||
// existed but was never registered for dispatch until 2026-04-25.
|
||||
// Now wired: 0x01AD lands on CombatState.OnKillerNotification +
|
||||
// fires the KillLanded event.
|
||||
var (d, _, combat, _, _) = MakeAll();
|
||||
string? gotVictimName = null;
|
||||
uint gotVictimGuid = 0;
|
||||
combat.KillLanded += (name, guid) => { gotVictimName = name; gotVictimGuid = guid; };
|
||||
|
||||
// Wire shape: string16L victimName + u32 victimGuid
|
||||
byte[] nameBytes = MakeString16L("Drudge");
|
||||
byte[] payload = new byte[nameBytes.Length + 4];
|
||||
Array.Copy(nameBytes, payload, nameBytes.Length);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(nameBytes.Length), 0x80001234u);
|
||||
var (d, _, _, _, chat) = MakeAll();
|
||||
byte[] payload = MakeString16L("You killed the drudge!");
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.KillerNotification, payload));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.Equal("Drudge", gotVictimName);
|
||||
Assert.Equal(0x80001234u, gotVictimGuid);
|
||||
Assert.Equal(1, chat.Count);
|
||||
var entry = chat.Snapshot()[0];
|
||||
Assert.Equal(ChatKind.Combat, entry.Kind);
|
||||
Assert.Equal(CombatLineKind.Info, entry.CombatKind);
|
||||
Assert.Equal("You killed the drudge!", entry.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WireAll_CombatCommenceAttack_FiresCombatStateEvent()
|
||||
{
|
||||
var (d, _, combat, _, _) = MakeAll();
|
||||
bool commenced = false;
|
||||
combat.AttackCommenced += () => commenced = true;
|
||||
|
||||
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.CombatCommenceAttack, Array.Empty<byte>()));
|
||||
d.Dispatch(env!.Value);
|
||||
|
||||
Assert.True(commenced);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -57,4 +57,13 @@ public sealed class CharacterActionsTests
|
|||
Assert.Equal(2u, // Melee = 2
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CombatMode_UsesRetailAceBitValues()
|
||||
{
|
||||
Assert.Equal(1u, (uint)CharacterActions.CombatMode.NonCombat);
|
||||
Assert.Equal(2u, (uint)CharacterActions.CombatMode.Melee);
|
||||
Assert.Equal(4u, (uint)CharacterActions.CombatMode.Missile);
|
||||
Assert.Equal(8u, (uint)CharacterActions.CombatMode.Magic);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -8,105 +7,140 @@ namespace AcDream.Core.Net.Tests.Messages;
|
|||
|
||||
public sealed class CombatEventTests
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttackTargetRequest_Build_EmitsCorrectWireBytes()
|
||||
public void AttackTargetRequest_BuildMelee_EmitsRetailWireBytes()
|
||||
{
|
||||
byte[] body = AttackTargetRequest.Build(
|
||||
byte[] body = AttackTargetRequest.BuildMelee(
|
||||
gameActionSequence: 3,
|
||||
targetGuid: 0x12345678u,
|
||||
powerLevel: 0.75f,
|
||||
accuracyLevel: 0.5f,
|
||||
attackHeight: 2);
|
||||
attackHeight: 2,
|
||||
powerLevel: 0.75f);
|
||||
|
||||
Assert.Equal(28, body.Length);
|
||||
Assert.Equal(24, body.Length);
|
||||
Assert.Equal(AttackTargetRequest.GameActionEnvelope,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body));
|
||||
Assert.Equal(3u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
|
||||
Assert.Equal(AttackTargetRequest.SubOpcode,
|
||||
Assert.Equal(AttackTargetRequest.TargetedMeleeAttackOpcode,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||
Assert.Equal(0x12345678u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||
Assert.Equal(2u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
||||
Assert.Equal(0.75f,
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(16)), 4);
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(20)), 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttackTargetRequest_BuildMissile_EmitsRetailWireBytes()
|
||||
{
|
||||
byte[] body = AttackTargetRequest.BuildMissile(
|
||||
gameActionSequence: 4,
|
||||
targetGuid: 0x87654321u,
|
||||
attackHeight: 1,
|
||||
accuracyLevel: 0.5f);
|
||||
|
||||
Assert.Equal(24, body.Length);
|
||||
Assert.Equal(AttackTargetRequest.TargetedMissileAttackOpcode,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||
Assert.Equal(0x87654321u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
|
||||
Assert.Equal(1u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
||||
Assert.Equal(0.5f,
|
||||
BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(20)), 4);
|
||||
Assert.Equal(2u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(24)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseVictimNotification_RoundTrip()
|
||||
public void AttackTargetRequest_BuildCancel_HasNoPayload()
|
||||
{
|
||||
byte[] name = MakeString16L("Attacker");
|
||||
byte[] tail = new byte[24];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail, 0xAAu); // guid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(4), 1u); // damageType
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(8), 42u); // damage
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(12), 3u); // quadrant
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(16), 1u); // crit
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(20), 8u); // attackType
|
||||
byte[] body = AttackTargetRequest.BuildCancel(gameActionSequence: 5);
|
||||
|
||||
byte[] payload = new byte[name.Length + tail.Length];
|
||||
Buffer.BlockCopy(name, 0, payload, 0, name.Length);
|
||||
Buffer.BlockCopy(tail, 0, payload, name.Length, tail.Length);
|
||||
Assert.Equal(12, body.Length);
|
||||
Assert.Equal(AttackTargetRequest.CancelAttackOpcode,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAttackDone_HoltburgerFixture()
|
||||
{
|
||||
var env = ParseFixture("B0F700000000000000000000A701000036000000");
|
||||
|
||||
Assert.Equal(GameEventType.AttackDone, env.EventType);
|
||||
var parsed = GameEvents.ParseAttackDone(env.Payload.Span);
|
||||
|
||||
var parsed = GameEvents.ParseVictimNotification(payload);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal("Attacker", parsed!.Value.AttackerName);
|
||||
Assert.Equal(0xAAu, parsed.Value.AttackerGuid);
|
||||
Assert.Equal(42u, parsed.Value.Damage);
|
||||
Assert.Equal(0u, parsed!.Value.AttackSequence);
|
||||
Assert.Equal(0x36u, parsed.Value.WeenieError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAttackerNotification_HoltburgerFixture()
|
||||
{
|
||||
var env = ParseFixture("B0F700000000000001000000B10100000E0044727564676520526176656E657201000000000000000000D03F25000000010000000600000000000000");
|
||||
|
||||
var parsed = GameEvents.ParseAttackerNotification(env.Payload.Span);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal("Drudge Ravener", parsed!.Value.DefenderName);
|
||||
Assert.Equal(1u, parsed.Value.DamageType);
|
||||
Assert.Equal(0.25, parsed.Value.HealthPercent, 6);
|
||||
Assert.Equal(37u, parsed.Value.Damage);
|
||||
Assert.Equal(1u, parsed.Value.Critical);
|
||||
Assert.Equal(6ul, parsed.Value.AttackConditions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAttackerNotification_RoundTrip()
|
||||
public void ParseDefenderNotification_HoltburgerFixture()
|
||||
{
|
||||
byte[] name = MakeString16L("Drudge");
|
||||
byte[] tail = new byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail, 1u); // damageType
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(tail.AsSpan(4), 30u); // damage
|
||||
BinaryPrimitives.WriteSingleLittleEndian(tail.AsSpan(8), 0.15f); // percent
|
||||
var env = ParseFixture("B0F700000000000002000000B20100000A0042616E6465726C696E6710000000000000000000C03F1200000001000000000000000800000000000000");
|
||||
|
||||
byte[] payload = new byte[name.Length + tail.Length];
|
||||
Buffer.BlockCopy(name, 0, payload, 0, name.Length);
|
||||
Buffer.BlockCopy(tail, 0, payload, name.Length, tail.Length);
|
||||
var parsed = GameEvents.ParseDefenderNotification(env.Payload.Span);
|
||||
|
||||
var parsed = GameEvents.ParseAttackerNotification(payload);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal("Drudge", parsed!.Value.DefenderName);
|
||||
Assert.Equal(30u, parsed.Value.Damage);
|
||||
Assert.Equal(0.15f, parsed.Value.DamagePercent, 4);
|
||||
Assert.Equal("Banderling", parsed!.Value.AttackerName);
|
||||
Assert.Equal(0x10u, parsed.Value.DamageType);
|
||||
Assert.Equal(0.125, parsed.Value.HealthPercent, 6);
|
||||
Assert.Equal(18u, parsed.Value.Damage);
|
||||
Assert.Equal(1u, parsed.Value.HitQuadrant);
|
||||
Assert.Equal(0u, parsed.Value.Critical);
|
||||
Assert.Equal(8ul, parsed.Value.AttackConditions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEvasionAttackerNotification_RoundTrip()
|
||||
public void ParseEvasionNotifications_HoltburgerFixtures()
|
||||
{
|
||||
byte[] payload = MakeString16L("Thrower");
|
||||
Assert.Equal("Thrower", GameEvents.ParseEvasionAttackerNotification(payload));
|
||||
var attacker = ParseFixture("B0F700000000000003000000B301000008004D6F7373776172740000");
|
||||
var defender = ParseFixture("B0F700000000000004000000B401000004004D6974650000");
|
||||
|
||||
Assert.Equal("Mosswart", GameEvents.ParseEvasionAttackerNotification(attacker.Payload.Span));
|
||||
Assert.Equal("Mite", GameEvents.ParseEvasionDefenderNotification(defender.Payload.Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAttackDone_RoundTrip()
|
||||
public void ParseCombatCommenceAttack_HoltburgerFixture()
|
||||
{
|
||||
byte[] payload = new byte[8];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload, 42u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0u); // no error
|
||||
var env = ParseFixture("B0F700000000000005000000B8010000");
|
||||
|
||||
var parsed = GameEvents.ParseAttackDone(payload);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(42u, parsed!.Value.AttackSequence);
|
||||
Assert.Equal(0u, parsed.Value.WeenieError);
|
||||
Assert.Equal(GameEventType.CombatCommenceAttack, env.EventType);
|
||||
Assert.True(GameEvents.ParseCombatCommenceAttack(env.Payload.Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDeathNotifications_HoltburgerFixtures()
|
||||
{
|
||||
var victim = ParseFixture("B0F700000000000006000000AC0100000E00596F752068617665206469656421");
|
||||
var killer = ParseFixture("B0F700000000000007000000AD0100001600596F75206B696C6C6564207468652064727564676521");
|
||||
|
||||
Assert.Equal("You have died!", GameEvents.ParseVictimNotification(victim.Payload.Span)?.DeathMessage);
|
||||
Assert.Equal("You killed the drudge!", GameEvents.ParseKillerNotification(killer.Payload.Span)?.DeathMessage);
|
||||
}
|
||||
|
||||
private static GameEventEnvelope ParseFixture(string hex)
|
||||
{
|
||||
byte[] body = Convert.FromHexString(hex);
|
||||
var env = GameEventEnvelope.TryParse(body);
|
||||
Assert.NotNull(env);
|
||||
return env.Value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue