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>
292 lines
11 KiB
Markdown
292 lines
11 KiB
Markdown
# 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.
|