fix(ui): port exact selected health policy

Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 11:42:04 +02:00
parent 52a80dc531
commit 2644d1d527
18 changed files with 416 additions and 50 deletions

View file

@ -197,7 +197,9 @@ public static class CreateObject
byte? CombatUse = null,
// PublicWeenieDesc._plural_name, gated by WeenieHeader flag 0x1.
// ACCWeenieObject::GetObjectName(NAME_APPROPRIATE) selects this for stacks.
string? PluralName = null);
string? PluralName = null,
// PublicWeenieDesc._pet_owner, gated by second-header flag 0x8.
uint? PetOwnerId = null);
/// <summary>
/// The relevant subset of the server-sent <c>MovementData</c> /
@ -760,6 +762,7 @@ public static class CreateObject
uint iconUnderlayId = 0;
uint uiEffects = 0;
uint weenieFlags2 = 0;
uint? petOwnerId = null;
try
{
// BF_INCLUDES_SECOND_HEADER = 0x04000000 per acclient.h:6458
@ -952,6 +955,28 @@ public static class CreateObject
{
iconUnderlayId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
}
// PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8 and ACE
// SerializeCreateObject both place these fields after IconUnderlay.
if ((weenieFlags & 0x80000000u) != 0) // MaterialType u32
{
if (body.Length - pos < 4) throw new FormatException("trunc MaterialType");
pos += 4;
}
if ((weenieFlags2 & 0x00000002u) != 0) // CooldownId u32
{
if (body.Length - pos < 4) throw new FormatException("trunc CooldownId");
pos += 4;
}
if ((weenieFlags2 & 0x00000004u) != 0) // CooldownDuration f64
{
if (body.Length - pos < 8) throw new FormatException("trunc CooldownDuration");
pos += 8;
}
if ((weenieFlags2 & 0x00000008u) != 0) // PetOwner u32
{
if (body.Length - pos < 4) throw new FormatException("trunc PetOwner");
petOwnerId = ReadU32(body, ref pos);
}
}
catch { /* truncated weenie tail — keep whatever we got. */ }
@ -974,7 +999,8 @@ public static class CreateObject
Workmanship: wWorkmanship,
RadarBlipColor: radarBlipColor, RadarBehavior: radarBehavior,
CombatUse: combatUse,
PluralName: pluralName);
PluralName: pluralName,
PetOwnerId: petOwnerId);
// Local helper: if we ran out of fields past PhysicsData, still
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).

View file

@ -88,5 +88,6 @@ public static class ObjectTableWiring
RadarBehavior: s.RadarBehavior,
PublicWeenieBitfield: s.ObjectDescriptionFlags,
CombatUse: s.CombatUse,
PluralName: s.PluralName);
PluralName: s.PluralName,
PetOwnerId: s.PetOwnerId);
}

View file

@ -124,7 +124,8 @@ public sealed class WorldSession : IDisposable
byte? RadarBlipColor = null,
byte? RadarBehavior = null,
byte? CombatUse = null,
string? PluralName = null);
string? PluralName = null,
uint? PetOwnerId = null);
/// <summary>
/// Projects the wire-level CreateObject result into the stable session
@ -177,7 +178,8 @@ public sealed class WorldSession : IDisposable
RadarBlipColor: parsed.RadarBlipColor,
RadarBehavior: parsed.RadarBehavior,
CombatUse: parsed.CombatUse,
PluralName: parsed.PluralName);
PluralName: parsed.PluralName,
PetOwnerId: parsed.PetOwnerId);
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
public event Action<EntitySpawn>? EntitySpawned;