feat(ui): port retail appraisal panel

Preserve retail's one-pending-appraisal busy lifetime, parse the complete gated response, and mount the authored examination layout in the shared main-panel host. Keep known 3D preview and inscription-write gaps explicit in AP-110.
This commit is contained in:
Erik 2026-07-23 11:34:08 +02:00
parent 6b1ae4fb76
commit 643cdfe66e
24 changed files with 17132 additions and 40 deletions

View file

@ -49,6 +49,50 @@ public sealed class GameEventWiringTests
return (dispatcher, items, combat, spellbook, chat);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WireAll_IdentifyObjectResponse_ForwardsCompleteResultAndMergesOnlySuccess(
bool success)
{
const uint guid = 0x50000001u;
var dispatcher = new GameEventDispatcher();
var items = new ClientObjectTable();
items.UpsertProperties(guid, new PropertyBundle());
AppraiseInfoParser.Parsed? received = null;
GameEventWiring.WireAll(
dispatcher,
items,
new CombatState(),
new Spellbook(),
new ChatLog(),
onAppraisal: appraisal => received = appraisal);
byte[] payload = new byte[24];
BinaryPrimitives.WriteUInt32LittleEndian(payload, guid);
BinaryPrimitives.WriteUInt32LittleEndian(
payload.AsSpan(4),
(uint)AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), success ? 1u : 0u);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(12), 1);
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(14), 16);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(16), 25u);
BinaryPrimitives.WriteInt32LittleEndian(payload.AsSpan(20), 42);
GameEventEnvelope? envelope = GameEventEnvelope.TryParse(
WrapEnvelope(GameEventType.IdentifyObjectResponse, payload));
Assert.NotNull(envelope);
dispatcher.Dispatch(envelope.Value);
Assert.NotNull(received);
Assert.Equal(success, received.Value.Success);
Assert.Equal(42, received.Value.Properties.Ints[25u]);
if (success)
Assert.Equal(42, items.Get(guid)!.Properties.Ints[25u]);
else
Assert.False(items.Get(guid)!.Properties.Ints.ContainsKey(25u));
}
[Fact]
public void WireAll_ChannelBroadcast_RoutesToChatLog()

View file

@ -183,6 +183,19 @@ public sealed class AppraiseInfoParserTests
Assert.Null(AppraiseInfoParser.TryParse(new byte[4]));
}
[Fact]
public void TryParse_TruncatedGatedField_RejectsEntirePacket()
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
bw.Write(0x50000001u);
bw.Write((uint)AppraiseInfoParser.IdentifyResponseFlags.HookProfile);
bw.Write(1u);
bw.Write(0x3u);
Assert.Null(AppraiseInfoParser.TryParse(ms.ToArray()));
}
// ── Profile blobs ────────────────────────────────────────────────────────
[Fact]
@ -322,4 +335,31 @@ public sealed class AppraiseInfoParserTests
Assert.Equal((ushort)0x00FF, parsed.Value.ArmorEnchantments!.Value.Highlight);
Assert.Equal((ushort)0x0042, parsed.Value.ArmorEnchantments.Value.Color);
}
[Fact]
public void TryParse_HookProfile_PreservesAlignmentForFollowingFields()
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms);
bw.Write(0x50000001u);
bw.Write((uint)(
AppraiseInfoParser.IdentifyResponseFlags.HookProfile
| AppraiseInfoParser.IdentifyResponseFlags.ArmorEnchantmentBitfield));
bw.Write(1u);
bw.Write(0x0000000Bu);
bw.Write(0x00100000u);
bw.Write(0x00000004u);
bw.Write((ushort)0x1234);
bw.Write((ushort)0x5678);
var parsed = AppraiseInfoParser.TryParse(ms.ToArray());
Assert.NotNull(parsed);
Assert.Equal(
new AppraiseInfoParser.HookProfile(0xBu, 0x00100000u, 4u),
parsed.Value.HookProfile);
Assert.Equal(
((ushort)0x1234, (ushort)0x5678),
parsed.Value.ArmorEnchantments);
}
}