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:
parent
06016014bc
commit
52c529be86
15 changed files with 857 additions and 204 deletions
166
docs/research/2026-07-17-retail-indicator-bar-pseudocode.md
Normal file
166
docs/research/2026-07-17-retail-indicator-bar-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue