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>
This commit is contained in:
Erik 2026-07-23 10:41:16 +02:00
parent 19e8863f4e
commit 6b1ae4fb76
27 changed files with 875 additions and 20 deletions

View file

@ -128,7 +128,7 @@ gives the per-state sprite ids (column 3 below).
| `m_elem_Icon_OpenContainer` | `0x10000450` | 3 | "this container is open" frame | `DirectState 0x06005D9C Alphablend` (dump:2232) |
| `m_elem_Icon_DragAccept` | `0x1000045A` | 3 | drag-rollover accept/reject frame | `ItemSlot_DragOver_Accept → 0x060011F9`, `_Reject → 0x060011F8`, `_DropIn → 0x060011F7` (dump:1174-1175,1258-1260) |
| `m_elem_Icon_Quantity` | `0x100004F5` | 12 (Text) | the stack-count number | — |
| `m_elem_Icon_Cooldown_10..100` | `0x1000054F..0x10000558` | 3 | 10-step radial cooldown ring | `DirectState 0x0600109D / 0x060012D9 / 0x06001DAE / 0x060067CF..D1 …` (dump:778-863) |
| `m_elem_Icon_Cooldown_10..100` | `0x1000054F..0x10000558` | 3 | 10-step radial cooldown ring | Common physical UIItem prototype `0x1000033E`: exact ordered `0x060067CF..0x060067D8`; ReadOrder 8, above every other cell layer. See `2026-07-23-retail-item-cooldown-pseudocode.md`. |
| `m_dragIcon` | `0x10000345` (created) | — | translucent drag-ghost | created via `CreateChildElement(this, dbobj, 0x10000345)`, `SetVisible(0)` (229738-229740) |
**The four named LayoutDesc states** that drive `m_elem_Icon` / `m_elem_Icon_DragAccept`

View file

@ -0,0 +1,174 @@
# 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:
```text
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:
```text
PWD2_Packed_CooldownID = 0x00000002
PWD2_Packed_CooldownDuration = 0x00000004
```
They occur after IconUnderlay and MaterialType, and before PetOwner:
```text
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:
```text
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
```text
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.