Port the authored Link Status, Vitae, and Mini Game detail roots and register every indicator page with retail's one-active gmPanelUI owner. Helpful/Harmful and the new pages now replace Inventory, Character, or Magic at one canonical window position while preserving the DAT restore-previous flag. Correct the retail ping wire to its payload-free request/response, publish measured RTT, and port Vitae recovery XP from the live modifier and player properties. Keep transport packet-loss averaging and mini-game gameplay explicitly tracked under AP-110. Release build and all 5,814 tests pass with five intentional skips. Connected visual gate pending. Co-authored-by: OpenAI Codex <codex@openai.com>
9.5 KiB
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 @ 0x004BFA90gmUIElement_VitaeIndicator::PostInit @ 0x004E5EA0gmUIElement_VitaeIndicator::Update @ 0x004E5FE0gmUIElement_MiniGameIndicator::PostInit @ 0x004E6390gmUIElement_MiniGameIndicator::RecvNotice_BeginGame @ 0x004E6450gmUIElement_MiniGameIndicator::RecvNotice_EndGame @ 0x004E6470gmUIElement_LinkStatusIndicator::SetLinkState @ 0x004E65E0gmUIElement_LinkStatusIndicator::PostInit @ 0x004E66D0gmUIElement_LinkStatusIndicator::UpdateLinkState @ 0x004E6730gmUIElement_LinkStatusIndicator::UseTime @ 0x004E67C0gmUIElement_EffectsIndicator::PostInit @ 0x004E6930gmUIElement_EffectsIndicator::Update @ 0x004E6A50gmUIElement_BurdenIndicator::PostInit @ 0x004E6B80gmUIElement_BurdenIndicator::Update @ 0x004E6CE0LinkStatusHolder::GetConnectionStatus @ 0x00411380LinkStatusHolder::OnHeartbeat @ 0x004113D0gmPanelUI::SetupChildren @ 0x004BC9E0gmPanelUI::RecvNotice_SetPanelVisibility @ 0x004BC6F0gmLinkStatusUI::PostInit @ 0x004AADB0gmLinkStatusUI::Update @ 0x004AAEA0gmLinkStatusUI::ListenToElementMessage @ 0x004AB0B0gmLinkStatusUI::ListenToGlobalMessage @ 0x004AB110gmLinkStatusUI::RecvNotice_Ping @ 0x004AB150gmVitaeUI::PostInit @ 0x004A7240gmVitaeUI::Update @ 0x004A7400VitaeSystem::VitaeCPPoolThreshold @ 0x005C8FD0CM_Character::Event_RequestPing @ 0x006A19A0gmEffectsUI::SpellEffectMatchesUIType @ 0x004B76C0gmGamePlayUI::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 |
gmPanelUI::SetupChildren reads enum property 0x10000029 from each
registered child. The installed main-panel LayoutDesc resolves the indicator
detail pages to Link Status 8, Mini Game 9, and Vitae 15 (Helpful and
Harmful remain 4 and 5). They are not independent floating-window ids.
Pseudocode
Link indicator
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.
Link Status detail page
On shown:
register for time notices
pleaseRequestPing = true
Update()
On hidden:
unregister time notices
pleaseRequestPing = false
On time notice while visible:
if now >= nextUpdate:
nextUpdate = now + 5 seconds
Update()
Update:
replace text with localized connection explanation + 5/20-second legend
append 10-second packet-loss percentage with two decimal places
append ping in milliseconds, or "????" before a response
if pleaseRequestPing or now - lastPingRequest >= 120 seconds:
lastPingRequest = now
Event_RequestPing()
pleaseRequestPing = false
Event_RequestPing:
send F7B1, game-action sequence, opcode 0x01E9
// no payload
On empty 0x01EA PingResponse:
pingRoundTrip = monotonicNow - lastPingRequest
The network session owns the send timestamp and response sample. The retained page owns only retail's display and refresh cadence. acdream's transport does not yet expose the retail NAK/retransmission moving averages, so packet-loss averaging remains the explicit AP-110 residual rather than being guessed from ordinary packet sequence gaps.
Helpful and harmful effects
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
On PlayerDesc or VitaeChanged:
if enchantment registry has Vitae and vitae.modifier < 1.0:
SetState(Normal = 1)
else:
SetState(Ghosted = 13)
The detail page uses the same modifier:
lostPercent = 100 - int(vitae * 100)
if lostPercent <= 0:
show localized full-strength sentence
else:
vitaePool = player PropertyInt 129
deathLevel = player PropertyInt 139, falling back to Level (25)
threshold = int(((deathLevel ^ 2.5 * 2.5) + 20)
* vitae ^ 5 + 0.5)
remaining = threshold - vitaePool
show localized lost-percent sentence and remaining-XP sentence
The x87 powers omitted from the named pseudo-C output for
VitaeCPPoolThreshold were cross-checked against the official ACE
Player_Xp.cs port. The property ids and rounding expression agree.
Burden / character information
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
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.
The authored gmMiniGameUI root 0x1000016A is mounted and registered as
main panel 9, including its close behavior. It remains unreachable while the
indicator is Ghosted; board state and game actions remain with the unported
mini-game subsystem.
Shared main-panel ownership
For every toolbar or indicator detail page:
register page with gmPanelUI panel id
when shown:
hide the current child
move the requested child to the shared main-panel placement
show only the requested child
when a page with bool property 0x10000049 closes:
restore the deferred ordinary child
Helpful/Harmful, Link Status, Vitae, and the Mini Game shell therefore share the same retained host position as Inventory, Character, and Magic. They do not remember independent positions.
End character session
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
BurdenMathmatches retailEncumbranceSystem::EncumbranceCapacity @ 0x004FCC00andEncumbranceSystem::Load @ 0x004FCC40. - The existing enchantment parser preserves the separate Mult, Add, Vitae, and Cooldown registry buckets used by the retail indicator queries.
- Official ACE was cross-checked for the empty PingResponse and the vitae-pool threshold expression; the named retail client remains the behavior oracle.
- The Link Status and Vitae pages now use their authored DAT roots and localized strings. AP-110 retains only packet-loss averaging and mini-game gameplay for this slice; neither is replaced with an ad-hoc approximation.