acdream/docs/research/2026-07-17-retail-indicator-bar-pseudocode.md
Erik d1d603105f fix(ui): complete retail indicator detail panels
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>
2026-07-17 11:36:43 +02:00

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 @ 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

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.

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 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.