feat(net): AppraiseInfoParser — ArmorProfile/CreatureProfile/WeaponProfile + enchantment bitfields

Completes the deferred-in-previous-commit profile blob deserializers.
The AppraiseInfo wire format has 10 flags; previous commit handled the
6 property tables + SpellBook; this adds the 7 remaining structured
blobs:

- ArmorProfile: 8× f32 per-damage-type protection values
  (Slashing / Piercing / Bludgeoning / Cold / Fire / Acid / Nether /
  Lightning).
- ArmorLevel: 9× i32 per-body-part AL
  (Head / Chest / Abdomen / UpperArm / LowerArm / Hand / UpperLeg /
  LowerLeg / Foot).
- WeaponProfile: 10 mixed fields — u32 DamageType / WeaponTime /
  WeaponSkill / Damage, f64 DamageVariance / DamageMod / WeaponLength /
  MaxVelocity / WeaponOffense, u32 MaxVelocityEstimated.
- CreatureProfile: flag-gated — always u32 Flags + Health + HealthMax,
  optional 10× u32 attributes + vitals (flag 0x08 = ShowAttributes),
  optional 2× u16 highlight/color (flag 0x01 = HasBuffsDebuffs).
- Enchantment bitfields (ArmorEnchantmentBitfield /
  WeaponEnchantmentBitfield / ResistEnchantmentBitfield): each 2× u16
  (highlight, color).

HookProfile (flag 0x200) still deferred — needs its own structure port.

Parsed record expanded to carry all these; callers that previously
consumed PropertyBundle + SpellBook keep working, new fields are
nullable record-struct payloads.

Tests (+6): ArmorProfile round-trip, ArmorLevels, WeaponProfile with
mixed primitives, CreatureProfile with + without attributes flag,
ArmorEnchantment bitfield.

Build green, 172 Core.Net tests pass (up from 166).

Ref: ACE AppraiseInfo.cs:735-778 (writer), ArmorProfile.cs / ArmorLevel.cs /
WeaponProfile.cs / CreatureProfile.cs (structure writers).
Ref: r08 §4 opcode 0x00C9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-19 10:24:35 +02:00
parent 63b6922fc2
commit a9f366718d
2 changed files with 321 additions and 5 deletions

View file

@ -67,12 +67,67 @@ public static class AppraiseInfoParser
ArmorLevels = 0x0000_4000,
}
/// <summary>Per-damage-type protections from an ArmorProfile blob.</summary>
public readonly record struct ArmorProfile(
float SlashingProtection,
float PiercingProtection,
float BludgeoningProtection,
float ColdProtection,
float FireProtection,
float AcidProtection,
float NetherProtection,
float LightningProtection);
/// <summary>Per-slot AL (armor level) from an ArmorLevels blob.</summary>
public readonly record struct ArmorLevel(
int Head, int Chest, int Abdomen,
int UpperArm, int LowerArm, int Hand,
int UpperLeg, int LowerLeg, int Foot);
/// <summary>Melee/missile weapon statistics from a WeaponProfile blob.</summary>
public readonly record struct WeaponProfile(
uint DamageType,
uint WeaponTime,
uint WeaponSkill,
uint Damage,
double DamageVariance,
double DamageMod,
double WeaponLength,
double MaxVelocity,
double WeaponOffense,
uint MaxVelocityEstimated);
/// <summary>Creature vitals + (optionally) attributes from a CreatureProfile blob.</summary>
public readonly record struct CreatureProfile(
uint Flags,
uint Health,
uint HealthMax,
uint? Strength,
uint? Endurance,
uint? Quickness,
uint? Coordination,
uint? Focus,
uint? Self,
uint? Stamina,
uint? Mana,
uint? StaminaMax,
uint? ManaMax,
ushort? AttributeHighlights,
ushort? AttributeColors);
public readonly record struct Parsed(
uint Guid,
IdentifyResponseFlags Flags,
bool Success,
PropertyBundle Properties,
uint[] SpellBook);
uint[] SpellBook,
ArmorProfile? ArmorProfile,
CreatureProfile? CreatureProfile,
WeaponProfile? WeaponProfile,
ArmorLevel? ArmorLevels,
(ushort Highlight, ushort Color)? ArmorEnchantments,
(ushort Highlight, ushort Color)? WeaponEnchantments,
(ushort Highlight, ushort Color)? ResistEnchantments);
/// <summary>
/// Parse a full IdentifyObjectResponse payload.
@ -92,6 +147,11 @@ public static class AppraiseInfoParser
var flags = (IdentifyResponseFlags)rawFlags;
var bundle = new PropertyBundle();
uint[] spellBook = Array.Empty<uint>();
ArmorProfile? armor = null;
CreatureProfile? creature = null;
WeaponProfile? weapon = null;
ArmorLevel? levels = null;
(ushort H, ushort C)? armorEnc = null, weaponEnc = null, resistEnc = null;
try
{
@ -110,16 +170,132 @@ public static class AppraiseInfoParser
if (flags.HasFlag(IdentifyResponseFlags.SpellBook))
spellBook = ReadSpellBook(payload, ref pos);
// Profile blobs: their sizes aren't trivially predictable
// without porting the whole profile structures. We stop here
// and let the caller handle the bundle they have.
// Profile blobs (AppraiseInfo.Write line 751 onward, in flag order).
if (flags.HasFlag(IdentifyResponseFlags.ArmorProfile))
armor = ReadArmorProfile(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.CreatureProfile))
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.ArmorEnchantmentBitfield))
armorEnc = ReadEnchantmentBitfield(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.WeaponEnchantmentBitfield))
weaponEnc = ReadEnchantmentBitfield(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.ResistEnchantmentBitfield))
resistEnc = ReadEnchantmentBitfield(payload, ref pos);
if (flags.HasFlag(IdentifyResponseFlags.ArmorLevels))
levels = ReadArmorLevel(payload, ref pos);
}
catch (FormatException)
{
// Malformed table — return what we got so far.
}
return new Parsed(guid, flags, success != 0, bundle, spellBook);
return new Parsed(
guid, flags, success != 0, bundle, spellBook,
armor, creature, weapon, levels,
armorEnc, weaponEnc, resistEnc);
}
// ── Profile readers ──────────────────────────────────────────────────────
private static ArmorProfile ReadArmorProfile(ReadOnlySpan<byte> src, ref int pos)
{
return new ArmorProfile(
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos),
ReadF32(src, ref pos));
}
private static ArmorLevel ReadArmorLevel(ReadOnlySpan<byte> src, ref int pos)
{
return new ArmorLevel(
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos),
(int)ReadU32(src, ref pos));
}
private static WeaponProfile ReadWeaponProfile(ReadOnlySpan<byte> src, ref int pos)
{
return new WeaponProfile(
DamageType: ReadU32(src, ref pos),
WeaponTime: ReadU32(src, ref pos),
WeaponSkill: ReadU32(src, ref pos),
Damage: ReadU32(src, ref pos),
DamageVariance: ReadF64(src, ref pos),
DamageMod: ReadF64(src, ref pos),
WeaponLength: ReadF64(src, ref pos),
MaxVelocity: ReadF64(src, ref pos),
WeaponOffense: ReadF64(src, ref pos),
MaxVelocityEstimated: ReadU32(src, ref pos));
}
private static CreatureProfile ReadCreatureProfile(ReadOnlySpan<byte> src, ref int pos)
{
uint flags = ReadU32(src, ref pos);
uint health = ReadU32(src, ref pos);
uint healthMax = ReadU32(src, ref pos);
uint? str = null, end = null, quic = null, coord = null, focus = null, self = null;
uint? sta = null, mana = null, staMax = null, manaMax = null;
ushort? attrHighlights = null, attrColors = null;
// Flag 0x08 = ShowAttributes
if ((flags & 0x08u) != 0)
{
str = ReadU32(src, ref pos);
end = ReadU32(src, ref pos);
quic = ReadU32(src, ref pos);
coord = ReadU32(src, ref pos);
focus = ReadU32(src, ref pos);
self = ReadU32(src, ref pos);
sta = ReadU32(src, ref pos);
mana = ReadU32(src, ref pos);
staMax = ReadU32(src, ref pos);
manaMax= ReadU32(src, ref pos);
}
// Flag 0x01 = HasBuffsDebuffs. ACE writes two u16 fields, 4 bytes total.
if ((flags & 0x01u) != 0)
{
if (src.Length - pos < 4) throw new FormatException("truncated creature buffs");
attrHighlights = BinaryPrimitives.ReadUInt16LittleEndian(src.Slice(pos)); pos += 2;
attrColors = BinaryPrimitives.ReadUInt16LittleEndian(src.Slice(pos)); pos += 2;
}
return new CreatureProfile(
flags, health, healthMax,
str, end, quic, coord, focus, self,
sta, mana, staMax, manaMax,
attrHighlights, attrColors);
}
private static (ushort Highlight, ushort Color) ReadEnchantmentBitfield(
ReadOnlySpan<byte> src, ref int pos)
{
if (src.Length - pos < 4) throw new FormatException("truncated enchantment bitfield");
ushort highlight = BinaryPrimitives.ReadUInt16LittleEndian(src.Slice(pos)); pos += 2;
ushort color = BinaryPrimitives.ReadUInt16LittleEndian(src.Slice(pos)); pos += 2;
return (highlight, color);
}
private static float ReadF32(ReadOnlySpan<byte> src, ref int pos)
{
if (src.Length - pos < 4) throw new FormatException("truncated f32");
float v = BinaryPrimitives.ReadSingleLittleEndian(src.Slice(pos));
pos += 4;
return v;
}
// ── Table readers ────────────────────────────────────────────────────────