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:
parent
19e8863f4e
commit
6b1ae4fb76
27 changed files with 875 additions and 20 deletions
174
docs/research/2026-07-23-retail-item-cooldown-pseudocode.md
Normal file
174
docs/research/2026-07-23-retail-item-cooldown-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue