acdream/docs/research/2026-07-23-retail-item-cooldown-pseudocode.md
Erik 6b1ae4fb76 feat(items): port retail shared cooldown overlays
Preserve public shared-cooldown metadata, resolve the authoritative cooldown enchantment with retail expiry semantics, and project the exact ten DAT-authored radial steps through the shared retained item-slot architecture.

Co-authored-by: Codex <codex@openai.com>
2026-07-23 10:41:16 +02:00

5.7 KiB

Retail item cooldown presentation

Date: 2026-07-23 Scope: the shared radial cooldown overlay drawn on inventory, equipment, container, and shortcut UIElement_UIItem cells.

Retail oracle

Named-retail symbols:

  • PublicWeenieDesc::UnPack — public object metadata, including the optional shared cooldown id and duration.
  • CEnchantmentRegistry::OnCooldown @ 0x005943C0 — resolves the live cooldown enchantment and its remaining duration.
  • UIElement_UIItem::PostInit @ 0x004E1870 — binds the ten authored cooldown children.
  • UIElement_UIItem::UpdateCooldownDisplay @ 0x004E1E20 — selects the visible radial step.
  • UIElement_UIItem::DoHeartbeat @ 0x004E2070 — refreshes the display.

The named pseudo-C loses the x87 expression in UpdateCooldownDisplay. Disassembly of the matching September 2013 client recovers the exact bucket expression:

truncate((remaining / itemCooldownDuration) * 10 + 1)

The result 1..9 selects the 10%..90% child. A result at or above 10 selects the 100% child. No child is visible when the cooldown is inactive or the item duration is non-positive.

Wire metadata

PublicWeenieDesc second-header flags:

PWD2_Packed_CooldownID       = 0x00000002
PWD2_Packed_CooldownDuration = 0x00000004

They occur after IconUnderlay and MaterialType, and before PetOwner:

if secondHeader & 0x00000002:
    cooldownId = read_u32()

if secondHeader & 0x00000004:
    cooldownDuration = read_f64()  // seconds

Absence remains distinct from a present zero. Assessment properties use the same semantic values: SharedCooldown PropertyInt 280 and CooldownDuration PropertyFloat 167.

Registry lookup

Retail stores cooldowns in the enchantment-registry cooldown list. The requested enchantment id is the item's shared cooldown group plus 0x8000. The list is examined in stored order and the first matching record decides the result:

OnCooldown(itemCooldownId, now):
    if itemCooldownId == 0:
        return inactive

    requestedSpellId = itemCooldownId + 0x8000

    for record in cooldownList, head to tail:
        if low16(record.spellId) != requestedSpellId:
            continue

        remaining = record.duration + record.startTime - now
        if remaining > 0:
            return active, remaining

        removeExpiredRecord(record)
        return inactive

    return inactive

The server remains authoritative for adding, updating, and removing the cooldown enchantment. Items that share a cooldown id therefore display the same live lockout without per-item client timers.

UI projection

UpdateCooldownDisplay(item, cooldownRegistry, now):
    hide cooldown children 10..100

    if item.cooldownDuration <= 0:
        return

    active, remaining =
        cooldownRegistry.OnCooldown(item.cooldownId + 0x8000, now)
    if not active:
        return

    step = truncate((remaining / item.cooldownDuration) * 10 + 1)

    if step >= 10:
        show cooldown_100
    else if step >= 1:
        show cooldown_[step * 10]

UIElement_UIItem::DoHeartbeat repeats this projection, so the artwork advances while no object packet changes.

DAT-authored presentation

The common physical UIItem prototype is element 0x1000033E in catalog LayoutDesc 0x21000037. Physical-item, container, and shortcut prototypes inherit the same children:

Step Element RenderSurface
10% 0x1000054F 0x060067CF
20% 0x10000550 0x060067D0
30% 0x10000551 0x060067D1
40% 0x10000552 0x060067D2
50% 0x10000553 0x060067D3
60% 0x10000554 0x060067D4
70% 0x10000555 0x060067D5
80% 0x10000556 0x060067D6
90% 0x10000557 0x060067D7
100% 0x10000558 0x060067D8

The empty-slot-only prototype 0x10000341 has no cooldown children. Cooldown children use ReadOrder 8, above icon/quantity/capacity (1), waiting/drag-accept (3), selection (4), shortcut digits (5), sell (6), and trade (7). The active cooldown sprite is therefore the last UIItem layer drawn.

Reference cross-checks

  • ACE serializes SharedCooldown (PropertyInt 280) and CooldownDuration (PropertyFloat 167) in the same optional public description order. Cooldown enchantments are placed in the registry's cooldown bucket.
  • Holtburger parses the same optional u32 id and f64 duration, and keeps enchantment spell/layer identifiers plus f64 start/duration values.

These references confirm the packet and registry interpretation. The named retail client and matching-binary disassembly remain authoritative for lookup order, the 0x8000 offset, bucket selection, and draw ordering.

acdream architecture

  • CreateObject preserves optional cooldown metadata through EntitySpawn, WeenieData, and ClientObject.
  • Spellbook.OnCooldown owns the ordered group lookup, remaining-time calculation, and exact expired-node removal.
  • ItemCooldownDisplay owns the pure ten-step display calculation.
  • ItemCooldownUiController binds every current and future UiItemList cell and evaluates represented items once in its pre-draw heartbeat.
  • ItemCooldownAssets imports all ten sprites from DAT. Missing assets fail visibly in diagnostics; no fabricated fallback ring is used.
  • UiItemSlot draws the selected sprite last, so inventory, paperdoll, external-container, and shortcut aliases share one mechanism.

Conformance cases

  • optional metadata present, absent, and present-zero;
  • exact 0x8000 shared-group mapping;
  • cooldown-bucket filtering and stored-order first-match behavior;
  • expired, zero-duration, and active records;
  • exact bucket boundaries and 100% clamping;
  • assessed property updates;
  • propagation to existing and future item-list cells;
  • exact production-DAT sprite sequence.