feat(ui): port retail selected-item mana

Parse and route the complete 0x0264 guid/fraction/valid response, send exact 0x0263 item-mana queries, and reproduce retail meter visibility plus guid-zero cancellation for mana and health selection changes. Keep the behavior in Core state and the retained selected-object controller with wire/state/UI conformance coverage.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 11:16:21 +02:00
parent df460f94c3
commit fa1da02783
21 changed files with 402 additions and 38 deletions

View file

@ -449,6 +449,30 @@ public sealed class GameEventWiringTests
Assert.Equal(1u, items.Get(0x50000A02u)!.ContainerTypeHint);
}
[Fact]
public void WireAll_QueryItemManaResponse_RoutesValidityToItemManaState()
{
var dispatcher = new GameEventDispatcher();
var itemMana = new ItemManaState();
(uint Guid, float Percent, bool Valid)? observed = null;
itemMana.ItemManaChanged += (guid, percent, valid) => observed = (guid, percent, valid);
GameEventWiring.WireAll(
dispatcher, new ClientObjectTable(), new CombatState(), new Spellbook(), new ChatLog(),
itemMana: itemMana);
byte[] payload = new byte[12];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x50000A01u);
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(4), 0.375f);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 1u);
var envelope = GameEventEnvelope.TryParse(
WrapEnvelope(GameEventType.QueryItemManaResponse, payload));
dispatcher.Dispatch(envelope!.Value);
Assert.Equal((0x50000A01u, 0.375f, true), observed);
Assert.True(itemMana.HasMana(0x50000A01u));
}
[Fact]
public void UseDone_releasesAppBusyOwnerThroughCallback()
{

View file

@ -177,6 +177,24 @@ public sealed class GameEventDispatcherTests
Assert.Equal(0.42f, parsed.Value.HealthPercent, 4);
}
[Fact]
public void ParseQueryItemManaResponse_RoundTripIncludingValidity()
{
// CM_Item::DispatchUI_QueryItemManaResponse @ 0x006A84D0 reads all three fields.
byte[] payload = new byte[12];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x50000A01u);
BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(4), 0.625f);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 1u);
var parsed = GameEvents.ParseQueryItemManaResponse(payload);
Assert.NotNull(parsed);
Assert.Equal(0x50000A01u, parsed!.Value.ItemGuid);
Assert.Equal(0.625f, parsed.Value.ManaPercent);
Assert.True(parsed.Value.Valid);
Assert.Null(GameEvents.ParseQueryItemManaResponse(payload.AsSpan(0, 8)));
}
[Fact]
public void ParseWieldObject_acceptsAceEightBytePayload()
{

View file

@ -18,6 +18,22 @@ public sealed class SocialActionsTests
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
}
[Fact]
public void BuildQueryItemMana_HasOpcode0x0263AndGuid()
{
// CM_Item::Event_QueryItemMana @ 0x006A8610: F7B1, seq, 0x0263, guid.
byte[] body = SocialActions.BuildQueryItemMana(seq: 4, itemGuid: 0x50000A01u);
Assert.Equal(16, body.Length);
Assert.Equal(SocialActions.GameActionEnvelope,
BinaryPrimitives.ReadUInt32LittleEndian(body));
Assert.Equal(4u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4)));
Assert.Equal(SocialActions.QueryItemManaOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
Assert.Equal(0x50000A01u,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
}
[Fact]
public void BuildPingRequest_HasOpcode0x01E9()
{

View file

@ -141,6 +141,19 @@ public sealed class WorldSessionCombatTests
Assert.Equal(SocialActions.BuildQueryHealth(1, 0x50000007u), captured);
}
[Fact]
public void SendQueryItemMana_UsesRetailQueryItemManaBuilder()
{
using var session = NewSession();
byte[]? captured = null;
session.GameActionCapture = body => captured = body;
session.SendQueryItemMana(0x50000A01u);
Assert.NotNull(captured);
Assert.Equal(SocialActions.BuildQueryItemMana(1, 0x50000A01u), captured);
}
[Fact]
public void SendUseWithTarget_UsesRetailBuilder()
{