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

@ -566,6 +566,29 @@ public sealed class CreateObjectTests
Assert.Equal("Pyreal Scarabs", parsed.Value.PluralName);
}
[Fact]
public void WeenieHeader_secondHeaderPetOwner_isPreservedAfterPrecedingFields()
{
// PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8:
// MaterialType, CooldownId, CooldownDuration, then PetOwner.
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000024u,
name: "Combat Pet",
itemType: (uint)ItemType.Creature,
objectDescriptionFlags: 0x04000000u,
weenieFlags: 0x80000000u,
weenieFlags2: 0x0000000Eu,
materialType: 7u,
cooldownId: 11u,
cooldownDuration: 12.5,
petOwnerId: 0x50000001u);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal(0x50000001u, parsed.Value.PetOwnerId);
}
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
uint guid,
string name,
@ -601,7 +624,11 @@ public sealed class CreateObjectTests
byte? radarBlipColor = null,
byte? radarBehavior = null,
byte? combatUse = null,
ushort movementSeq = 0)
ushort movementSeq = 0,
uint materialType = 0,
uint cooldownId = 0,
double cooldownDuration = 0,
uint petOwnerId = 0)
{
var bytes = new List<byte>();
WriteU32(bytes, CreateObject.Opcode);
@ -691,6 +718,16 @@ public sealed class CreateObjectTests
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
if ((weenieFlags & 0x80000000u) != 0) WriteU32(bytes, materialType); // MaterialType
if ((weenieFlags2 & 0x00000002u) != 0) WriteU32(bytes, cooldownId); // CooldownId
if ((weenieFlags2 & 0x00000004u) != 0)
{
Span<byte> tmp = stackalloc byte[8];
BinaryPrimitives.WriteDoubleLittleEndian(tmp, cooldownDuration);
bytes.AddRange(tmp.ToArray());
}
if ((weenieFlags2 & 0x00000008u) != 0) WriteU32(bytes, petOwnerId); // PetOwner
Align4(bytes);
return bytes.ToArray();
}