Port the authored effect row template, remaining-time and selection details, synchronize the full gmPanelUI child geometry, and route the burden indicator to Character Information panel 3. Co-authored-by: OpenAI Codex <codex@openai.com>
11 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
apply the shared main-panel rectangle to the requested child
show only the requested child
when a page with bool property 0x10000049 closes:
restore the deferred ordinary child
Character Information, Helpful/Harmful, Link Status, Vitae, and the Mini Game shell therefore share the same retained host size and position as Inventory, Character/Skills, and Magic. They do not remember independent rectangles.
The burden/backpack indicator sends panel id 3, whose production child is
root 0x10000183 under LayoutDesc 0x2100006E. Panel 11 is the separate
Attributes/Skills child and is not the burden indicator destination.
Helpful and harmful effect rows
gmEffectsUI::RebuildList @ 0x004B8350:
sort visible enchantment tokens alphabetically by spell name
AddItemFromTemplateList(template 0)
EffectInfoRegion row template 0x10000128 (LayoutDesc 0x2100001B):
icon 0x10000129 at x=0, 32 x 32
name 0x1000012A at x=37, 188 x 32
time left 0x1000012B at x=225, 50 x 32
gmEffectsUI::UpdateSelection @ 0x004B7F90:
no selected spell -> display ID_Effects_Info_SelectASpell
selected spell -> display name + "\n\n" + description
matching rows use Highlight state 6; other rows use Normal state 1
gmEffectsUI::SetSelectedSpell @ 0x004B8290:
clicking the selected spell again clears selection
The installed local.dat resolves ID_Effects_Info_SelectASpell to
SELECT A SPELL. Remaining finite durations use m:ss below one hour and
h:mm:ss at one hour or more.
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.