# 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` - `gmPanelUI::SetupChildren @ 0x004BC9E0` - `gmPanelUI::RecvNotice_SetPanelVisibility @ 0x004BC6F0` - `gmLinkStatusUI::PostInit @ 0x004AADB0` - `gmLinkStatusUI::Update @ 0x004AAEA0` - `gmLinkStatusUI::ListenToElementMessage @ 0x004AB0B0` - `gmLinkStatusUI::ListenToGlobalMessage @ 0x004AB110` - `gmLinkStatusUI::RecvNotice_Ping @ 0x004AB150` - `gmVitaeUI::PostInit @ 0x004A7240` - `gmVitaeUI::Update @ 0x004A7400` - `VitaeSystem::VitaeCPPoolThreshold @ 0x005C8FD0` - `CM_Character::Event_RequestPing @ 0x006A19A0` - `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 | `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 ```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. ### Link Status detail page ```text 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 ```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) ``` The detail page uses the same modifier: ```text 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 ```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. 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 ```text 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 ```text 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 ```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. - 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.