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>
174 lines
5.7 KiB
Markdown
174 lines
5.7 KiB
Markdown
# 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.
|