feat(ui): port retail gameplay indicator bar

Promote all seven LayoutDesc 0x21000071 controls to retained buttons, drive link quality, effects, Vitae, and burden from live state, and route Character Information plus end-session confirmation through the shared UI owners. Keep network timing in WorldSession and pin retail thresholds, flash cadence, authored states, and action routing with focused conformance tests.

Release build and all 5,807 tests pass with five intentional skips. Connected visual gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 10:02:44 +02:00
parent 06016014bc
commit 52c529be86
15 changed files with 857 additions and 204 deletions

View file

@ -44,6 +44,39 @@ Copy this block when adding a new issue:
---
## #224 — Gameplay indicator bar only implemented effect icons
**Status:** IN-PROGRESS
**Severity:** MEDIUM
**Filed:** 2026-07-17
**Component:** retained UI / gameplay indicators / session telemetry
**Description:** Retail LayoutDesc `0x21000071` is one seven-control strip for
link quality, helpful effects, harmful effects, vitae, burden/character
information, mini-game state, and end-character-session. acdream only promoted
the two effects controls to behavioral buttons; the other custom classes fell
back to inert generic DAT elements.
**Root cause / status:** `DatWidgetFactory` recognized only
`gmUIElement_EffectsIndicator`. The retained controller consequently had no
live bindings for session packet age, Vitae, or the player load ratio and did
not dispatch Character Information or the shared logout confirmation path.
**Files:** `src/AcDream.App/UI/Layout/IndicatorBarController.cs`;
`src/AcDream.App/UI/Layout/DatWidgetFactory.cs`;
`src/AcDream.Core.Net/WorldSession.cs`; `src/AcDream.App/UI/RetailUiRuntime.cs`.
**Research:**
`docs/research/2026-07-17-retail-indicator-bar-pseudocode.md`.
**Acceptance:** All seven authored controls render their retail sprites. Link
quality follows the 5/20/40-second thresholds and bad-link flash cadence;
helpful, harmful, Vitae, and burden react to live state; the burden icon opens
Character Information; effects open their matching panels; crossed swords use
the shared confirmation dialog and close gracefully only after acceptance.
---
## #223 — Primary panel resize stopped before the screen edge
**Status:** DONE — 2026-07-17 (connected visual gate accepted)

View file

@ -1,6 +1,6 @@
# acdream — strategic roadmap
**Status:** Living document. Updated 2026-07-16. **M3 active; automated implementation complete, visual gates pending.** M2's connected demo loop is complete. M3 now has the retail cast-intent/component/target lifecycle, exact live enchantment wire state, scoped Magic input, retained spell bar, spellbook, component book, positive/negative effects panels, authored effect-indicator buttons, and shared `gmPanelUI` switching over the Step 9 projectile/PhysicsScript/particle/portal foundation. The remaining M3 work is the connected single-client magic-UI/casting gate and final two-client portal-out/materialization observer gate.
**Status:** Living document. Updated 2026-07-17. **M3 active; automated implementation complete, visual gates pending.** M2's connected demo loop is complete. M3 now has the retail cast-intent/component/target lifecycle, exact live enchantment wire state, scoped Magic input, retained spell bar, spellbook, component book, positive/negative effects panels, the complete authored seven-control gameplay indicator strip, and shared `gmPanelUI` switching over the Step 9 projectile/PhysicsScript/particle/portal foundation. The remaining M3 work is the connected single-client magic-UI/casting gate and final two-client portal-out/materialization observer gate.
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
---
@ -20,8 +20,9 @@ active enchantment, then recall through the DAT-driven portal presentation.
3,956-row CSV is no longer a production dependency.
2. **Done (automated):** authored Magic combat page, eight favorite tabs,
equipped caster endowment, scoped retail keys, spellbook/component book,
positive/negative active-enchantment panels, Helpful/Harmful indicator
buttons, and the retail one-active-main-panel lifecycle. Connected-gate
positive/negative active-enchantment panels, the seven-control link/effect/
Vitae/burden/mini-game/session indicator strip, and the retail
one-active-main-panel lifecycle. Connected-gate
correction now also pins type-12 spellbook tabs, retail zero text margins,
and `PassToChildren` Closed/Open chrome through real-DAT fixtures.
3. **Pending user visual gate:** connected projectile/self-buff casts plus the

View file

@ -706,7 +706,11 @@ targeted/untargeted messages. Live enchantment add/remove/dispel/purge state is
fully parsed with layer identity and client-clock normalization. The authored
Magic combat page, eight favorite tabs, caster endowment, spellbook/component
book, scoped keys, positive/negative effects windows, and the authored
Helpful/Harmful indicators are mounted in the retained runtime. Main-panel
seven-control gameplay indicator strip are mounted in the retained runtime.
The strip now uses retail DAT states for link quality, Helpful/Harmful effects,
Vitae, burden, mini-game availability, and end-character-session; live link
age, player load, and enchantment registries drive their retail thresholds and
the burden/effect/logout actions use the shared panel/dialog owners. Main-panel
visibility now flows through retail's one-active `gmPanelUI` owner. M3 remains
active only for the connected single-client magic
UI/casting visual gate and the final two-client portal observer gate.

View file

@ -0,0 +1,166 @@
# Retail gameplay indicator bar pseudocode (2026-07-17)
## Scope and oracle
This note covers retail LayoutDesc `0x21000071` (`gmIndicatorsUI`) and its
seven 20x20 children. The behavior oracle is the named Sept-2013 retail
decomp. The retained port continues to use the authored LayoutDesc states and
sprites; code only chooses a state and dispatches the authored input action.
Named retail functions:
- `gmIndicatorsUI::ListenToElementMessage @ 0x004BFA90`
- `gmUIElement_VitaeIndicator::PostInit @ 0x004E5EA0`
- `gmUIElement_VitaeIndicator::Update @ 0x004E5FE0`
- `gmUIElement_MiniGameIndicator::PostInit @ 0x004E6390`
- `gmUIElement_MiniGameIndicator::RecvNotice_BeginGame @ 0x004E6450`
- `gmUIElement_MiniGameIndicator::RecvNotice_EndGame @ 0x004E6470`
- `gmUIElement_LinkStatusIndicator::SetLinkState @ 0x004E65E0`
- `gmUIElement_LinkStatusIndicator::PostInit @ 0x004E66D0`
- `gmUIElement_LinkStatusIndicator::UpdateLinkState @ 0x004E6730`
- `gmUIElement_LinkStatusIndicator::UseTime @ 0x004E67C0`
- `gmUIElement_EffectsIndicator::PostInit @ 0x004E6930`
- `gmUIElement_EffectsIndicator::Update @ 0x004E6A50`
- `gmUIElement_BurdenIndicator::PostInit @ 0x004E6B80`
- `gmUIElement_BurdenIndicator::Update @ 0x004E6CE0`
- `LinkStatusHolder::GetConnectionStatus @ 0x00411380`
- `LinkStatusHolder::OnHeartbeat @ 0x004113D0`
- `gmEffectsUI::SpellEffectMatchesUIType @ 0x004B76C0`
- `gmGamePlayUI::RecvNotice_EndCharacterSession @ 0x004EBEA0`
The installed retail LayoutDesc establishes the fixed child order and art:
| X | Element | Retail class | Meaning / input action |
|---:|---:|---:|---|
| 5 | `0x100000F8` | `0x10000003` | link status / `LinkStatusPanel` |
| 25 | `0x100000F5` | `0x10000002` | beneficial enchantments / `HelpfulSpellsPanel` |
| 45 | `0x100000F6` | `0x10000002` | harmful enchantments / `HarmfulSpellsPanel` |
| 65 | `0x100000F4` | `0x10000006` | vitae / `VitaePanel` |
| 85 | `0x100000F7` | `0x10000001` | burden / `CharacterInformationPanel` |
| 105 | `0x100000F3` | `0x10000004` | mini-game |
| 125 | `0x100000FA` | ordinary button | end character session |
## Pseudocode
### Link indicator
```text
PostInit:
base.PostInit()
listen to global time notices
semanticLinkState = Good
SetState(Connection_good = 17)
On each global-time notice:
if now - lastUpdate >= 4.0 seconds:
(secondsSinceServerPacket, connected) = GetConnectionStatus()
if !connected or secondsSinceServerPacket >= 40: Disconnected
else if secondsSinceServerPacket >= 20: Bad
else if secondsSinceServerPacket >= 5: Uncertain
else: Good
SetLinkState(result)
lastUpdate = now
if semanticLinkState == Bad and now - lastFlash >= 0.75 seconds:
toggle visual state between Connection_uncertain (18)
and Connection_bad (19)
lastFlash = now
SetLinkState(newState):
if newState == semanticLinkState: return
semanticLinkState = newState
set authored state Good=17, Uncertain=18, Bad=19, Disconnected=20
when entering Bad: lastFlash = now
OnHeartbeat:
lastHeardFromCurrentServer = now
packetLoss = linkAverages.averagePacketLoss
```
acdream records the last successfully decoded server datagram in
`WorldSession`; the retained controller reads an immutable link snapshot. This
keeps socket/session facts out of the UI while preserving retail's thresholds
and cadence.
### Helpful and harmful effects
```text
On PlayerDesc or enchantment change:
helpfulCount = 0
harmfulCount = 0
for each raw multiplicative/additive enchantment node:
spell = lookup spell metadata
if spell flags contain Beneficial: helpfulCount++
else: harmfulCount++
Helpful button = Normal when helpfulCount > 0, else Ghosted
Harmful button = Normal when harmfulCount > 0, else Ghosted
```
The indicator counts raw bucket-1/bucket-2 nodes. It does not use the effects
panel's later spell-category duel projection.
### Vitae
```text
On PlayerDesc or VitaeChanged:
if enchantment registry has Vitae and vitae.modifier < 1.0:
SetState(Normal = 1)
else:
SetState(Ghosted = 13)
```
### Burden / character information
```text
On PlayerDesc or LoadChanged:
if InqLoad fails: load = absent
if load is absent or load < 1.0: SetState(Unencumbered = 14)
else if load < 2.0: SetState(Encumbered = 15)
else: SetState(Heavily_encumbered = 16)
On click:
dispatch CharacterInformationPanel
```
`InqLoad` is the existing retail burden ratio:
`EncumbranceVal / EncumbranceCapacity(Strength, augmentation)`.
### Mini-game
```text
On BeginGame notice: SetState(Normal = 1)
On EndGame notice: SetState(Ghosted = 13)
```
The authored ghosted state is rendered now. The notices remain owned by the
still-unported mini-game subsystem (existing divergence AP-110), so this task
does not manufacture a game-active state from unrelated packets.
### End character session
```text
When child 0x100000FA sends Click:
SendNotice_EndCharacterSession(1)
RecvNotice_EndCharacterSession(1):
build the shared logout confirmation dialog using
ID_Client_EndCharacterSessionConfirm
on acceptance, end the character session
```
The acdream port reuses `RetailDialogFactory`; acceptance closes the current
client, whose existing `WorldSession.Dispose` sends retail's graceful
CharacterLogOff and transport Disconnect packets.
## Cross-checks and retained boundaries
- The already-ported `BurdenMath` matches retail
`EncumbranceSystem::EncumbranceCapacity @ 0x004FCC00` and
`EncumbranceSystem::Load @ 0x004FCC40`.
- The existing enchantment parser preserves the separate Mult, Add, Vitae, and
Cooldown registry buckets used by the retail indicator queries.
- ACE/reference source is not present in this worktree. No server behavior is
inferred for the strip: state is derived only from packets already parsed by
the client, and the named retail client remains the behavior oracle.
- Full Link Status, Vitae, and mini-game panels remain tracked by AP-110. This
port does not replace them with an ad-hoc panel.