acdream/docs/research/2026-07-11-retail-selected-health-policy-pseudocode.md
Erik 2644d1d527 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>
2026-07-11 11:42:04 +02:00

3.5 KiB

Retail selected-object health policy pseudocode — 2026-07-11

Oracle

  • gmToolbarUI::HandleSelectionChanged @ 0x004BF380, health branch 0x004BF725..0x004BF76C.
  • ClientCombatSystem::ObjectIsAttackable @ 0x0056A600.
  • ACCWeenieObject::IsPlayer/IsPK/IsPKLite @ 0x0058C890/0x0058C8B0/0x0058C8A0.
  • PublicWeenieDesc::UnPack @ 0x005AD470, second-header PetOwner read 0x005AD7EC..0x005AD7F8.
  • Struct/flag definitions: acclient.h PublicWeenieDesc, PublicWeenieDescPackHeader2::PWD2_Packed_PetOwner = 0x8.

ACE cross-check: WorldObject_Networking.SerializeCreateObject writes the second header after the ordinary optional tail in this order: IconUnderlay, MaterialType, CooldownId, CooldownDuration (double), PetOwner (u32), then alignment. Its UpdateObjectDescriptionFlags maps the same PWD player/attackable/PK/Free-PK/PKLite bits read by retail.

Chorizite/holtburger cross-check: the independently generated Chorizite.ACProtocol/Types/PublicWeenieDesc schema and holtburger inventory client inventory are catalogued in deepdives/r06-items-inventory.md; that schema pins the same Header2 0x01/0x02/0x04/0x08 order and field widths. Neither interpretation aid changes the client behavior above; the named retail branch order remains ground truth.

Constants

ITEM_TYPE_CREATURE = 0x10

BF_PLAYER          = 0x00000008
BF_ATTACKABLE      = 0x00000010
BF_PLAYER_KILLER   = 0x00000020
BF_FREE_PKSTATUS   = 0x00200000
BF_PKLITE_PKSTATUS = 0x02000000

PWD2_PET_OWNER     = 0x00000008

Exact pseudocode

ToolbarShouldQueryHealth(selectedId):
    selected = lookup selectedId
    if selected missing:
        return false

    if selected.stackSize > 1:
        return false                         // stack UI is mutually exclusive

    isPlayer = (selected.pwd.bitfield & BF_PLAYER) != 0
    isPet = selected.pwd.petOwner != 0

    if isPlayer OR isPet OR ObjectIsAttackable(selectedId):
        QueryHealth(selectedId)
        return true

    return false

ObjectIsAttackable(targetId):
    if targetId == 0 OR targetId == localPlayerId:
        return true

    target = lookup targetId
    if target missing OR (target.type & ITEM_TYPE_CREATURE) == 0:
        return false

    if (target.pwd.bitfield & BF_FREE_PKSTATUS) != 0:
        return true

    player = lookup localPlayerId
    if player missing:
        return false

    if (player.pwd.bitfield & BF_FREE_PKSTATUS) != 0:
        return true

    if target.IsPlayer():
        if target.IsPK() AND player.IsPK():
            return true
        if target.IsPKLite() AND player.IsPKLite():
            return true
        return false

    if target.pwd.petOwner != 0:
        return false

    return (target.pwd.bitfield & BF_ATTACKABLE) != 0

Toolbar consequence

The toolbar's outer IsPlayer || petOwner test runs before ObjectIsAttackable. Therefore every selected player and every selected pet gets a health query regardless of PK pairing. The full PK/PKLite branches remain part of the faithful reusable ObjectIsAttackable port, but they do not decide toolbar player visibility. Relative to acdream's former PWD-only approximation, the observable missing toolbar cases are:

  1. the local player/self (formerly rejected explicitly),
  2. creatures with PetOwner != 0,
  3. Free-PK creatures, or ordinary creatures while the local player is Free-PK.

Friendly non-pet NPCs remain name-only. Non-creature objects remain name-only even when they carry BF_ATTACKABLE (the existing attackable-door regression guard).