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

@ -34,7 +34,7 @@ namespace AcDream.Core.Net.Messages;
/// if (flags & StringStatsTable) packedTable<u32 key, string16L value>
/// if (flags & DidStatsTable) packedTable<u32 key, u32 value>
/// if (flags & SpellBook) u32 count, u32[count] spellIds
/// ...armor/creature/weapon/hook profile blobs follow (deferred)
/// ...armor/creature/weapon/hook profile blobs follow
/// </code>
/// </para>
///
@ -48,10 +48,9 @@ namespace AcDream.Core.Net.Messages;
/// </para>
///
/// <para>
/// Returns a fully-populated <see cref="PropertyBundle"/>. Profile
/// blobs (ArmorProfile / CreatureProfile / WeaponProfile / HookProfile)
/// are recognised via the flag bits but not yet deserialized — the
/// parser seeks past them to keep the flag walk aligned.
/// Returns the complete gated appraisal payload. A malformed gated field
/// rejects the packet rather than returning a partial profile whose cursor
/// could be misaligned for every later field.
/// </para>
/// </summary>
public static class AppraiseInfoParser
@ -107,6 +106,12 @@ public static class AppraiseInfoParser
double WeaponOffense,
uint MaxVelocityEstimated);
/// <summary>Hook-container item metadata serialized as three consecutive u32 values.</summary>
public readonly record struct HookProfile(
uint Flags,
uint ValidLocations,
uint AmmoType);
/// <summary>Creature vitals + (optionally) attributes from a CreatureProfile blob.</summary>
public readonly record struct CreatureProfile(
uint Flags,
@ -134,6 +139,7 @@ public static class AppraiseInfoParser
ArmorProfile? ArmorProfile,
CreatureProfile? CreatureProfile,
WeaponProfile? WeaponProfile,
HookProfile? HookProfile,
ArmorLevel? ArmorLevels,
(ushort Highlight, ushort Color)? ArmorEnchantments,
(ushort Highlight, ushort Color)? WeaponEnchantments,
@ -160,6 +166,7 @@ public static class AppraiseInfoParser
ArmorProfile? armor = null;
CreatureProfile? creature = null;
WeaponProfile? weapon = null;
HookProfile? hook = null;
ArmorLevel? levels = null;
(ushort H, ushort C)? armorEnc = null, weaponEnc = null, resistEnc = null;
@ -187,7 +194,8 @@ public static class AppraiseInfoParser
creature = ReadCreatureProfile(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.WeaponProfile))
weapon = ReadWeaponProfile(payload, ref pos);
// HookProfile (0x200) — not deserialized; would need HookProfile struct port.
if (flags.HasFlag(IdentifyResponseFlags.HookProfile))
hook = ReadHookProfile(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.ArmorEnchantmentBitfield))
armorEnc = ReadEnchantmentBitfield(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.WeaponEnchantmentBitfield))
@ -199,12 +207,12 @@ public static class AppraiseInfoParser
}
catch (FormatException)
{
// Malformed table — return what we got so far.
return null;
}
return new Parsed(
guid, flags, success != 0, bundle, spellBook,
armor, creature, weapon, levels,
armor, creature, weapon, hook, levels,
armorEnc, weaponEnc, resistEnc);
}
@ -252,6 +260,12 @@ public static class AppraiseInfoParser
MaxVelocityEstimated: ReadU32(src, ref pos));
}
private static HookProfile ReadHookProfile(ReadOnlySpan<byte> src, ref int pos)
=> new(
Flags: ReadU32(src, ref pos),
ValidLocations: ReadU32(src, ref pos),
AmmoType: ReadU32(src, ref pos));
private static CreatureProfile ReadCreatureProfile(ReadOnlySpan<byte> src, ref int pos)
{
uint flags = ReadU32(src, ref pos);