docs(ui): pin retained widget retail oracles
This commit is contained in:
parent
5931a820ea
commit
44f9ec13d9
10 changed files with 2753 additions and 73 deletions
|
|
@ -1576,22 +1576,22 @@ R5/R6 touches the action list. **Where:**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #196 — Inventory window close never notifies the controller (NoLongerViewingContents skipped)
|
## #196 — External-container lifecycle and retained window events are missing
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
**Severity:** MEDIUM
|
**Severity:** MEDIUM
|
||||||
**Filed:** 2026-07-02
|
**Filed:** 2026-07-02
|
||||||
**Component:** ui / net
|
**Component:** ui / net
|
||||||
|
|
||||||
**Description:** `UiRoot`'s window registry flips `Visible` with no event, and the inventory close button routes to `GameWindow.CloseRetailWindow` (HideWindow + toolbar sync only). `InventoryController` never learns the window closed: `NoLongerViewingContents (0x0195)` is not sent for the viewed side pack, `_openContainer` stays stale, and reopening shows the old bag grid without re-sending Use.
|
**Description:** `UiRoot`'s window registry flips `Visible` with no lifecycle event, toolbar-button sync is manually pushed at each mutation site, and acdream has no retained external/ground-container window. The original issue incorrectly assigned `NoLongerViewingContents (0x0195)` to closing or navigating the owned inventory's side bags. Named-retail evidence instead ties it to `ClientUISystem.groundObject`: replacing or closing an external container sends `0x0195` exactly once. The owned `gmInventoryUI` side-bag path has no corresponding static send.
|
||||||
|
|
||||||
**Root cause / status:** Review finding (theme T2). Fix direction: add `WindowShown`/`WindowHidden` events to the UiRoot window registry; InventoryController closes out the viewed container on hide; toolbar-button sync subscribes instead of the manual `SyncToolbarWindowButtons()` push at every mutation site.
|
**Root cause / status:** Review finding (theme T2), re-framed by the 2026-07-10 named-retail trace. Add typed retained-window lifecycle events and subscriber-driven toolbar state. Port a separate external-container owner whose ground-object replacement/close path emits one `0x0195`. Do not add that packet to owned side-bag navigation without a live packet trace.
|
||||||
|
|
||||||
**Files:** `src/AcDream.App/UI/UiRoot.cs:541-572`, `src/AcDream.App/UI/Layout/InventoryController.cs:404`, `src/AcDream.App/Rendering/GameWindow.cs` (`CloseRetailWindow`/`SyncToolbarWindowButtons`).
|
**Files:** `src/AcDream.App/UI/UiRoot.cs`, `src/AcDream.App/UI/Layout/InventoryController.cs`, `src/AcDream.App/Rendering/GameWindow.cs` (`CloseRetailWindow`/`SyncToolbarWindowButtons`); future external-container controller.
|
||||||
|
|
||||||
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`; correction oracle `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md`.
|
||||||
|
|
||||||
**Acceptance:** closing the inventory window sends NoLongerViewingContents for the open side pack; reopening shows the main pack; no manual Sync call sites remain.
|
**Acceptance:** replacing/closing an external container sends `NoLongerViewingContents` exactly once; owned main/side-pack navigation and closing send nothing absent contrary trace evidence; lifecycle subscribers replace manual toolbar sync calls; repeat hiding is idempotent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1638,22 +1638,22 @@ states `0x28` and `0x29`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #199 — Character raise flow: determine and port authoritative/predicted reconciliation
|
## #199 — Port the server-authoritative character raise flow
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
**Severity:** MEDIUM
|
**Severity:** MEDIUM
|
||||||
**Filed:** 2026-07-02
|
**Filed:** 2026-07-02
|
||||||
**Component:** ui / core
|
**Component:** ui / core
|
||||||
|
|
||||||
**Description:** Item moves reconcile optimistic state through `RecordPending`/`ConfirmMove`/`RollbackMove` (0x0022/0x0023/0x00A0); character raises currently apply optimistically (`CharacterSheetProvider.HandleRaiseRequest` → `ApplyLocalRaise`) with no pending snapshot and no rollback. First trace whether retail predicts or waits for authoritative state. Then either remove prediction or port a faithful pending/confirm/rollback path. Register row AP-73.
|
**Description:** Character raises currently mutate XP/credits/ranks optimistically (`CharacterSheetProvider.HandleRaiseRequest` → `ApplyLocalRaise`). Named retail does not predict: it permits one request in flight, ghosts the clicked button, and leaves displayed state unchanged until an authoritative quality-change element message supplies the new values.
|
||||||
|
|
||||||
**Root cause / status:** Review finding (theme T2); the spends now at least go through eventful store APIs (extraction commit). The old issue assumed retail predicts without evidence; Wave 8 requires the named/cdb behavior pin before choosing the reconciliation mechanism.
|
**Root cause / status:** Review finding (theme T2), resolved oracle question on 2026-07-10. Remove local mutation, add a one-in-flight owner, ghost the requested button, and clear awaiting state from the authoritative quality-change path. The exact rejection cleanup still needs a live ACE trace; do not mask it with a timer/retry.
|
||||||
|
|
||||||
**Files:** `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`HandleRaiseRequest`), `src/AcDream.Core/Player/LocalPlayerState.cs` (`Apply*Raise`).
|
**Files:** `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` (`HandleRaiseRequest`), `src/AcDream.Core/Player/LocalPlayerState.cs` (`Apply*Raise`).
|
||||||
|
|
||||||
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`.
|
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`; authoritative flow `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md`.
|
||||||
|
|
||||||
**Acceptance:** a raise the server rejects (e.g. insufficient XP race) visibly reverts within one echo round-trip; a confirmed raise never flickers.
|
**Acceptance:** sending a raise does not change local XP/credits/ranks; a second request is blocked while one is awaiting; the matching authoritative update refreshes state and re-enables the button; rejection/relog cleanup follows captured server behavior without retry loops.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Documented approximation (AP) — 100 active rows
|
## 3. Documented approximation (AP) — 99 active rows
|
||||||
|
|
||||||
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
||||||
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
||||||
|
|
@ -179,7 +179,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
||||||
| AP-71 | **`CEnvCell::find_env_collisions` omits the `CObjCell::check_entry_restrictions` gate** — retail's `CEnvCell::find_env_collisions` (pc:309576) calls `CObjCell::check_entry_restrictions` (pc:309576) FIRST and returns `COLLIDED` when an access-locked cell's `restriction_obj` entity rejects the mover (`CanMoveInto` fails AND `CanBypassMoveRestrictions` is false). acdream's `FindEnvCollisions` (`src/AcDream.Core/Physics/TransitionTypes.cs:2088`) goes straight to BSP collision. | `src/AcDream.Core/Physics/TransitionTypes.cs:2088` | **No access-restriction cell data exists in acdream.** `CellPhysics` (`src/AcDream.Core/Physics/PhysicsDataCache.cs:540`) has no `restriction_obj` field; `DatReaderWriter` does not model per-cell access locks; no weenie-object-table exists on the client for the gate's `CanMoveInto` / `CanBypassMoveRestrictions` dispatch. The gap is **inert** in all dev content (ACE starter area has no access-locked env cells). | If access-locked dungeon cells are ever modeled (dat + wire), the player will walk through the restriction barrier without being blocked — wrong PK/housing gating. | `CObjCell::check_entry_restrictions` pc:309576; `CEnvCell::find_env_collisions` pc:309573–309597 |
|
| AP-71 | **`CEnvCell::find_env_collisions` omits the `CObjCell::check_entry_restrictions` gate** — retail's `CEnvCell::find_env_collisions` (pc:309576) calls `CObjCell::check_entry_restrictions` (pc:309576) FIRST and returns `COLLIDED` when an access-locked cell's `restriction_obj` entity rejects the mover (`CanMoveInto` fails AND `CanBypassMoveRestrictions` is false). acdream's `FindEnvCollisions` (`src/AcDream.Core/Physics/TransitionTypes.cs:2088`) goes straight to BSP collision. | `src/AcDream.Core/Physics/TransitionTypes.cs:2088` | **No access-restriction cell data exists in acdream.** `CellPhysics` (`src/AcDream.Core/Physics/PhysicsDataCache.cs:540`) has no `restriction_obj` field; `DatReaderWriter` does not model per-cell access locks; no weenie-object-table exists on the client for the gate's `CanMoveInto` / `CanBypassMoveRestrictions` dispatch. The gap is **inert** in all dev content (ACE starter area has no access-locked env cells). | If access-locked dungeon cells are ever modeled (dat + wire), the player will walk through the restriction barrier without being blocked — wrong PK/housing gating. | `CObjCell::check_entry_restrictions` pc:309576; `CEnvCell::find_env_collisions` pc:309573–309597 |
|
||||||
| AP-72 | **Cursor art falls back to OS standard cursors when dat resolution fails** — retail always renders MediaDescCursor / EnumIDMap-resolved dat cursor art; acdream's `RetailCursorManager.Apply` falls back to Silk `StandardCursor` (IBeam/crosshair/not-allowed/…) when the EnumIDMap chain or RenderSurface decode fails, and `RetailCursorResolver`/`RetailCursorManager` permanently negative-cache the failed enum/surface id for the session. | `src/AcDream.App/Rendering/RetailCursorManager.cs:47` (`ApplyStandard`), `RetailCursorResolver.cs:47` (negative cache) | Fallback triggers only when the dat lacks the asset — nominal EoR dats always resolve the 0x27/0x28/0x29 chain; an OS cursor keeps the UI usable rather than showing nothing. | A dat-read or decode regression silently shows OS-native cursors instead of surfacing an error — masked failure class; check the `[D.2b]` cursor log lines before suspecting art. | `ClientUISystem::UpdateCursorState` 0x00564630 |
|
| AP-72 | **Cursor art falls back to OS standard cursors when dat resolution fails** — retail always renders MediaDescCursor / EnumIDMap-resolved dat cursor art; acdream's `RetailCursorManager.Apply` falls back to Silk `StandardCursor` (IBeam/crosshair/not-allowed/…) when the EnumIDMap chain or RenderSurface decode fails, and `RetailCursorResolver`/`RetailCursorManager` permanently negative-cache the failed enum/surface id for the session. | `src/AcDream.App/Rendering/RetailCursorManager.cs:47` (`ApplyStandard`), `RetailCursorResolver.cs:47` (negative cache) | Fallback triggers only when the dat lacks the asset — nominal EoR dats always resolve the 0x27/0x28/0x29 chain; an OS cursor keeps the UI usable rather than showing nothing. | A dat-read or decode regression silently shows OS-native cursors instead of surfacing an error — masked failure class; check the `[D.2b]` cursor log lines before suspecting art. | `ClientUISystem::UpdateCursorState` 0x00564630 |
|
||||||
| AP-74 | **UseDone WeenieError text comes from a hardcoded subset map, not the portal String tables** — retail resolves the 0x01C7 UseDone error code through the client String tables into the canonical line ("You are not trained in healing!"); acdream's `WeenieErrorText.For` hardcodes the handful of codes the current use/heal flows produce (0x001D/0x04EB/0x04FC/0x04FE, texts phrased after the ACE enum names) with a generic code-carrying fallback. | `src/AcDream.Core.Net/Messages/WeenieErrorText.cs` | Every refusal is now visible; only unmapped wording deviates, and those lines retain the raw code. Retire by porting the String-table lookup (#202). | An unmapped WeenieError shows a generic line instead of retail's exact sentence | retail String-table error lookup; ACE `WeenieError.cs` values |
|
| AP-74 | **UseDone WeenieError text comes from a hardcoded subset map, not the portal String tables** — retail resolves the 0x01C7 UseDone error code through the client String tables into the canonical line ("You are not trained in healing!"); acdream's `WeenieErrorText.For` hardcodes the handful of codes the current use/heal flows produce (0x001D/0x04EB/0x04FC/0x04FE, texts phrased after the ACE enum names) with a generic code-carrying fallback. | `src/AcDream.Core.Net/Messages/WeenieErrorText.cs` | Every refusal is now visible; only unmapped wording deviates, and those lines retain the raw code. Retire by porting the String-table lookup (#202). | An unmapped WeenieError shows a generic line instead of retail's exact sentence | retail String-table error lookup; ACE `WeenieError.cs` values |
|
||||||
| AP-73 | **Character raises apply optimistically with no verified retail prediction contract or rollback** — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, `CharacterSheetProvider.ApplyLocalRaise` bumps ranks and debits XP/credits through eventful APIs. Retail's predict-vs-authoritative behavior is unverified (**#199**). | `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` | ACE normally confirms client-affordable raises and later property echoes overwrite state; Wave 8 must trace retail before choosing prediction or authoritative-only display | A rejected/reordered raise can leave the sheet wrong until a later full refresh | candidate cdb trace: retail raise-button send/echo path |
|
| AP-73 | **Character raises mutate optimistically, contrary to retail's server-authoritative flow** — after sending RaiseAttribute/RaiseVital/RaiseSkill/TrainSkill, `CharacterSheetProvider.ApplyLocalRaise` immediately bumps ranks and debits XP/credits. Named retail permits one request in flight, ghosts the clicked button, and waits for an authoritative quality-change element message before changing displayed state (**#199**). | `src/AcDream.App/UI/Layout/CharacterSheetProvider.cs` | ACE usually accepts client-affordable raises, so its later property echoes conceal the incorrect prediction; Wave 8 removes local mutation and owns one awaiting request | A rejected/reordered raise can display invented state until a later full refresh, and repeated clicks can create multiple speculative spends | `gmAttributeUI`/`gmSkillUI` raise and quality-change paths, pinned in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
||||||
---
|
---
|
||||||
|
|
||||||
| AP-75 | **Adapter-boundary `adjust_motion` + locomotion velocity/omega synthesis**: `SetCycle` still (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in `CMotionInterp` — R3 scope; GameWindow's local-player path passes raw ids), and (b) post-dispatch overwrites sequence velocity (low-bytes 05/06/07/0F/10) and omega (0D/0E, only when dat-silent) with the retail locomotion constants — retail drives BODY velocity from `get_state_velocity`, not the sequence accumulators (R2-Q4 carry-over of the pre-Q4 adapter tail) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap + tail synthesis) | (a) preserves unadjusted GameWindow callers until R3-W6 unifies the local player onto MotionInterpreter; (b) preserves the remote-DR and local Option-B velocity consumers until R6 root motion drives the body (sibling of IA-3) | (a) none while callers stay in the known set; (b) a dat whose locomotion MotionData carries a REAL velocity different from the constants gets overwritten — exotic-creature speed skew (same class as IA-3's risk) | `CMotionInterp::adjust_motion` @305343; `get_state_velocity` 0x00528960; retire (a) R3-W6, (b) R6 |
|
| AP-75 | **Adapter-boundary `adjust_motion` + locomotion velocity/omega synthesis**: `SetCycle` still (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in `CMotionInterp` — R3 scope; GameWindow's local-player path passes raw ids), and (b) post-dispatch overwrites sequence velocity (low-bytes 05/06/07/0F/10) and omega (0D/0E, only when dat-silent) with the retail locomotion constants — retail drives BODY velocity from `get_state_velocity`, not the sequence accumulators (R2-Q4 carry-over of the pre-Q4 adapter tail) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap + tail synthesis) | (a) preserves unadjusted GameWindow callers until R3-W6 unifies the local player onto MotionInterpreter; (b) preserves the remote-DR and local Option-B velocity consumers until R6 root motion drives the body (sibling of IA-3) | (a) none while callers stay in the known set; (b) a dat whose locomotion MotionData carries a REAL velocity different from the constants gets overwritten — exotic-creature speed skew (same class as IA-3's risk) | `CMotionInterp::adjust_motion` @305343; `get_state_velocity` 0x00528960; retire (a) R3-W6, (b) R6 |
|
||||||
|
|
@ -202,7 +202,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
||||||
| AP-94 | Imported Type-12 text defaults to interactive/selectable behavior; display labels can focus/capture/drag, and vitals synthesize duplicate runtime labels instead of binding `0x100000EB/ED/EF` | `src/AcDream.App/UI/UiText.cs`; `src/AcDream.App/UI/Layout/VitalsController.cs` | Historical widget-generalization default; Wave 1 ports explicit Display/Selectable/Editable roles | Invisible/static text steals input and duplicate labels drift from DAT geometry | `UIElement_Text` property handlers; `gmVitalsUI::PostInit @ 0x004BFCE0` |
|
| AP-94 | Imported Type-12 text defaults to interactive/selectable behavior; display labels can focus/capture/drag, and vitals synthesize duplicate runtime labels instead of binding `0x100000EB/ED/EF` | `src/AcDream.App/UI/UiText.cs`; `src/AcDream.App/UI/Layout/VitalsController.cs` | Historical widget-generalization default; Wave 1 ports explicit Display/Selectable/Editable roles | Invisible/static text steals input and duplicate labels drift from DAT geometry | `UIElement_Text` property handlers; `gmVitalsUI::PostInit @ 0x004BFCE0` |
|
||||||
| AP-95 | `UiButton` dispatches Click only; it lacks retail hover, pressed, released-outside, disabled, selected/toggle, and missing-state fallback transitions | `src/AcDream.App/UI/UiButton.cs` | Controllers manually set a few semantic states; generic pointer visuals remain incomplete | Buttons look inert or overwrite custom states, and disabled/toggle behavior can fire incorrectly | `UIElement_Button::UpdateState_ @ 0x00471CF0`; mouse handlers `0x00471FF0..0x004721F0` |
|
| AP-95 | `UiButton` dispatches Click only; it lacks retail hover, pressed, released-outside, disabled, selected/toggle, and missing-state fallback transitions | `src/AcDream.App/UI/UiButton.cs` | Controllers manually set a few semantic states; generic pointer visuals remain incomplete | Buttons look inert or overwrite custom states, and disabled/toggle behavior can fire incorrectly | `UIElement_Button::UpdateState_ @ 0x00471CF0`; mouse handlers `0x00471FF0..0x004721F0` |
|
||||||
| AP-96 | Layout/property import loses raw edge-mode semantics (especially 3/4) and treats default scalar values as absent, so explicit `false`/`0` cannot override inheritance | `src/AcDream.App/UI/Layout/ElementReader.cs`; `LayoutImporter.cs` | Existing shipped layouts are hand-gated; Wave 1 adds presence-aware properties and the exact solver | Non-reference resizing recenters/stretches incorrectly; derived DAT properties silently inherit the wrong base value | `UIElement::UpdateForParentSizeChange @ 0x00462640`; production LayoutDesc inheritance |
|
| AP-96 | Layout/property import loses raw edge-mode semantics (especially 3/4) and treats default scalar values as absent, so explicit `false`/`0` cannot override inheritance | `src/AcDream.App/UI/Layout/ElementReader.cs`; `LayoutImporter.cs` | Existing shipped layouts are hand-gated; Wave 1 adds presence-aware properties and the exact solver | Non-reference resizing recenters/stretches incorrectly; derived DAT properties silently inherit the wrong base value | `UIElement::UpdateForParentSizeChange @ 0x00462640`; production LayoutDesc inheritance |
|
||||||
| AP-97 | Device timers are registered in `UiRoot` but never pumped or cancelled with subtree removal | `src/AcDream.App/UI/UiRoot.cs` timer registry | No shipped controller depends on timer delivery yet; Wave 1 adds deterministic dispatch/cancellation | Tooltip, repeat, state-delay, or deferred UI actions never fire or can target removed widgets | Keystone Device timer API and retail UI timer call sites |
|
| ~~AP-97~~ | **RETIRED 2026-07-10 (Wave 1 retained-widget foundation)** — the prior generic Device-timer premise was a decomp misread. Named retail polls the hovered element's tooltip deadline, then broadcasts global UI time message `3`; it does not expose the assumed arbitrary timer queue. `UiRoot.Tick` now preserves that order and subtree removal clears input/time ownership before another pulse. | `src/AcDream.App/UI/UiRoot.cs`; `IUiGlobalTimeListener.cs` | — | — | `UIElementManager::UseTime` and hover/focus paths, pinned in `docs/research/2026-07-10-retained-widget-foundations-pseudocode.md` |
|
||||||
| AP-98 | Window registry lacks complete lifecycle/focus/persistence ownership; only radar position persists, and chat maximizes its imported content to hardcoded 320 px rather than resizing the outer frame | `src/AcDream.App/UI/UiRoot.cs`; `ChatWindowController.cs`; production mounts in `GameWindow.cs` | Windows remain usable through manual show/hide and local mount recipes; Wave 2 centralizes the contract | Close cleanup is skipped, layouts reset/drift, and chat chrome/content disagree after maximize | `gmMainChatUI::HandleMaximizeButton @ 0x004CCE50`; retail saveui/loadui behavior |
|
| AP-98 | Window registry lacks complete lifecycle/focus/persistence ownership; only radar position persists, and chat maximizes its imported content to hardcoded 320 px rather than resizing the outer frame | `src/AcDream.App/UI/UiRoot.cs`; `ChatWindowController.cs`; production mounts in `GameWindow.cs` | Windows remain usable through manual show/hide and local mount recipes; Wave 2 centralizes the contract | Close cleanup is skipped, layouts reset/drift, and chat chrome/content disagree after maximize | `gmMainChatUI::HandleMaximizeButton @ 0x004CCE50`; retail saveui/loadui behavior |
|
||||||
| AP-99 | Shared item activation implements only a subset of `ItemHolder::UseObject`/placement legality and confirmation policy | `src/AcDream.App/UI/ItemInteractionController.cs` | Current PvE use/equip/container flows cover the M2 loop; Wave 3 ports the complete pure decision matrix | Vendor/trade/busy/confirmation/target edge cases send the wrong action or permit an illegal use | `ItemHolder::DetermineUseResult @ 0x00588460`; `UseObject @ 0x00588A80`; `AttemptPlaceIn3D @ 0x00588600` |
|
| AP-99 | Shared item activation implements only a subset of `ItemHolder::UseObject`/placement legality and confirmation policy | `src/AcDream.App/UI/ItemInteractionController.cs` | Current PvE use/equip/container flows cover the M2 loop; Wave 3 ports the complete pure decision matrix | Vendor/trade/busy/confirmation/target edge cases send the wrong action or permit an illegal use | `ItemHolder::DetermineUseResult @ 0x00588460`; `UseObject @ 0x00588A80`; `AttemptPlaceIn3D @ 0x00588600` |
|
||||||
| AP-100 | Cursor semantics cover only a reachable subset of retail states, and widget cursor media can outrank authoritative global target mode | `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/Rendering/RetailCursorManager.cs` | Target-use art works for current flows; Wave 3 ports the full table and precedence | Hovering UI can replace the use/examine/busy/combat cursor; unsupported branches fall back to OS art | `ClientUISystem::UpdateCursorState @ 0x00564630` |
|
| AP-100 | Cursor semantics cover only a reachable subset of retail states, and widget cursor media can outrank authoritative global target mode | `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/Rendering/RetailCursorManager.cs` | Target-use art works for current flows; Wave 3 ports the full table and precedence | Hovering UI can replace the use/examine/busy/combat cursor; unsupported branches fall back to OS art | `ClientUISystem::UpdateCursorState @ 0x00564630` |
|
||||||
|
|
@ -211,7 +211,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
||||||
| AP-103 | Shortcut session storage keeps only object guid and discards raw non-object fields; parser/builder names the final uint as `u16 spellId/u16 layer` instead of preserving retail `spellID_` as raw data | `src/AcDream.Core/Items/ShortcutStore.cs`; `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs`; `InventoryActions.cs` | `gmToolbarUI` itself displays only nonzero object ids, so current item shortcuts work | Round-trip mutation corrupts unknown/raw fields and encourages invented spell support in the item toolbar | `ShortCutData` in `acclient.h:36484`; `gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810` |
|
| AP-103 | Shortcut session storage keeps only object guid and discards raw non-object fields; parser/builder names the final uint as `u16 spellId/u16 layer` instead of preserving retail `spellID_` as raw data | `src/AcDream.Core/Items/ShortcutStore.cs`; `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs`; `InventoryActions.cs` | `gmToolbarUI` itself displays only nonzero object ids, so current item shortcuts work | Round-trip mutation corrupts unknown/raw fields and encourages invented spell support in the item toolbar | `ShortCutData` in `acclient.h:36484`; `gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810` |
|
||||||
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |
|
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |
|
||||||
| AP-105 | Retained chat lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; scrollbar arrow roles are reversed and a second ChatVM loses FPS/position providers | `src/AcDream.App/UI/Layout/ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path and word wrapping work; Waves 2/5 consolidate the remaining state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `ChatInterface` methods |
|
| AP-105 | Retained chat lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; scrollbar arrow roles are reversed and a second ChatVM loses FPS/position providers | `src/AcDream.App/UI/Layout/ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path and word wrapping work; Waves 2/5 consolidate the remaining state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `ChatInterface` methods |
|
||||||
| AP-106 | Hiding inventory does not send `NoLongerViewingContents` or reset the viewed side container (#196) | `src/AcDream.App/UI/Layout/InventoryController.cs`; window hide path | Reopening is usable for the last viewed container, but session/view lifecycle diverges | Server viewing state and local caption/grid remain stale across close/reopen | `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50` |
|
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
||||||
| AP-107 | Inventory grid/bag clicks bypass active target-mode acquisition while cursor resolution treats occupied cells as targets (#197) | `src/AcDream.App/UI/Layout/InventoryController.cs`; `UiItemSlot.cs` | Main pack/toolbar/paperdoll already opt in at their call sites | A valid-target cursor click selects/opens rather than consuming the target | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
|
| AP-107 | Inventory grid/bag clicks bypass active target-mode acquisition while cursor resolution treats occupied cells as targets (#197) | `src/AcDream.App/UI/Layout/InventoryController.cs`; `UiItemSlot.cs` | Main pack/toolbar/paperdoll already opt in at their call sites | A valid-target cursor click selects/opens rather than consuming the target | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
|
||||||
| AP-108 | Paperdoll omits Aetheria, authoritative upper-item priority, full `AutoWieldIsLegal`/dual-wield rules, and body-part interaction/highlighting | `src/AcDream.App/UI/Layout/PaperdollController.cs` | Basic equip slots, live doll, toggle, and optimistic wield work | Layered clothing, special slots, invalid drops, and doll-body clicks differ functionally | `gmPaperDollUI @ 0x004A3590..0x004A5F90`; `InventoryPlacement::DetermineHigherPriority` |
|
| AP-108 | Paperdoll omits Aetheria, authoritative upper-item priority, full `AutoWieldIsLegal`/dual-wield rules, and body-part interaction/highlighting | `src/AcDream.App/UI/Layout/PaperdollController.cs` | Basic equip slots, live doll, toggle, and optimistic wield work | Layered clothing, special slots, invalid drops, and doll-body clicks differ functionally | `gmPaperDollUI @ 0x004A3590..0x004A5F90`; `InventoryPlacement::DetermineHigherPriority` |
|
||||||
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
||||||
|
|
@ -248,7 +248,7 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
|
||||||
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
||||||
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
|
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
|
||||||
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:331` | Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
|
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:331` | Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
|
||||||
| TS-30 | Numbered chat tabs (element ids `0x10000522`–`0x10000525`) render as clickable buttons but do not switch channel filter or affect the transcript — tab state is a no-op | `src/AcDream.App/UI/Layout/ChatWindowController.cs:210` | Retail's tab switching routes transcript lines by chat channel (`gmMainChatUI::gmScrollWindow` sub-windows per tab); the tab wiring is D.5 scope | Tab clicks produce no visible transcript change; retail would filter to the selected channel — all chat always shows in all tabs | `gmMainChatUI::PostInit` tab setup @0x4ce2a0; holtburger chat tab handling |
|
| TS-30 | Chat DAT elements `0x10000522`–`0x10000525` render but have no controller semantics; the older claim that they are numbered in-window filter tabs is **unproven** | `src/AcDream.App/UI/Layout/ChatWindowController.cs` | Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; correction in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
||||||
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
||||||
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
||||||
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change), AND acdream's frame-changed diff compares POSITION only (`ApproxPositionEqual`) where retail's `Frame::is_equal` compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's `HandleTurnToHeading` arrival snap, `set_heading(send:true)`) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`); `src/AcDream.App/Input/PlayerMovementController.cs` (`ApproxPositionEqual` + the heartbeat diff); the player MoveToManager `setHeading` seam in `EnterPlayerModeNow` (the `send` flag's would-be consumer) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping + full-frame `Frame::is_equal` diff) is a dedicated follow-up slice (R7 outbound) | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0, `Frame::is_equal` (pc:700263) |
|
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change), AND acdream's frame-changed diff compares POSITION only (`ApproxPositionEqual`) where retail's `Frame::is_equal` compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's `HandleTurnToHeading` arrival snap, `set_heading(send:true)`) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`); `src/AcDream.App/Input/PlayerMovementController.cs` (`ApproxPositionEqual` + the heartbeat diff); the player MoveToManager `setHeading` seam in `EnterPlayerModeNow` (the `send` flag's would-be consumer) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping + full-frame `Frame::is_equal` diff) is a dedicated follow-up slice (R7 outbound) | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0, `Frame::is_equal` (pc:700263) |
|
||||||
|
|
|
||||||
851
docs/research/2026-07-10-retail-panel-behavior-pseudocode.md
Normal file
851
docs/research/2026-07-10-retail-panel-behavior-pseudocode.md
Normal file
|
|
@ -0,0 +1,851 @@
|
||||||
|
# Retail panel behavior oracle — vitals, chat, inventory, paperdoll, character
|
||||||
|
|
||||||
|
Date: 2026-07-10
|
||||||
|
Scope: Wave 0 static research for the retail UI fidelity completion plan.
|
||||||
|
Status: implementation oracle; no runtime code is changed by this note.
|
||||||
|
|
||||||
|
## Source order and confidence
|
||||||
|
|
||||||
|
The Sept 2013 named retail client is the behavioral authority:
|
||||||
|
|
||||||
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||||
|
- `docs/research/named-retail/acclient.h`
|
||||||
|
- `docs/research/named-retail/symbols.json`
|
||||||
|
|
||||||
|
The layout/DAT discoveries already recorded under `docs/research/` are used for
|
||||||
|
element IDs, layout IDs, and imported media. Existing ACE and holtburger
|
||||||
|
cross-references in the inventory deep dive are interpretation aids for the
|
||||||
|
wire contract; the retail client still wins when they differ.
|
||||||
|
|
||||||
|
Binary Ninja occasionally assigns a neighboring multiple-inheritance field
|
||||||
|
name to a value. This note therefore names fields by the behavior proved by
|
||||||
|
their setters/callers and records such cases as semantic pseudocode rather
|
||||||
|
than copying a suspect generated field name.
|
||||||
|
|
||||||
|
### Corrections to assumptions in earlier UI notes
|
||||||
|
|
||||||
|
1. **The named client does not prove a tab strip inside the main chat
|
||||||
|
window.** It proves one main chat interface plus separately configured
|
||||||
|
floaty chat windows, each with a window ID and a 64-bit text-type filter.
|
||||||
|
The `General / Combat / Chat / 1...` strip in
|
||||||
|
`docs/research/retail-ui/05-panels.md` was explicitly an inference and must
|
||||||
|
not be implemented as retail fact. Live retail visual confirmation is
|
||||||
|
required before adding a tab skin.
|
||||||
|
2. **`NoLongerViewingContents` is an external ground-object lifetime event,
|
||||||
|
not an owned-bag navigation event.** Retail centralizes the send in
|
||||||
|
`ClientUISystem::SetGroundObject(old, new, notifyServer)`. Main-inventory
|
||||||
|
and owned side-bag navigation use the inventory item-list notices instead.
|
||||||
|
3. **Stat raising is server-authoritative.** Retail serializes one request,
|
||||||
|
ghosts the raise control, and waits for a quality-change message. It does
|
||||||
|
not predict the local rank, XP, credits, or cost and then roll them back.
|
||||||
|
|
||||||
|
## 1. Vitals detail mode
|
||||||
|
|
||||||
|
### Retail anchors
|
||||||
|
|
||||||
|
| Behavior | Named symbol |
|
||||||
|
|---|---|
|
||||||
|
| Bind imported elements | `gmVitalsUI::PostInit @ 0x004BFCE0` |
|
||||||
|
| Toggle detail state | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00` |
|
||||||
|
| Refresh meters and text | `gmVitalsUI::Update @ 0x004BFE30` |
|
||||||
|
|
||||||
|
Layout: `0x2100006C`.
|
||||||
|
|
||||||
|
| Meaning | Element/state ID |
|
||||||
|
|---|---:|
|
||||||
|
| Health meter | `0x100000E6` |
|
||||||
|
| Health value label | `0x100000EB` |
|
||||||
|
| Stamina meter | `0x100000EC` |
|
||||||
|
| Stamina value label | `0x100000ED` |
|
||||||
|
| Mana meter | `0x100000EE` |
|
||||||
|
| Mana value label | `0x100000EF` |
|
||||||
|
| Root `HideDetail` state | `0x10000006` |
|
||||||
|
| Root `ShowDetail` state | `0x10000007` |
|
||||||
|
| Meter fill/value property | `0x69` |
|
||||||
|
|
||||||
|
The imported `ShowDetail` layout state selects these overlay media:
|
||||||
|
|
||||||
|
| Vital | Back | Front |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Health | `0x06007490` | `0x06007491` |
|
||||||
|
| Stamina | `0x06007492` | `0x06007493` |
|
||||||
|
| Mana | `0x06007494` | `0x06007495` |
|
||||||
|
|
||||||
|
### Pseudocode
|
||||||
|
|
||||||
|
```text
|
||||||
|
PostInit():
|
||||||
|
healthMeter = child(0x100000E6)
|
||||||
|
healthLabel = child(0x100000EB)
|
||||||
|
staminaMeter = child(0x100000EC)
|
||||||
|
staminaLabel = child(0x100000ED)
|
||||||
|
manaMeter = child(0x100000EE)
|
||||||
|
manaLabel = child(0x100000EF)
|
||||||
|
|
||||||
|
OnElementMessage(message):
|
||||||
|
if message.id == 0x1C and message.pointerAction in { LEFT_CLICK=7,
|
||||||
|
DOUBLE_CLICK=10 }:
|
||||||
|
nextState = ShowDetail if root.state == HideDetail else HideDetail
|
||||||
|
root.setState(nextState)
|
||||||
|
return base.OnElementMessage(message)
|
||||||
|
|
||||||
|
Update():
|
||||||
|
updateVital(healthMeter, healthLabel, currentSecondAttribute=2, max=1)
|
||||||
|
updateVital(staminaMeter, staminaLabel, currentSecondAttribute=4, max=3)
|
||||||
|
updateVital(manaMeter, manaLabel, currentSecondAttribute=6, max=5)
|
||||||
|
|
||||||
|
updateVital(meter, label, currentProperty, maximumProperty):
|
||||||
|
current = player.attribute2nd(currentProperty)
|
||||||
|
maximum = player.attribute2nd(maximumProperty)
|
||||||
|
meter.property[0x69] = current / maximum
|
||||||
|
label.text = formatCurrentAndMaximum(current, maximum)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation consequence: bind and update the imported label elements.
|
||||||
|
Creating new text children over the meters is not retail-equivalent and loses
|
||||||
|
the DAT state-driven geometry/visibility.
|
||||||
|
|
||||||
|
## 2. Chat window
|
||||||
|
|
||||||
|
### Layout and elements
|
||||||
|
|
||||||
|
Main chat layout: `0x21000006`.
|
||||||
|
|
||||||
|
| Meaning | ID |
|
||||||
|
|---|---:|
|
||||||
|
| Root field | `0x1000000E` |
|
||||||
|
| Resize bar | `0x1000000F` |
|
||||||
|
| Transcript panel | `0x10000010` |
|
||||||
|
| Transcript/log | `0x10000011` |
|
||||||
|
| Scrollbar track | `0x10000012` |
|
||||||
|
| Input bar | `0x10000013` |
|
||||||
|
| Talk-focus menu | `0x10000014` |
|
||||||
|
| Chat target text | `0x10000015` |
|
||||||
|
| Editable input | `0x10000016` |
|
||||||
|
| Send button | `0x10000019` |
|
||||||
|
| Maximize/restore button | `0x1000046F` |
|
||||||
|
| New non-visible text indicator | `0x1000048C` |
|
||||||
|
|
||||||
|
Retail option/property IDs named in the client are:
|
||||||
|
|
||||||
|
| Meaning | ID |
|
||||||
|
|---|---:|
|
||||||
|
| Talk-focus enum on a menu item | `0x1000000B` |
|
||||||
|
| Chat window ID | `0x1000007E` |
|
||||||
|
| 64-bit text-type filter | `0x1000007F` |
|
||||||
|
| Default/inactive opacity | `0x10000080` |
|
||||||
|
| Active/focused opacity | `0x10000081` |
|
||||||
|
| Maximum height | `0x3C` |
|
||||||
|
| Maximum width | `0x3D` |
|
||||||
|
| Minimum height | `0x3E` |
|
||||||
|
| Minimum width | `0x3F` |
|
||||||
|
| Maximize button state | `0x10000047` |
|
||||||
|
| Restore/minimized button state | `0x10000048` |
|
||||||
|
|
||||||
|
### Maximize, restore, and resize
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmMainChatUI::HandleMaximizeButton @ 0x004CCE50`
|
||||||
|
- `gmMainChatUI::ResizeTo @ 0x004CD0A0`
|
||||||
|
- `ChatInterface::ResizeTo @ 0x004F39E0`
|
||||||
|
- `UIElement::ResizeTo @ 0x00463C30`
|
||||||
|
|
||||||
|
```text
|
||||||
|
HandleMaximizeButton():
|
||||||
|
parentHeight = root.parent.height
|
||||||
|
minHeight = root.property[0x3E]
|
||||||
|
maxHeight = root.property[0x3C]
|
||||||
|
|
||||||
|
if maximized:
|
||||||
|
restoredHeight = clamp(savedHeight, minHeight, maxHeight)
|
||||||
|
restoredY = clamp(savedY, 0, parentHeight - minHeight)
|
||||||
|
maximized = false
|
||||||
|
maximizeButton.state = 0x10000048
|
||||||
|
root.resize(root.width, restoredHeight)
|
||||||
|
root.move(root.x, restoredY)
|
||||||
|
return
|
||||||
|
|
||||||
|
savedY = root.y
|
||||||
|
savedHeight = root.height
|
||||||
|
|
||||||
|
expansion = parentHeight / 2
|
||||||
|
targetHeight = root.height + expansion
|
||||||
|
|
||||||
|
if expanding downward would cross the parent bottom,
|
||||||
|
or the window is already in the lower half:
|
||||||
|
targetY = max(0, root.y - expansion)
|
||||||
|
else:
|
||||||
|
targetY = root.y
|
||||||
|
|
||||||
|
targetHeight = min(targetHeight, parentHeight - targetY)
|
||||||
|
targetHeight = clamp(targetHeight, minHeight, maxHeight)
|
||||||
|
|
||||||
|
maximized = true
|
||||||
|
maximizeButton.state = 0x10000047
|
||||||
|
root.resize(root.width, targetHeight)
|
||||||
|
root.move(root.x, targetY)
|
||||||
|
|
||||||
|
ResizeTo(width, height):
|
||||||
|
wasAtTranscriptEnd = transcript.isAtVerticalEnd()
|
||||||
|
base.resizeTo(width, height) // base enforces 0x3C..0x3F limits
|
||||||
|
resize the bound transcript/input panel, not a hard-coded synthetic box
|
||||||
|
if wasAtTranscriptEnd:
|
||||||
|
transcript.scrollToFinalGlyph()
|
||||||
|
```
|
||||||
|
|
||||||
|
The saved position and size are part of `gmMainChatUI` (`m_OldY`,
|
||||||
|
`m_OldHeight`, `m_Maximized` in `acclient.h`). A fixed expanded height is not
|
||||||
|
retail behavior.
|
||||||
|
|
||||||
|
### Opacity and input activation
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `ChatInterface::ChatInterface @ 0x004F4550`
|
||||||
|
- `ChatInterface::OnSetAttribute @ 0x004F3F60`
|
||||||
|
- `ChatInterface::SetDefaultOpacity @ 0x004F3BC0`
|
||||||
|
- `ChatInterface::SetActiveOpacity @ 0x004F3C40`
|
||||||
|
- `ChatInterface::ListenToGlobalMessage @ 0x004F3840`
|
||||||
|
- `ChatInterface::ActivateChatEntry @ 0x004F2F90`
|
||||||
|
- `ChatInterface::DeactivateChatEntry @ 0x004F2FC0`
|
||||||
|
- `ChatInterface::ToggleChatEntry @ 0x004F3B30`
|
||||||
|
- `ChatInterface::IsTextEntryFocused @ 0x004F30A0`
|
||||||
|
|
||||||
|
Defaults are 0.5 inactive, 1.0 active, and 0.5 current.
|
||||||
|
|
||||||
|
```text
|
||||||
|
OnAttributeChanged(id, value):
|
||||||
|
if id == 0x10000080: setDefaultOpacity(value)
|
||||||
|
if id == 0x10000081: setActiveOpacity(value)
|
||||||
|
|
||||||
|
OnChatFocusStateChanged():
|
||||||
|
target = activeOpacity if input is focused or window is forced active
|
||||||
|
else defaultOpacity
|
||||||
|
registerForGlobalTicks()
|
||||||
|
|
||||||
|
OnGlobalTick():
|
||||||
|
step = abs(activeOpacity - defaultOpacity) * 0.05
|
||||||
|
currentOpacity = moveToward(currentOpacity, target, step)
|
||||||
|
enclosingWindow.alpha = currentOpacity
|
||||||
|
if currentOpacity == target:
|
||||||
|
unregisterFromGlobalTicks()
|
||||||
|
|
||||||
|
ActivateChatEntry():
|
||||||
|
input.activate()
|
||||||
|
input.takeFocus()
|
||||||
|
sendNotice(ToggleChatEntry, true)
|
||||||
|
|
||||||
|
DeactivateChatEntry():
|
||||||
|
input.relinquishFocus()
|
||||||
|
input.deactivate()
|
||||||
|
sendNotice(ToggleChatEntry, false)
|
||||||
|
|
||||||
|
IsTextEntryFocused():
|
||||||
|
return activeRoot == thisChatRoot and focusedElement is input-or-descendant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Talk focus and squelch
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmMainChatUI::InitTalkFocusMenu @ 0x004CDC50`
|
||||||
|
- `gmMainChatUI::GetTalkFocusMenuItem @ 0x004CD370`
|
||||||
|
- `gmMainChatUI::ResetAllTalkFocusMenuButtons @ 0x004CD4A0`
|
||||||
|
- `gmMainChatUI::HandleSelection @ 0x004CD540`
|
||||||
|
- `gmMainChatUI::EnableSelection @ 0x004CE0A0`
|
||||||
|
- `gmMainChatUI::ToggleSquelchOnCurrentSpeakableTarget @ 0x004CD230`
|
||||||
|
- `gmMainChatUI::ListenToElementMessage @ 0x004CDA80`
|
||||||
|
- `gmMainChatUI::PostInit @ 0x004CE130`
|
||||||
|
|
||||||
|
The menu is flushed and rebuilt at runtime. Its first item is the squelch
|
||||||
|
toggle. The 13 talk-focus entries store these enum values, in visual order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[5, 2, 4, 1, 6, 3, 7, 8, 9, 10, 11, 12, 13]
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
InitTalkFocusMenu():
|
||||||
|
menu = child(0x10000014)
|
||||||
|
menu.flush()
|
||||||
|
squelchItem = menu.addRuntimeItem(...)
|
||||||
|
for focus in [5,2,4,1,6,3,7,8,9,10,11,12,13]:
|
||||||
|
item = menu.addRuntimeItem(stringTableTextFor(focus))
|
||||||
|
item.enumProperty[0x1000000B] = focus
|
||||||
|
|
||||||
|
ResetTalkFocusButtons():
|
||||||
|
for each focus item:
|
||||||
|
item.state = ENABLED(1) if communication.isTalkFocusEnabled(focus)
|
||||||
|
else GHOSTED(13)
|
||||||
|
// Olthoi mode permits only focus 13 and focus 1.
|
||||||
|
select item whose 0x1000000B equals communication.currentTalkFocus
|
||||||
|
|
||||||
|
OnTalkFocusMenuSelection(item):
|
||||||
|
menu.clearSelectedItem()
|
||||||
|
if item == squelchItem:
|
||||||
|
ToggleSquelchOnCurrentSpeakableTarget()
|
||||||
|
else:
|
||||||
|
communication.setTalkFocus(item.enumProperty[0x1000000B])
|
||||||
|
ResetTalkFocusButtons()
|
||||||
|
|
||||||
|
ToggleSquelchOnCurrentSpeakableTarget():
|
||||||
|
target = communication.lastSpeakableTarget
|
||||||
|
objectAndName = resolve(target)
|
||||||
|
currentlySquelched = communication.isSquelched(target, objectAndName.name, 1)
|
||||||
|
squelchItem.state = ENABLED(1)
|
||||||
|
send ModifyCharacterSquelch(
|
||||||
|
add = not currentlySquelched,
|
||||||
|
targetId = target,
|
||||||
|
targetName = objectAndName.name,
|
||||||
|
scope = 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Exact localized menu labels are table-driven and are not legible enough in
|
||||||
|
the pseudo-C to reproduce verbatim. Resolve them from the retail string table
|
||||||
|
or capture the menu in live retail; do not invent labels.
|
||||||
|
|
||||||
|
### Chat windows and the unproven “tabs”
|
||||||
|
|
||||||
|
`ChatInterface::PostInit @ 0x004F3DD0` reads window ID `0x1000007E`,
|
||||||
|
text filter `0x1000007F`, and binds transcript `0x10000011`, input
|
||||||
|
`0x10000016`, and the non-visible indicator `0x1000048C`. Static default filter
|
||||||
|
values include:
|
||||||
|
|
||||||
|
| Window ID | Default 64-bit filter |
|
||||||
|
|---:|---:|
|
||||||
|
| 1 or 8 | `0x0` for the non-visible indicator mask path |
|
||||||
|
| 2 | `0x101C` |
|
||||||
|
| 3 | `0x40C00` |
|
||||||
|
| 4 | `0x80000` |
|
||||||
|
| 5 | `0x78000000` |
|
||||||
|
|
||||||
|
`PlayerModule` persists a filter per window and the client has
|
||||||
|
`gmFloatyChatUI`/`gmFloatyMainChatUI` types. That is evidence for multiple
|
||||||
|
configurable windows, not evidence for tabs inside `gmMainChatUI`.
|
||||||
|
|
||||||
|
## 3. Inventory click targeting and view teardown
|
||||||
|
|
||||||
|
### Target-mode precedence
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`
|
||||||
|
- `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0`
|
||||||
|
- `UIElement_ItemList::ItemList_SetParentContainer @ 0x004E49D0`
|
||||||
|
- `UIElement_ItemList::ItemList_OpenContainer @ 0x004E4AE0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
OnItemListPointerMessage(message):
|
||||||
|
item = uiItemUnderPointer(message)
|
||||||
|
|
||||||
|
if message.action == LEFT_CLICK(7):
|
||||||
|
if ClientUISystem.targetMode != NONE:
|
||||||
|
HandleTargetedUseLeftClick(item)
|
||||||
|
return CONSUMED // before select/open navigation
|
||||||
|
|
||||||
|
setUiSelected(item)
|
||||||
|
SetSelectedObject(item.itemId, false)
|
||||||
|
|
||||||
|
if this list is container navigation:
|
||||||
|
open/select the item's parent-or-child list according to list role
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.action == DOUBLE_CLICK(10):
|
||||||
|
if vendor/salvage rules did not consume it:
|
||||||
|
if this is a container-list, require item == current external object
|
||||||
|
ItemHolder.UseObject(item.itemId)
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.action == EXAMINE_CLICK(8):
|
||||||
|
select item
|
||||||
|
ExamineSpell(item.spellId) if this is a spell shortcut
|
||||||
|
else ExamineObject(item.itemId)
|
||||||
|
|
||||||
|
HandleTargetedUseLeftClick(item):
|
||||||
|
targetMode = ClientUISystem.targetMode
|
||||||
|
if item represents a spell:
|
||||||
|
ClientUISystem.ExecuteTargetModeForSpell(item.spellId, targetMode)
|
||||||
|
else:
|
||||||
|
ClientUISystem.ExecuteTargetModeForItem(item.itemId, targetMode)
|
||||||
|
```
|
||||||
|
|
||||||
|
The critical rule is ordering: a target-mode click is consumed before ordinary
|
||||||
|
selection, container opening, or drag behavior, including an invalid target
|
||||||
|
attempt.
|
||||||
|
|
||||||
|
### Owned inventory versus external ground views
|
||||||
|
|
||||||
|
Owned inventory anchors:
|
||||||
|
|
||||||
|
- `gmInventoryUI::RecvNotice_SetDisplayInventory @ 0x004A6750`
|
||||||
|
- `gmInventoryUI::RecvNotice_OpenContainedContainer @ 0x004A6A80`
|
||||||
|
|
||||||
|
External view anchors:
|
||||||
|
|
||||||
|
- `gmExternalContainerUI::ListenToElementMessage @ 0x004CBAD0`
|
||||||
|
- `gmExternalContainerUI::SetGroundObject @ 0x004CBBD0`
|
||||||
|
- `gmExternalContainerUI::CloseCurrentContainer @ 0x004CBCB0`
|
||||||
|
- `gmExternalContainerUI::OnVisibilityChanged @ 0x004CBF50`
|
||||||
|
- `gmExternalContainerUI::RecvNotice_SetGroundObject @ 0x004CBFD0`
|
||||||
|
- `ClientUISystem::SetGroundObject @ 0x00564510`
|
||||||
|
- `ClientUISystem::OnViewContents @ 0x00565870`
|
||||||
|
- `ClientUISystem::CleanUpGameUI @ 0x005648A0`
|
||||||
|
- `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`
|
||||||
|
|
||||||
|
Wire contract, cross-confirmed by the existing holtburger reference in the
|
||||||
|
inventory deep dive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
opcode: 0x0195 NoLongerViewingContents
|
||||||
|
payload: u32 containerGuid
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
ShowMainOwnedInventory():
|
||||||
|
topList.flush()
|
||||||
|
topList.add(playerId)
|
||||||
|
childList.setParentContainer(playerId)
|
||||||
|
topList.openContainer(playerId)
|
||||||
|
paperdoll.remake()
|
||||||
|
|
||||||
|
OpenOwnedContainedContainer(containerId):
|
||||||
|
require object is owned by player and has an allowed container type
|
||||||
|
make inventory visible
|
||||||
|
if containerId == playerId:
|
||||||
|
use top/main list
|
||||||
|
else:
|
||||||
|
use owned side/container list
|
||||||
|
// This path does not make containerId ClientUISystem.groundObject.
|
||||||
|
|
||||||
|
OnExternalViewContentsResponse(containerId, contentProfile):
|
||||||
|
apply authoritative content profile to the external object
|
||||||
|
if containerId == requestedGroundObject:
|
||||||
|
SetGroundObject(containerId)
|
||||||
|
publish SetGroundObject notice to gmExternalContainerUI
|
||||||
|
|
||||||
|
ExternalPanel.SetGroundObject(containerId):
|
||||||
|
unregister range handler for old external object
|
||||||
|
flush top list
|
||||||
|
add containerId
|
||||||
|
bind child lists and open container
|
||||||
|
if containerId == 0:
|
||||||
|
hide panel
|
||||||
|
else:
|
||||||
|
register range handler and show panel
|
||||||
|
|
||||||
|
ExternalPanel.OnCloseButton():
|
||||||
|
setVisible(false)
|
||||||
|
|
||||||
|
ExternalPanel.OnVisibilityChanged(false):
|
||||||
|
CloseCurrentContainer()
|
||||||
|
|
||||||
|
ExternalPanel.CloseCurrentContainer():
|
||||||
|
if externalObjectId != 0:
|
||||||
|
ItemHolder.UseObject(externalObjectId) // retail close/use handshake
|
||||||
|
unregister range handler
|
||||||
|
externalObjectId = 0
|
||||||
|
flush and detach lists
|
||||||
|
|
||||||
|
ClientUISystem.SetGroundObject(newId, notifyServer):
|
||||||
|
if oldId == newId: return
|
||||||
|
close vendor if needed
|
||||||
|
if oldId != 0:
|
||||||
|
queue old contents for destruction
|
||||||
|
publish SetGroundObject(0)
|
||||||
|
if notifyServer:
|
||||||
|
send NoLongerViewingContents(oldId) exactly once
|
||||||
|
clear selected child of old object when applicable
|
||||||
|
groundObject = newId
|
||||||
|
requestedGroundObject = newId
|
||||||
|
mark corpse-open state if applicable
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetGroundObject(0, true)` is also used for session cleanup, vendor opening,
|
||||||
|
the viewed object moving, range/error conditions, and other external-view
|
||||||
|
termination. This centralized old-ID comparison provides the one-shot guard;
|
||||||
|
adding independent sends in several UI close callbacks would duplicate it.
|
||||||
|
|
||||||
|
Static code does **not** establish that hiding the main owned-inventory window
|
||||||
|
after navigating an owned side bag sends `0x0195`; the object is not the
|
||||||
|
`ClientUISystem.groundObject` on that path. Treat such a send as a divergence
|
||||||
|
unless a packet trace proves otherwise.
|
||||||
|
|
||||||
|
## 4. Paperdoll
|
||||||
|
|
||||||
|
Paperdoll layout: `0x21000024`.
|
||||||
|
|
||||||
|
### Slot mapping
|
||||||
|
|
||||||
|
Retail anchor: `gmPaperDollUI::GetLocationInfoFromElementID @ 0x004A37F0`.
|
||||||
|
|
||||||
|
| Element | Equip/location mask | Side/meaning |
|
||||||
|
|---:|---:|---|
|
||||||
|
| `0x100001DA` | `0x00008000` | Neck |
|
||||||
|
| `0x100001DB` | `0x00010000` | Left wrist |
|
||||||
|
| `0x100001DC` | `0x00040000` | Left ring |
|
||||||
|
| `0x100001DD` | `0x00020000` | Right wrist |
|
||||||
|
| `0x100001DE` | `0x00080000` | Right ring |
|
||||||
|
| `0x100001DF` | `0x03500000` | Weapon composite |
|
||||||
|
| `0x100001E0` | `0x00800000` | Ammunition |
|
||||||
|
| `0x100001E1` | `0x00200000` | Shield/offhand |
|
||||||
|
| `0x100001E2` | `0x00000002` | Shirt |
|
||||||
|
| `0x100001E3` | `0x00000040` | Pants |
|
||||||
|
| `0x1000058E` | `0x04000000` | Trinket |
|
||||||
|
| `0x100005E9` | `0x08000000` | Cloak |
|
||||||
|
| `0x10000595` | `0x10000000` | Aetheria/sigil 1 |
|
||||||
|
| `0x10000596` | `0x20000000` | Aetheria/sigil 2 |
|
||||||
|
| `0x10000597` | `0x40000000` | Aetheria/sigil 3 |
|
||||||
|
| `0x100005AB` | `0x00000001` | Head armor |
|
||||||
|
| `0x100005AC` | `0x00000200` | Chest armor |
|
||||||
|
| `0x100005AD` | `0x00000400` | Abdomen armor |
|
||||||
|
| `0x100005AE` | `0x00000800` | Upper arm armor |
|
||||||
|
| `0x100005AF` | `0x00001000` | Lower arm armor |
|
||||||
|
| `0x100005B0` | `0x00000020` | Hand armor |
|
||||||
|
| `0x100005B1` | `0x00002000` | Upper leg armor |
|
||||||
|
| `0x100005B2` | `0x00004000` | Lower leg armor |
|
||||||
|
| `0x100005B3` | `0x00000100` | Foot armor |
|
||||||
|
|
||||||
|
The named pseudo-C renders the ammunition constant poorly in one expression;
|
||||||
|
`0x00800000` is the consistent EquipMask/source value. Preserve a decomp cite
|
||||||
|
and conformance test when implementing that branch.
|
||||||
|
|
||||||
|
### Upper-item priority and body hit testing
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `InventoryPlacement::DetermineHigherPriority @ 0x004A44B0`
|
||||||
|
- `gmPaperDollUI::GetUpperInvObj @ 0x004A4750`
|
||||||
|
- `gmPaperDollUI::CreateClickMap @ 0x004A4850`
|
||||||
|
- `gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920`
|
||||||
|
- `gmPaperDollUI::ListenToElementMessage @ 0x004A5C30`
|
||||||
|
- `gmPaperDollUI::BeginPartSelectionLighting @ 0x004A4C50`
|
||||||
|
- `gmPaperDollUI::ApplyPartSelectionLighting @ 0x004A3CA0`
|
||||||
|
- `gmPaperDollUI::UpdatePartSelectionLighting @ 0x004A4360`
|
||||||
|
|
||||||
|
```text
|
||||||
|
DetermineHigherPriority(a, b, bodyLocationMask):
|
||||||
|
aLoc = a.location & bodyLocationMask
|
||||||
|
bLoc = b.location & bodyLocationMask
|
||||||
|
|
||||||
|
aPriority = a.placementPriority
|
||||||
|
bPriority = b.placementPriority
|
||||||
|
|
||||||
|
if aPriority == 0 and aLoc overlaps armor range 0x200..0x4000:
|
||||||
|
aPriority = 0x7F
|
||||||
|
if bPriority == 0 and bLoc overlaps armor range 0x200..0x4000:
|
||||||
|
bPriority = 0x7F
|
||||||
|
|
||||||
|
return b if bPriority > aPriority else a // stable: existing wins ties
|
||||||
|
|
||||||
|
GetUpperInvObj(bodyLocationMask):
|
||||||
|
if invalid mask/player/list: return 0
|
||||||
|
winner = none
|
||||||
|
for placement in player.inventoryPlacements in list order:
|
||||||
|
if placement.location overlaps bodyLocationMask:
|
||||||
|
winner = placement if winner is none
|
||||||
|
else DetermineHigherPriority(winner, placement,
|
||||||
|
bodyLocationMask)
|
||||||
|
if winner exists: return winner.itemId
|
||||||
|
return playerId // retail body hit fallback when a placement list exists
|
||||||
|
```
|
||||||
|
|
||||||
|
`CreateClickMap` loads enum DID `0x1000000C` in category 7. The sampled body
|
||||||
|
color maps to these masks:
|
||||||
|
|
||||||
|
| Click-map value | Body mask |
|
||||||
|
|---:|---:|
|
||||||
|
| Head | `0x00000001` |
|
||||||
|
| Chest | `0x00000202` (shirt + chest) |
|
||||||
|
| Abdomen | `0x00000404` |
|
||||||
|
| Upper arm | `0x00000808` |
|
||||||
|
| Lower arm | `0x00001010` |
|
||||||
|
| Upper leg | `0x00002040` |
|
||||||
|
| Lower leg | `0x00004080` |
|
||||||
|
| Hand | `0x00000020` |
|
||||||
|
| Foot | `0x00000100` |
|
||||||
|
|
||||||
|
The drag mask is `0x100001D6`. Left-click selects the upper equipped object
|
||||||
|
when not targeting. In target mode, the body click deliberately executes the
|
||||||
|
target mode against the **player ID**, not the visually upper item. Examine
|
||||||
|
and drag use the upper item. Selection lighting pulses matching body parts at
|
||||||
|
0.2-second intervals; player-ID selection uses mask `0x7FFFFFFF`.
|
||||||
|
|
||||||
|
### Aetheria visibility
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmPaperDollUI::PostInit @ 0x004A5360`
|
||||||
|
- `gmPaperDollUI::UpdateAetheria @ 0x004A3E50`
|
||||||
|
- `gmPaperDollUI::RecvNotice_PlayerDescReceived @ 0x004A43E0`
|
||||||
|
- `gmPaperDollUI::OnQualityChanged @ 0x004A4490`
|
||||||
|
|
||||||
|
Property `0x142` is the Aetheria bitfield.
|
||||||
|
|
||||||
|
```text
|
||||||
|
PostInit():
|
||||||
|
aetheria[0..2] = children(0x10000595, 0x10000596, 0x10000597)
|
||||||
|
hide all three
|
||||||
|
observe player int quality 0x142
|
||||||
|
|
||||||
|
UpdateAetheria():
|
||||||
|
bits = player.intQuality(0x142)
|
||||||
|
aetheria[0].visible = (bits & 1) != 0
|
||||||
|
aetheria[1].visible = (bits & 2) != 0
|
||||||
|
aetheria[2].visible = (bits & 4) != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wield/drop legality
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmPaperDollUI::HandlePaperDollDragOver @ 0x004A3A70`
|
||||||
|
- `gmPaperDollUI::AcceptDragObject @ 0x004A3B10`
|
||||||
|
- `gmPaperDollUI::OnItemListDragOver @ 0x004A4270`
|
||||||
|
- `gmPaperDollUI::AcceptPaperDollDragObject @ 0x004A4A70`
|
||||||
|
- `gmPaperDollUI::HandleDropRelease @ 0x004A4D80`
|
||||||
|
- `CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60`
|
||||||
|
|
||||||
|
```text
|
||||||
|
OnSlotDragOver(item, slotMask):
|
||||||
|
accept = item.validLocations overlaps slotMask
|
||||||
|
and PlayerSystem.AutoWieldIsLegal(item, silent=true)
|
||||||
|
set overlay accept/reject state
|
||||||
|
|
||||||
|
AcceptDragObject(item, slotMask, side):
|
||||||
|
if slotMask == SHIELD(0x00200000)
|
||||||
|
and item.validLocations includes MELEE_WEAPON(0x00100000):
|
||||||
|
side = LEFT // retail dual-wield/offhand special case
|
||||||
|
|
||||||
|
if not (item.validLocations overlaps slotMask): reject
|
||||||
|
|
||||||
|
if slotMask overlaps wearable mask 0x080001FF:
|
||||||
|
PlayerSystem.AutoWear(item)
|
||||||
|
else:
|
||||||
|
PlayerSystem.AutoWield(item, side)
|
||||||
|
|
||||||
|
AcceptWholeDollDrop(item):
|
||||||
|
require item.validLocations overlaps wearable body mask 0x08007FFF
|
||||||
|
require AutoWearIsLegal(item)
|
||||||
|
AutoWear(item)
|
||||||
|
```
|
||||||
|
|
||||||
|
`AutoWieldIsLegal` checks nonzero valid locations, missile/ammunition
|
||||||
|
compatibility, shield versus current weapon, and held-item restrictions while
|
||||||
|
in combat. One mask expression is corrupt in the named pseudo-C. Resolve that
|
||||||
|
exact branch from the older Ghidra chunk or a live breakpoint before porting
|
||||||
|
it; do not infer the missing bit from the error string alone.
|
||||||
|
|
||||||
|
### Race/body viewport variants
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmPaperDollUI::PostInit @ 0x004A5360`
|
||||||
|
- `gmPaperDollUI::UpdateForRace @ 0x004A3ED0`
|
||||||
|
|
||||||
|
Viewport `0x100001D5`, drag mask `0x100001D6`, drag overlay `0x1000046D`,
|
||||||
|
and slot-mode checkbox `0x100005BE` are bound in `PostInit`. The default camera
|
||||||
|
is approximately `(0.12, -2.4, 0.88)` with heading `191.37 degrees`; the
|
||||||
|
default animation is enum DID `0x10000005`.
|
||||||
|
|
||||||
|
Heritage/body property `0xBC` selects these static variants:
|
||||||
|
|
||||||
|
| Heritage value | Name | Camera position | Animation override |
|
||||||
|
|---:|---|---|---:|
|
||||||
|
| 6 | Gearknight | `(0.12, -3.0, 0.88)` | none |
|
||||||
|
| 7 | Tumerok | `(0.12, -3.0, 0.88)` | none |
|
||||||
|
| 8 | Lugian | `(0.12, -3.4, 1.0)` | none |
|
||||||
|
| 9 | Empyrean | `(0.12, -3.4, 0.88)` | none |
|
||||||
|
| 10 | Penumbraen | no explicit branch | none |
|
||||||
|
| 11 | Undead | no explicit branch | none |
|
||||||
|
| 12 | Olthoi | `(0.12, -3.4, 0.88)` | enum DID `0x10000011`, category 7 |
|
||||||
|
| 13 | Olthoi variant | `(0.12, -3.4, 0.88)` | enum DID `0x10000013`, category 7 |
|
||||||
|
|
||||||
|
The look-at point for explicit variants is `(0, 0, 0)`. Values 10 and 11 have
|
||||||
|
no explicit case in this function; preserve default/previous setup rather than
|
||||||
|
inventing a camera.
|
||||||
|
|
||||||
|
## 5. Character titles, luminance, and stat raises
|
||||||
|
|
||||||
|
Character layout: `0x2100002E`; Titles sublayout: `0x2100005E`; Titles page
|
||||||
|
element: `0x10000539`.
|
||||||
|
|
||||||
|
### Titles
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmCharacterTitleUI::PostInit @ 0x0049A610`
|
||||||
|
- `gmCharacterTitleUI::UpdateButtons @ 0x0049A500`
|
||||||
|
- `gmCharacterTitleUI::ListenToElementMessage @ 0x0049A6D0`
|
||||||
|
- `gmCharacterTitleUI::FindSortedInsertPosition @ 0x0049A760`
|
||||||
|
- `gmCharacterTitleUI::AddTitleToList @ 0x0049A840`
|
||||||
|
- `gmCharacterTitleUI::Refresh @ 0x0049ABC0`
|
||||||
|
- `gmCharacterTitleUI::RecvNotice_UpdateCharacterTitleTable @ 0x0049AD30`
|
||||||
|
- `gmCharacterTitleUI::RecvNotice_SetDisplayCharacterTitle @ 0x0049AD50`
|
||||||
|
|
||||||
|
| Meaning | ID |
|
||||||
|
|---|---:|
|
||||||
|
| Displayed title text | `0x1000052F` |
|
||||||
|
| Set/display button | `0x10000535` |
|
||||||
|
| Title listbox | `0x10000532` |
|
||||||
|
| Runtime row title text child | `0x10000537` |
|
||||||
|
| Runtime row title-ID enum property | `0x1000008E` |
|
||||||
|
|
||||||
|
```text
|
||||||
|
Refresh():
|
||||||
|
displayedTitle.text = titleTable.resolve(displayedTitleId) or "Unknown"
|
||||||
|
titleList.flush()
|
||||||
|
titleList.clearSelection()
|
||||||
|
for titleId in ownedTitleIds:
|
||||||
|
AddTitleToList(titleId)
|
||||||
|
UpdateButtons()
|
||||||
|
|
||||||
|
AddTitleToList(titleId):
|
||||||
|
titleText = titleTable.resolve(titleId)
|
||||||
|
insertIndex = first row whose text compares after titleText using wcscmp
|
||||||
|
row = createRuntimeTitleRow()
|
||||||
|
row.child(0x10000537).text = titleText
|
||||||
|
row.enumProperty[0x1000008E] = titleId
|
||||||
|
titleList.insert(insertIndex, row)
|
||||||
|
|
||||||
|
UpdateButtons():
|
||||||
|
selectedId = selectedRow.enumProperty[0x1000008E]
|
||||||
|
displayButton.state = ENABLED(1) if selectedId exists
|
||||||
|
and selectedId != displayedTitleId
|
||||||
|
else GHOSTED(13)
|
||||||
|
|
||||||
|
OnDisplayButtonClick():
|
||||||
|
send CM_Social.Event_SetDisplayCharacterTitle(selectedId)
|
||||||
|
|
||||||
|
OnTitleTableNotice(table):
|
||||||
|
copy table
|
||||||
|
Refresh()
|
||||||
|
|
||||||
|
OnDisplayedTitleNotice(titleId):
|
||||||
|
displayedTitleId = titleId
|
||||||
|
ensure titleId is present in the owned list
|
||||||
|
Refresh()
|
||||||
|
```
|
||||||
|
|
||||||
|
`gmStatManagementUI::UpdateCharacterInfo @ 0x004F0770` resolves the current
|
||||||
|
title through `CharacterTitleTable` and appends it to the gender/heritage
|
||||||
|
header. A synthetic inert titles page is therefore incomplete behavior.
|
||||||
|
|
||||||
|
### Luminance
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmStatManagementUI::PostInit @ 0x004EFD90`
|
||||||
|
- `gmStatManagementUI::UpdateExperience @ 0x004F0A70`
|
||||||
|
|
||||||
|
| Meaning | ID/property |
|
||||||
|
|---|---:|
|
||||||
|
| Luminance label element | `0x100005C5` |
|
||||||
|
| Luminance value element | `0x100005C6` |
|
||||||
|
| Total XP text | `0x10000235` |
|
||||||
|
| XP meter | `0x10000236` |
|
||||||
|
| Next XP text | `0x10000238` |
|
||||||
|
| XP list | `0x1000023D` |
|
||||||
|
| Level int quality | `0x19` |
|
||||||
|
| TotalExperience int64 quality | `1` |
|
||||||
|
| AvailableLuminance int64 quality | `6` |
|
||||||
|
| MaximumLuminance int64 quality | `7` |
|
||||||
|
| Meter float property | `0x69` |
|
||||||
|
|
||||||
|
```text
|
||||||
|
UpdateExperience():
|
||||||
|
level = player.intQuality(0x19)
|
||||||
|
totalXp = player.int64Quality(1)
|
||||||
|
availableLuminance = player.int64Quality(6)
|
||||||
|
maximumLuminance = player.int64Quality(7)
|
||||||
|
|
||||||
|
if level < 200 or maximumLuminance == 0:
|
||||||
|
luminanceLabel.text = ""
|
||||||
|
luminanceValue.text = ""
|
||||||
|
else:
|
||||||
|
luminanceLabel.text = localizedLuminanceLabel
|
||||||
|
luminanceValue.text = formatXp(availableLuminance)
|
||||||
|
+ localizedSeparator
|
||||||
|
+ formatXp(maximumLuminance)
|
||||||
|
|
||||||
|
update total/next XP strings
|
||||||
|
xpMeter.property[0x69] = fractionalProgressWithinCurrentLevel
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact localized label/separator is obscured in the generated pseudo-C.
|
||||||
|
Read it from the retail string table or capture the rendered panel before
|
||||||
|
claiming verbatim text.
|
||||||
|
|
||||||
|
### Server-authoritative raises
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmStatManagementUI::gmStatManagementUI @ 0x004F03F0`
|
||||||
|
- `gmAttributeUI::RaiseSelection @ 0x0049D020`
|
||||||
|
- `gmAttributeUI::Raise10 @ 0x0049D150`
|
||||||
|
- `gmSkillUI::RaiseSelection @ 0x0049C8C0`
|
||||||
|
- `gmSkillUI::Raise10 @ 0x0049B760`
|
||||||
|
- `InfoRegion::OnQualityChanged @ 0x004F0EB0`
|
||||||
|
- `gmStatManagementUI::ListenToElementMessage @ 0x004EFBE0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
OnRaiseClick(amount):
|
||||||
|
if awaitingRaise: return
|
||||||
|
require selected row and player description
|
||||||
|
exactCost = selectedRow.computeRetailCost(amount)
|
||||||
|
if local preconditions fail: return
|
||||||
|
|
||||||
|
awaitingRaise = true
|
||||||
|
send the appropriate TrainAttribute/TrainSkill event
|
||||||
|
clickedButton.state = GHOSTED(13)
|
||||||
|
// Do not mutate rank, advancement class, XP, credits, or costs locally.
|
||||||
|
|
||||||
|
InfoRegion.OnAuthoritativeQualityChanged(statType, statId):
|
||||||
|
refresh row from player qualities
|
||||||
|
broadcast element message 0x10000004(statType, statId)
|
||||||
|
|
||||||
|
StatManagement.OnElementMessage(0x10000004, statType, statId):
|
||||||
|
awaitingRaise = false
|
||||||
|
if changed stat is the selected row:
|
||||||
|
refresh footer and controls from authoritative qualities
|
||||||
|
```
|
||||||
|
|
||||||
|
This is direct static evidence against optimistic prediction. The current
|
||||||
|
implementation should remove its local `ApplyLocalRaise` mutation rather than
|
||||||
|
building a rollback ledger around behavior retail does not have.
|
||||||
|
|
||||||
|
The static class does not make the rejection-only path fully obvious when the
|
||||||
|
server sends no changed quality. Verify capped/invalid requests with a packet
|
||||||
|
trace or breakpoint before choosing how to release `awaitingRaise` on explicit
|
||||||
|
server failure. Retail still permits only one request in flight.
|
||||||
|
|
||||||
|
## 6. Items requiring live retail, cdb, or an older Ghidra chunk
|
||||||
|
|
||||||
|
These uncertainties are intentionally not guessed:
|
||||||
|
|
||||||
|
1. **Chat surface:** visually confirm whether this client build exposes its
|
||||||
|
multiple chat filters as separate floaty windows, tabs supplied by another
|
||||||
|
layout layer, or both. The named `gmMainChatUI` path alone has no tab logic.
|
||||||
|
2. **Chat strings:** capture the exact localized talk-focus labels, target
|
||||||
|
label, and squelch wording from the string table/live menu.
|
||||||
|
3. **Chat geometry:** capture a maximize/restore sequence near the top and
|
||||||
|
bottom edges to conformance-test the decompiler's signed half-height
|
||||||
|
arithmetic and DAT min/max constraints.
|
||||||
|
4. **Owned bag close packet:** trace packets while hiding inventory on an
|
||||||
|
owned side bag. Static evidence says this is not an external
|
||||||
|
`NoLongerViewingContents` event.
|
||||||
|
5. **External close handshake:** capture the order between
|
||||||
|
`gmExternalContainerUI::CloseCurrentContainer`, `ItemHolder::UseObject`, the
|
||||||
|
server response, and `ClientUISystem::SetGroundObject(0, true)`.
|
||||||
|
6. **Paperdoll legality:** recover the corrupt `AutoWieldIsLegal` mask branch
|
||||||
|
from the older Ghidra chunk and verify missile/ammo, shield, dual-wield, and
|
||||||
|
in-combat denials with golden cases.
|
||||||
|
7. **Paperdoll visuals:** visually verify race camera framing, Olthoi
|
||||||
|
animation enum results, click-map alignment, and the 0.2-second part pulse.
|
||||||
|
8. **Luminance text:** resolve exact localized label/value formatting from
|
||||||
|
the string table or rendered client.
|
||||||
|
9. **Raise rejection:** trace a capped/invalid raise that produces no quality
|
||||||
|
change to identify the exact event that clears `m_bAwaitingRaise`.
|
||||||
|
|
||||||
|
Everything else above is statically pinned strongly enough to drive the first
|
||||||
|
implementation and conformance-test wave.
|
||||||
1020
docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md
Normal file
1020
docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,769 @@
|
||||||
|
# Retained widget foundations -- named-retail pseudocode
|
||||||
|
|
||||||
|
**Date:** 2026-07-10
|
||||||
|
**Scope:** Wave 0 oracle for `UIElement_Text`, `UIElement_Button`, raw edge
|
||||||
|
layout, UI time delivery, tooltip cancellation, and focus ordering. This note
|
||||||
|
changes no runtime behavior.
|
||||||
|
|
||||||
|
## Sources and confidence
|
||||||
|
|
||||||
|
Primary evidence:
|
||||||
|
|
||||||
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`, the Sept 2013 EoR
|
||||||
|
PDB-named client decompilation.
|
||||||
|
- `docs/research/named-retail/acclient.h`, especially
|
||||||
|
`UIElement_Text::UIText_Flag`, `UIElement_Text`, `UIElement_Button`,
|
||||||
|
`UIElementManager`, `ElementDesc`, and `Box2D`.
|
||||||
|
- Static x86 disassembly of the matching `C:\Turbine\Asheron's Call\acclient.exe`
|
||||||
|
for the expressions that both Binary Ninja and Ghidra lost around x87
|
||||||
|
`_ftol2` calls in `UIElement::UpdateForParentSizeChange`.
|
||||||
|
|
||||||
|
Cross-checks:
|
||||||
|
|
||||||
|
- `docs/research/2026-06-15-layoutdesc-format.md` and the production LayoutDesc
|
||||||
|
data it records. Its property observations agree with the named code, but its
|
||||||
|
`AnchorEdges` reduction is intentionally lossy and its text-property table has
|
||||||
|
two margin fields transposed; the corrections are listed below.
|
||||||
|
- `docs/research/retail-ui/02-class-hierarchy.md` and
|
||||||
|
`docs/research/retail-ui/04-input-events.md`, the older `FUN_xxx` research.
|
||||||
|
These are useful as provenance but are superseded where they conflict with
|
||||||
|
the PDB-named client. In particular, the named client contains the full
|
||||||
|
`UIElement*` hierarchy and does **not** implement the inferred generic
|
||||||
|
`Device::RegisterTimerEvent` queue described there.
|
||||||
|
- The only vendored reference tree present in this worktree is WorldBuilder;
|
||||||
|
it does not implement the retail retained-widget system. No independent ACE,
|
||||||
|
AC2D, ACViewer, or holtburger UI implementation was available here to
|
||||||
|
corroborate these client-internal mechanics.
|
||||||
|
|
||||||
|
Confidence is **high** for every branch below unless it is explicitly marked
|
||||||
|
uncertain. Addresses were checked against `symbols.json` or the named function
|
||||||
|
header in the pseudo-C.
|
||||||
|
|
||||||
|
## Corrections that affect the implementation plan
|
||||||
|
|
||||||
|
1. Retail text interactivity is **two orthogonal capabilities**, not a
|
||||||
|
three-value exclusive mode. `Editable` (`0x16`) and `Selectable` (`0x27`)
|
||||||
|
can be independently false/true. An editable, non-selectable field accepts
|
||||||
|
edits but does not begin drag selection. The modern representation should
|
||||||
|
therefore be flags or two booleans; `Display` means both are false.
|
||||||
|
2. A newly constructed Type-12 text starts with `m_bitField = 0x300`
|
||||||
|
(`DIRTY | CURSOR_VISIBLE`), not editable and not selectable. Imported labels
|
||||||
|
must not be made interactive merely because their resolved type is 12.
|
||||||
|
3. There is no evidence for the old generic Device timer queue in this build.
|
||||||
|
Retail time-driven UI is a per-frame global message (`3`) plus explicit
|
||||||
|
deadline polling by `UIElementManager::CheckTooltip`. The unpumped
|
||||||
|
`UiRoot.RegisterTimerEvent` list is not a faithful retail mechanism.
|
||||||
|
4. Raw edge modes `3` and `4` cannot be reduced to `AnchorEdges`: `3` performs
|
||||||
|
exact inclusive-pixel centering and `4` proportionally scales the individual
|
||||||
|
edge coordinate with truncation toward zero.
|
||||||
|
5. In the older LayoutDesc note, the exact text margins should be
|
||||||
|
`0x23=left`, `0x24=right`, `0x25=upper`, `0x26=lower`, as shown by
|
||||||
|
`UIElement_Text::OnSetAttribute @ 0x0046A640`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `UIElement_Text`
|
||||||
|
|
||||||
|
### 1.1 Flag model and defaults
|
||||||
|
|
||||||
|
`acclient.h` defines the exact flags:
|
||||||
|
|
||||||
|
| Flag | Value | Meaning |
|
||||||
|
|---|---:|---|
|
||||||
|
| `UITF_EDITABLE` | `0x0001` | Editing/input capability |
|
||||||
|
| `UITF_ONE_LINE` | `0x0002` | One-line layout |
|
||||||
|
| `UITF_SELECTABLE` | `0x0004` | Selection/copy capability |
|
||||||
|
| `UITF_NO_IME` | `0x0008` | Do not enable IME |
|
||||||
|
| `UITF_OUTLINE` | `0x0010` | Outline glyphs |
|
||||||
|
| `UITF_DROPSHADOW` | `0x0020` | Drop shadow glyphs |
|
||||||
|
| `UITF_MOUSE_SELECTING` | `0x0040` | Pointer drag is active |
|
||||||
|
| `UITF_SELECTING` | `0x0080` | Selection anchor/end are active |
|
||||||
|
| `UITF_DIRTY` | `0x0100` | Glyph/layout cache dirty |
|
||||||
|
| `UITF_CURSOR_VISIBLE` | `0x0200` | Caret blink phase visible |
|
||||||
|
| `UITF_FIT_TO_TEXT` | `0x0400` | Resize element to paper |
|
||||||
|
| `UITF_TRUNCATE_TEXT_TO_FIT` | `0x0800` | Trailer/truncation path |
|
||||||
|
| `UITF_LOSE_FOCUS_ON_ESCAPE` | `0x1000` | Escape relinquishes focus |
|
||||||
|
| `UITF_LOSE_FOCUS_ON_ACCEPT_INPUT` | `0x2000` | Accept-input action relinquishes focus |
|
||||||
|
|
||||||
|
`UIElement_Text::UIElement_Text @ 0x00468570` initializes the flag word to
|
||||||
|
`0x300`, selection/cursor positions to zero, horizontal justification to `2`,
|
||||||
|
vertical justification to `4`, margins to zero, and cursor/flash timestamps to
|
||||||
|
`-1.0`. Thus the retail default is a display-only label.
|
||||||
|
|
||||||
|
### 1.2 Interaction-relevant property IDs
|
||||||
|
|
||||||
|
`UIElement_Text::OnSetAttribute @ 0x0046A640` first delegates to
|
||||||
|
`UIElement_Scrollable::OnSetAttribute`, then handles these properties:
|
||||||
|
|
||||||
|
| Property | Value kind | Exact effect |
|
||||||
|
|---:|---|---|
|
||||||
|
| `0x14` | enum | Horizontal justification |
|
||||||
|
| `0x15` | enum | Vertical justification |
|
||||||
|
| `0x16` | bool | `SetEditable`; then recompute mouse visibility |
|
||||||
|
| `0x17` | `StringInfo` | Set contents, or clear when invalid |
|
||||||
|
| `0x1A` | array/data-id | Re-resolve current font |
|
||||||
|
| `0x1B` | array/color | Re-resolve current font color |
|
||||||
|
| `0x1D` | array/color | Re-resolve tag font color |
|
||||||
|
| `0x1E` | integer | Maximum characters |
|
||||||
|
| `0x1F` | bool | `SetNoIme` |
|
||||||
|
| `0x20` | bool | `SetOneLine` |
|
||||||
|
| `0x21` | bool | `SetOutline` |
|
||||||
|
| `0x22` | color | Outline color; then font reset |
|
||||||
|
| `0x23` | integer | Left margin |
|
||||||
|
| `0x24` | integer | Right margin |
|
||||||
|
| `0x25` | integer | Upper margin |
|
||||||
|
| `0x26` | integer | Lower margin |
|
||||||
|
| `0x27` | bool | `SetSelectable`; then recompute mouse visibility |
|
||||||
|
| `0x28` | bool | Glyph list `TrimFromTop` |
|
||||||
|
| `0x29` | bool | `SetFitToText` |
|
||||||
|
| `0xC7` | `StringInfo` | Enable/define truncation trailer; invalid disables it |
|
||||||
|
| `0xCB` | bool | Lose focus on Escape |
|
||||||
|
| `0xCC` | bool | Lose focus on accept-input |
|
||||||
|
|
||||||
|
### 1.3 Capability setters
|
||||||
|
|
||||||
|
Sources: `SetEditable @ 0x004680D0`, `SetOneLine @ 0x00468170`, and
|
||||||
|
`SetSelectable @ 0x004681C0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SetEditable(value):
|
||||||
|
if value == current Editable: return
|
||||||
|
|
||||||
|
if value is false:
|
||||||
|
if this has global focus AND Selectable is false:
|
||||||
|
RelinquishFocus()
|
||||||
|
else if TruncateTextToFit is true:
|
||||||
|
clear TruncateTextToFit
|
||||||
|
mark dirty and dirty the root
|
||||||
|
|
||||||
|
set Editable = value
|
||||||
|
|
||||||
|
SetSelectable(value):
|
||||||
|
if value == current Selectable: return
|
||||||
|
|
||||||
|
if value is false:
|
||||||
|
if this has global focus AND Editable is false:
|
||||||
|
RelinquishFocus()
|
||||||
|
else if TruncateTextToFit is true:
|
||||||
|
clear TruncateTextToFit
|
||||||
|
mark dirty and dirty the root
|
||||||
|
|
||||||
|
set Selectable = value
|
||||||
|
if the internal selection-direction marker is active:
|
||||||
|
clear selecting/mouse-selecting and zero both selection endpoints
|
||||||
|
|
||||||
|
SetOneLine(value):
|
||||||
|
if value differs:
|
||||||
|
set OneLine = value
|
||||||
|
mark dirty and dirty the root
|
||||||
|
```
|
||||||
|
|
||||||
|
The high internal selection-direction bit checked by `SetSelectable` and
|
||||||
|
`SetCursorPosition` is not named in `UIText_Flag`; it is deliberately not given
|
||||||
|
a speculative public name here.
|
||||||
|
|
||||||
|
`SetTruncateTextToFit @ 0x00467480` refuses to enable truncation whenever
|
||||||
|
either `Editable` or `Selectable` is set.
|
||||||
|
|
||||||
|
### 1.4 Mouse visibility
|
||||||
|
|
||||||
|
Sources: `UIElement_Text::GetShouldBeMouseVisible @ 0x00467460` and
|
||||||
|
`UIElement::GetShouldBeMouseVisible @ 0x004601B0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Text.GetShouldBeMouseVisible():
|
||||||
|
if Editable OR Selectable:
|
||||||
|
return true
|
||||||
|
return base.GetShouldBeMouseVisible()
|
||||||
|
|
||||||
|
Base.GetShouldBeMouseVisible():
|
||||||
|
return explicit mouse-visible/interaction flag OR valid tooltip text
|
||||||
|
```
|
||||||
|
|
||||||
|
Consequently, a plain imported label does not claim the mouse unless another
|
||||||
|
base property (for example a tooltip/explicit mouse role) says it should.
|
||||||
|
|
||||||
|
### 1.5 Mouse down, selection, and activation
|
||||||
|
|
||||||
|
Sources: `MouseDown @ 0x00469370`, `MouseUp @ 0x004694F0`, and
|
||||||
|
`MouseMove @ 0x004695F0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
MouseDown(screenX, screenY, mouseAction):
|
||||||
|
remember whether this already had focus
|
||||||
|
call Scrollable.MouseDown(...)
|
||||||
|
|
||||||
|
if hidden/blocked base flags reject input: return
|
||||||
|
if mouseAction is not one of the accepted text actions (5, 6, 7): return
|
||||||
|
if the base action-enable bit is false: return
|
||||||
|
|
||||||
|
local = screen point - screen origin - margins
|
||||||
|
|
||||||
|
if mouseAction == 7 AND (Editable OR Selectable):
|
||||||
|
read bool property 0xD1
|
||||||
|
if this did not previously have focus AND property 0xD1 is true:
|
||||||
|
SelectAll()
|
||||||
|
return
|
||||||
|
|
||||||
|
hitPos = DeterminePositionFromXY(local)
|
||||||
|
previousAnchor = selectionStart when the internal selection direction
|
||||||
|
is active, otherwise current cursor
|
||||||
|
SetCursorPosition(hitPos, DontSelectText)
|
||||||
|
Activate() // activate the owning root
|
||||||
|
|
||||||
|
if Selectable:
|
||||||
|
set Selecting and MouseSelecting
|
||||||
|
if Shift is down:
|
||||||
|
selectionStart = previousAnchor
|
||||||
|
selectionEnd = cursor
|
||||||
|
else:
|
||||||
|
selectionStart = cursor
|
||||||
|
|
||||||
|
MouseMove(screenX, screenY):
|
||||||
|
if not MouseSelecting:
|
||||||
|
call base MouseMove and return
|
||||||
|
local = screen point - screen origin - margins
|
||||||
|
SetCursorPosition(DeterminePositionFromXY(local), Default)
|
||||||
|
|
||||||
|
MouseUp(screenX, screenY, mouseAction):
|
||||||
|
remember whether a glyph callback is registered for mouseAction
|
||||||
|
call base MouseUp(...)
|
||||||
|
|
||||||
|
if MouseSelecting:
|
||||||
|
set cursor from release point using Default
|
||||||
|
clear MouseSelecting
|
||||||
|
|
||||||
|
if a glyph callback was registered:
|
||||||
|
glyph = glyph at release point
|
||||||
|
if glyph has a callback: invoke it with mouseAction
|
||||||
|
```
|
||||||
|
|
||||||
|
Important boundary: `Editable` alone allows caret placement and root
|
||||||
|
activation, but only `Selectable` starts pointer selection. The method calls
|
||||||
|
`Activate`, not `TakeFocus`, so the focus transition remains governed by the
|
||||||
|
root's remembered focus element and explicit `TakeFocus` requests.
|
||||||
|
|
||||||
|
### 1.6 Focus registration and key capabilities
|
||||||
|
|
||||||
|
Sources: `RegisterInputMaps @ 0x00467350`, `UnregisterInputMaps @
|
||||||
|
0x004673D0`, `ListenToElementMessage @ 0x004687C0`, and `OnAction @
|
||||||
|
0x0046A260`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
RegisterInputMaps(priority):
|
||||||
|
unregister this callback first
|
||||||
|
result = Scrollable.RegisterInputMaps(priority)
|
||||||
|
if Editable:
|
||||||
|
register input map 1 at priority - 10
|
||||||
|
register input map 7 at priority
|
||||||
|
if Editable OR Selectable:
|
||||||
|
register input map 8 at priority
|
||||||
|
return OR of registration results
|
||||||
|
|
||||||
|
On focus message 0x2F for this element:
|
||||||
|
if neither Editable nor Selectable: delegate to base
|
||||||
|
|
||||||
|
if Editable:
|
||||||
|
on gain:
|
||||||
|
register as ICIDM input handler
|
||||||
|
turn IME on unless disabled
|
||||||
|
ask Keystone to lose its external focus
|
||||||
|
on loss:
|
||||||
|
unregister ICIDM input handler
|
||||||
|
|
||||||
|
on gain:
|
||||||
|
ICIDM.SetTextMode(true)
|
||||||
|
subscribe to global message 3
|
||||||
|
on loss:
|
||||||
|
Deselect()
|
||||||
|
ICIDM.SetTextMode(false)
|
||||||
|
unsubscribe from global message 3
|
||||||
|
|
||||||
|
delegate to Scrollable.ListenToElementMessage
|
||||||
|
```
|
||||||
|
|
||||||
|
Map 8 exposes cursor movement, line/document home/end, up/down/page movement,
|
||||||
|
and Copy for both editable and selectable text. Editable-only maps add character
|
||||||
|
editing. `Cut`, `Paste`, and deletion helpers independently guard on
|
||||||
|
`UITF_EDITABLE`, so merely selectable text cannot mutate its contents.
|
||||||
|
|
||||||
|
The action at `0x25` relinquishes focus when `0xCC` is set; the action at
|
||||||
|
`0x27` relinquishes focus when `0xCB` is set. Actions `0x26` and `0x28` are
|
||||||
|
forward-delete and backspace behavior respectively, deleting the active
|
||||||
|
selection first.
|
||||||
|
|
||||||
|
### 1.7 Per-frame caret work
|
||||||
|
|
||||||
|
`UIElement_Text::Global_Loop @ 0x00469FC0` runs on global message `3`.
|
||||||
|
It checks asynchronous string downloads, then, only while
|
||||||
|
`(Editable || Selectable) && HasFocus`, uses `GetCaretBlinkTime()` converted
|
||||||
|
from milliseconds to seconds to flip `UITF_CURSOR_VISIBLE` and dirty the last
|
||||||
|
caret rectangle. This is subscription-driven; it is not a registered timer
|
||||||
|
object.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `UIElement_Button`
|
||||||
|
|
||||||
|
### 2.1 State inputs and numeric states
|
||||||
|
|
||||||
|
Source: `UIElement_Button::UpdateState_ @ 0x00471CF0`, verified against
|
||||||
|
the matching x86 instructions.
|
||||||
|
|
||||||
|
The method reads these bool properties:
|
||||||
|
|
||||||
|
- `0x0D`: ghosted/disabled role;
|
||||||
|
- `0x0E`: selected/toggled role;
|
||||||
|
- `0x13`: allow rollover-state change role.
|
||||||
|
|
||||||
|
It combines them with `mousePressedOnButton` and the inherited
|
||||||
|
`mouseOverTop` bit. The standard state IDs are:
|
||||||
|
|
||||||
|
| Visual phase | Unselected | Selected |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Normal | `1` | `6` |
|
||||||
|
| Rollover / pressed outside | `2` | `7` |
|
||||||
|
| Pressed while pointer is over | `3` | `8` |
|
||||||
|
| Ghosted | `13` | `13` |
|
||||||
|
|
||||||
|
```text
|
||||||
|
UpdateState_():
|
||||||
|
disabled = bool property 0x0D
|
||||||
|
if disabled:
|
||||||
|
requested = 13
|
||||||
|
else:
|
||||||
|
selected = bool property 0x0E
|
||||||
|
rolloverEnabled = bool property 0x13
|
||||||
|
|
||||||
|
requested = selected ? 6 : 1
|
||||||
|
|
||||||
|
if mousePressedOnButton OR (rolloverEnabled AND mouseOverTop):
|
||||||
|
requested = selected ? 7 : 2
|
||||||
|
|
||||||
|
if mousePressedOnButton AND mouseOverTop:
|
||||||
|
requested = selected ? 8 : 3
|
||||||
|
|
||||||
|
if ElementDesc contains requested state:
|
||||||
|
virtual SetState(requested)
|
||||||
|
else:
|
||||||
|
leave the current state unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
That last test is the exact fallback rule. There is no search for a nearest
|
||||||
|
standard state. A custom semantic state survives only when the newly requested
|
||||||
|
standard state is absent from the element's DAT state table.
|
||||||
|
|
||||||
|
### 2.2 `SetState` synchronization
|
||||||
|
|
||||||
|
Source: `UIElement_Button::SetState @ 0x00471DA0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SetState(requested):
|
||||||
|
if bool property 0x0B is true: // toggle behavior
|
||||||
|
selected = bool property 0x0E
|
||||||
|
if requested == 6 AND not selected:
|
||||||
|
set property 0x0E = true
|
||||||
|
return true
|
||||||
|
if requested == 1 AND selected:
|
||||||
|
set property 0x0E = false
|
||||||
|
return true
|
||||||
|
|
||||||
|
wantsDisabled = requested == 13
|
||||||
|
disabled = bool property 0x0D
|
||||||
|
if wantsDisabled != disabled:
|
||||||
|
set property 0x0D = wantsDisabled
|
||||||
|
return true
|
||||||
|
|
||||||
|
return UIElement.SetState(requested)
|
||||||
|
```
|
||||||
|
|
||||||
|
Thus programmatic `SetState(1/6)` is also the selected-property API for a
|
||||||
|
toggle button, and `SetState(13)` is the disabled-property API. Property
|
||||||
|
changes call `UpdateState_` through `OnSetAttribute`.
|
||||||
|
|
||||||
|
### 2.3 Mouse state machine
|
||||||
|
|
||||||
|
Sources: `MouseDown @ 0x00471FF0`, `MouseUp @ 0x004720E0`, and
|
||||||
|
`MouseOverTop @ 0x004721F0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
MouseOverTop(isOver):
|
||||||
|
base.MouseOverTop(isOver)
|
||||||
|
UpdateState_()
|
||||||
|
|
||||||
|
MouseDown(x, y, mouseAction):
|
||||||
|
Text.MouseDown(...)
|
||||||
|
if inherited visibility/input gates reject: return
|
||||||
|
if mouseAction is neither 7 nor accepted alternate action 0x0A: return
|
||||||
|
|
||||||
|
mousePressedOnButton = true
|
||||||
|
UpdateState_()
|
||||||
|
|
||||||
|
disabled = bool property 0x0D
|
||||||
|
allowHotClickWhileDisabled = bool property 0x0C
|
||||||
|
if disabled AND not allowHotClickWhileDisabled: return
|
||||||
|
|
||||||
|
if bool property 0x0F: // hot-click/auto-repeat
|
||||||
|
BroadcastElementMessage(2, mouseAction, 0) // immediate activation
|
||||||
|
hotClickingInProgress = true
|
||||||
|
subscribe to global message 3
|
||||||
|
nextHotClickTime = Timer.cur_time + float property 0x10
|
||||||
|
|
||||||
|
MouseUp(x, y, mouseAction):
|
||||||
|
Text.MouseUp(...)
|
||||||
|
disabled = bool property 0x0D
|
||||||
|
|
||||||
|
if mousePressedOnButton AND mouseAction is 7 or 0x0A:
|
||||||
|
emitClick = false
|
||||||
|
|
||||||
|
if mouseOverTop AND not disabled:
|
||||||
|
if bool property 0x0B: // toggle
|
||||||
|
property 0x0E = !property 0x0E
|
||||||
|
if not hotClickingInProgress:
|
||||||
|
emitClick = true
|
||||||
|
|
||||||
|
mousePressedOnButton = false
|
||||||
|
if hotClickingInProgress:
|
||||||
|
hotClickingInProgress = false
|
||||||
|
unsubscribe from global message 3
|
||||||
|
|
||||||
|
UpdateState_()
|
||||||
|
if emitClick:
|
||||||
|
BroadcastElementMessage(1, 7, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Release outside never emits message `1`. During a captured press, moving outside
|
||||||
|
changes state `3/8 -> 2/7`; moving back inside restores `3/8`. A hot-click press
|
||||||
|
emits message `2` immediately and suppresses the ordinary release click.
|
||||||
|
|
||||||
|
### 2.4 Hot-click deadline delivery and cancellation
|
||||||
|
|
||||||
|
Sources: `ListenToGlobalMessage @ 0x00471C60`, `OnSetAttribute @
|
||||||
|
0x00471F40`, and the button destructor at `0x00471BC0`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
On global message 3 while hotClickingInProgress:
|
||||||
|
now = Timer.cur_time
|
||||||
|
|
||||||
|
if not mouseOverTop AND now >= nextHotClickTime:
|
||||||
|
nextHotClickTime = now
|
||||||
|
|
||||||
|
if mouseOverTop AND now >= nextHotClickTime:
|
||||||
|
BroadcastElementMessage(2, 0, 0)
|
||||||
|
nextHotClickTime += float property 0x11
|
||||||
|
```
|
||||||
|
|
||||||
|
Cancellation is explicit and idempotent at all three owner transitions:
|
||||||
|
|
||||||
|
- mouse up clears `hotClickingInProgress` and unsubscribes from global `3`;
|
||||||
|
- changing bool property `0x0F` to false does the same;
|
||||||
|
- destruction does the same if a hot click is active.
|
||||||
|
|
||||||
|
`OnSetAttribute` recalculates visual state for `0x0D`, `0x0E`, and `0x13`;
|
||||||
|
`0x0D` also recomputes mouse visibility.
|
||||||
|
|
||||||
|
The descriptive names for button properties `0x0B..0x13` are not present in
|
||||||
|
the recovered headers. Their behavioral roles above are exact; do not bake the
|
||||||
|
descriptive labels into a wire/DAT enum until the master property-name table is
|
||||||
|
recovered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `UIElement::UpdateForParentSizeChange`
|
||||||
|
|
||||||
|
**Oracle:** `UIElement::UpdateForParentSizeChange @ 0x00462640`.
|
||||||
|
|
||||||
|
`Box2D` is inclusive: width is `x1 - x0 + 1`, height is
|
||||||
|
`y1 - y0 + 1`. For a root/no-parent element (or the relevant root-sizing flag),
|
||||||
|
the original parent box is `[0,0,layout.displayWidth-1,layout.displayHeight-1]`
|
||||||
|
and the current parent box is `[0,0,displayWidth-1,displayHeight-1]`.
|
||||||
|
Otherwise, both boxes come from the parent.
|
||||||
|
|
||||||
|
```text
|
||||||
|
orig = this.GetOriginalPosition()
|
||||||
|
origParent = parent.GetOriginalPosition() or layout display box
|
||||||
|
curParent = parent.GetCurrentPosition() or current display box
|
||||||
|
|
||||||
|
origParentWidth = origParent.x1 - origParent.x0 + 1
|
||||||
|
origParentHeight = origParent.y1 - origParent.y0 + 1
|
||||||
|
curParentWidth = curParent.x1 - curParent.x0 + 1
|
||||||
|
curParentHeight = curParent.y1 - curParent.y0 + 1
|
||||||
|
|
||||||
|
deltaX = curParentWidth - origParentWidth
|
||||||
|
deltaY = curParentHeight - origParentHeight
|
||||||
|
|
||||||
|
scaleX = origParentWidth != 0 ? curParentWidth / origParentWidth : 0.0
|
||||||
|
scaleY = origParentHeight != 0 ? curParentHeight / origParentHeight : 0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
All ratios are floating point. Mode-4 conversion uses MSVC `_ftol2`, i.e.
|
||||||
|
truncate toward zero, not floor and not round-to-nearest.
|
||||||
|
|
||||||
|
Define `halfTowardZero(n) = truncate(n / 2)` for signed integers.
|
||||||
|
|
||||||
|
```text
|
||||||
|
newX0 = orig.x0
|
||||||
|
switch LeftEdge:
|
||||||
|
2: newX0 = orig.x0 + deltaX
|
||||||
|
3: newX0 = halfTowardZero(curParentWidth)
|
||||||
|
- halfTowardZero(orig.width)
|
||||||
|
4: newX0 = truncateTowardZero(orig.x0 * scaleX)
|
||||||
|
0,1: leave orig.x0 for now
|
||||||
|
|
||||||
|
newX1 = orig.x1
|
||||||
|
switch RightEdge:
|
||||||
|
1: newX1 = orig.x1 + deltaX
|
||||||
|
3: newX1 = halfTowardZero(curParentWidth)
|
||||||
|
+ halfTowardZero(orig.width) - 1
|
||||||
|
4: newX1 = truncateTowardZero(orig.x1 * scaleX)
|
||||||
|
0,2: leave orig.x1 for now
|
||||||
|
|
||||||
|
newY0 = orig.y0
|
||||||
|
switch TopEdge:
|
||||||
|
2: newY0 = orig.y0 + deltaY
|
||||||
|
3: newY0 = halfTowardZero(curParentHeight)
|
||||||
|
- halfTowardZero(orig.height)
|
||||||
|
4: newY0 = truncateTowardZero(orig.y0 * scaleY)
|
||||||
|
0,1: leave orig.y0 for now
|
||||||
|
|
||||||
|
newY1 = orig.y1
|
||||||
|
switch BottomEdge:
|
||||||
|
1: newY1 = orig.y1 + deltaY
|
||||||
|
3: newY1 = halfTowardZero(curParentHeight)
|
||||||
|
+ halfTowardZero(orig.height) - 1
|
||||||
|
4: newY1 = truncateTowardZero(orig.y1 * scaleY)
|
||||||
|
0,2: leave orig.y1 for now
|
||||||
|
```
|
||||||
|
|
||||||
|
Then retail applies mode `0` as a preservation policy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
if current width != 0 OR current height != 0 OR preserve-current flag is set:
|
||||||
|
if LeftEdge == 0: newX0 = current.x0
|
||||||
|
if TopEdge == 0: newY0 = current.y0
|
||||||
|
if RightEdge == 0: newX1 = current.x1
|
||||||
|
if BottomEdge == 0: newY1 = current.y1
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MoveTo(newX0, newY0)
|
||||||
|
ResizeTo(newX1 - newX0 + 1, newY1 - newY0 + 1)
|
||||||
|
|
||||||
|
if original z-level differs from current z-level:
|
||||||
|
restore original z-level
|
||||||
|
ask parent to remove/reinsert this child in z-order
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw-mode summary
|
||||||
|
|
||||||
|
| Raw mode | Near edge (`Left`/`Top`) | Far edge (`Right`/`Bottom`) |
|
||||||
|
|---:|---|---|
|
||||||
|
| `0` | Preserve current edge after initialization | Preserve current edge after initialization |
|
||||||
|
| `1` | Keep original near edge | Add parent size delta (stretch) |
|
||||||
|
| `2` | Add parent size delta (move with far side) | Keep original far edge |
|
||||||
|
| `3` | Recompute centered near edge | Recompute centered far edge |
|
||||||
|
| `4` | Proportionally scale original coordinate | Proportionally scale original coordinate |
|
||||||
|
|
||||||
|
Mode `4` does **not** mean “both anchors.” That description in the earlier
|
||||||
|
LayoutDesc note was only an approximation of the resulting resize for common
|
||||||
|
assets and must not survive in the imported layout policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Time delivery, tooltip cancellation, and focus ordering
|
||||||
|
|
||||||
|
### 4.1 No generic Device timer queue in the named client
|
||||||
|
|
||||||
|
The old `04-input-events.md` inferred a Device vtable timer API from unnamed
|
||||||
|
calls. The recovered `UIElementManager` fields in `acclient.h` contain tooltip
|
||||||
|
timestamps but no timer collection, and the named execution path is explicit.
|
||||||
|
The exact per-frame method is `UIElementManager::UseTime @ 0x0045CFD0`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UseTime():
|
||||||
|
CleanDeleteQueue()
|
||||||
|
ProcessUIMessageRemovalData()
|
||||||
|
if mouse hit-test requested:
|
||||||
|
DoMouseUpdate()
|
||||||
|
CheckTooltip()
|
||||||
|
BroadcastGlobalMessage(3, 0)
|
||||||
|
if ICIDM exists:
|
||||||
|
ICIDM.UseTime()
|
||||||
|
DrawDirtyRegions()
|
||||||
|
```
|
||||||
|
|
||||||
|
Global message `3` is the shared time pulse used by text caret/download work and
|
||||||
|
button hot-click repeat. Subscription and unsubscription are the cancellation
|
||||||
|
mechanism. Deletions and deferred listener removals are processed **before** the
|
||||||
|
time pulse.
|
||||||
|
|
||||||
|
### 4.2 Tooltip deadlines
|
||||||
|
|
||||||
|
Sources: manager constructor `0x0045F5D0`, `CheckTooltip @ 0x0045B6E0`,
|
||||||
|
`StartHover @ 0x00459250`, `StopHover @ 0x00459280`, `SwitchMouseOver @
|
||||||
|
0x0045B560`, `MouseMoveHandler @ 0x0045E710`, and `ReleaseMouseCapture @
|
||||||
|
0x0045D2B0`.
|
||||||
|
|
||||||
|
Defaults are **0.25 seconds** delay and **10 seconds** duration. The delay is a
|
||||||
|
user preference (`Misc.TooltipDelay`, allowed range 0..10 seconds); per-element
|
||||||
|
float property `0x50` overrides it.
|
||||||
|
|
||||||
|
```text
|
||||||
|
MouseMoveHandler(...):
|
||||||
|
lastMouseMoveTime = Timer.local_time
|
||||||
|
refresh the element under the pointer and call SwitchMouseOver
|
||||||
|
continue drag processing
|
||||||
|
|
||||||
|
ReleaseMouseCapture(owner):
|
||||||
|
if owner holds capture:
|
||||||
|
lastMouseMoveTime = Timer.local_time
|
||||||
|
decrement/release capture
|
||||||
|
|
||||||
|
CheckTooltip():
|
||||||
|
now = Timer.local_time
|
||||||
|
|
||||||
|
if hover has not started AND mouse is inside the window:
|
||||||
|
if no element has mouse capture AND lastEntered exists:
|
||||||
|
delay = lastEntered.floatProperty(0x50) or manager default
|
||||||
|
if now >= lastMouseMoveTime + delay:
|
||||||
|
StartHover(currentMouseX, currentMouseY)
|
||||||
|
|
||||||
|
if tooltip element exists:
|
||||||
|
if now - tooltipStart >= tooltipDuration:
|
||||||
|
queue tooltip element for deletion
|
||||||
|
tooltipElement = null
|
||||||
|
SwitchMouseOver(null)
|
||||||
|
|
||||||
|
StartHover(x, y):
|
||||||
|
if hover has not started:
|
||||||
|
hoverStarted = lastEntered.MouseHover(x, y)
|
||||||
|
|
||||||
|
StopHover():
|
||||||
|
if hoverStarted:
|
||||||
|
hoverStarted = false
|
||||||
|
lastEntered?.MouseUnhover()
|
||||||
|
if tooltipElement exists:
|
||||||
|
queue it for deletion
|
||||||
|
tooltipElement = null
|
||||||
|
```
|
||||||
|
|
||||||
|
`SwitchMouseOver(old -> new)` calls `StopHover` before sending
|
||||||
|
`old.MouseOverTop(false)`, then sends `new.MouseOverTop(true)`. Capture prevents
|
||||||
|
a new tooltip deadline from starting. `UIElementManager::DeletingElement @
|
||||||
|
0x0045E520` also clears capture/hover/drag/tooltip ownership and removes queued
|
||||||
|
element messages that target the deleted element.
|
||||||
|
|
||||||
|
There is therefore no “remove a due timer before callback” retail rule to port
|
||||||
|
for tooltips. The faithful rule is owner-state cancellation plus subscription
|
||||||
|
teardown before the global time pulse.
|
||||||
|
|
||||||
|
### 4.3 Focus transfer order
|
||||||
|
|
||||||
|
Sources: `UIElementManager::SetFocusElement @ 0x0045B890`,
|
||||||
|
`UIElement::TakeFocus @ 0x004601E0`, `UIElement::RelinquishFocus @
|
||||||
|
0x00461DE0`, and `UIElementManager::BroadcastElementMessage @ 0x0045AB40`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
SetFocusElement(newFocus):
|
||||||
|
oldFocus = focusElement
|
||||||
|
if oldFocus == newFocus: return
|
||||||
|
|
||||||
|
if oldFocus exists:
|
||||||
|
BroadcastElementMessage(oldFocus, 0x2F, param1=0, param2=0)
|
||||||
|
oldFocus.UnregisterInputMaps()
|
||||||
|
|
||||||
|
focusElement = newFocus
|
||||||
|
|
||||||
|
if newFocus exists:
|
||||||
|
BroadcastElementMessage(newFocus, 0x2F, param1=1, param2=0)
|
||||||
|
newFocus.RegisterInputMaps(priority=0x0BB8) // 3000
|
||||||
|
|
||||||
|
TakeFocus(element):
|
||||||
|
root = element.GetRootElement()
|
||||||
|
if root is null: return false
|
||||||
|
set element's owns-focus marker
|
||||||
|
if root is active:
|
||||||
|
SetFocusElement(element)
|
||||||
|
becameGlobal = true
|
||||||
|
root.rememberedFocusDescendant = element
|
||||||
|
return becameGlobal
|
||||||
|
|
||||||
|
RelinquishFocus(element):
|
||||||
|
root = element.GetRootElement()
|
||||||
|
if root exists:
|
||||||
|
root.rememberedFocusDescendant = null
|
||||||
|
clear element's owns-focus marker
|
||||||
|
if manager.focusElement == element:
|
||||||
|
SetFocusElement(null)
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
Focus messages are synchronous. If a handler broadcasts another element message
|
||||||
|
while one is already broadcasting, `BroadcastElementMessage` appends it to a
|
||||||
|
FIFO list and drains that list before clearing the broadcasting flag. This
|
||||||
|
prevents recursive listener traversal while retaining deterministic order.
|
||||||
|
|
||||||
|
Active-root changes are separate from text focus. `UIElement::Activate @
|
||||||
|
0x00461C80` activates/registers the root and restores its remembered focus
|
||||||
|
descendant through `SetFocusElement`; `Deactivate @ 0x00461D50` broadcasts root
|
||||||
|
deactivation and clears global focus when the root owned it.
|
||||||
|
|
||||||
|
### 4.4 Keyboard routing relative to focus
|
||||||
|
|
||||||
|
`UIElementManager::KeyPressEvent @ 0x0045C300` uses this order when the debug
|
||||||
|
console is not intercepting input:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target = focusElement if present else activeElement
|
||||||
|
if target exists:
|
||||||
|
consumed = target.KeyDown(action, extent)
|
||||||
|
if not consumed:
|
||||||
|
BroadcastGlobalMessage(1, action)
|
||||||
|
else:
|
||||||
|
BroadcastGlobalMessage(1, action)
|
||||||
|
|
||||||
|
DoVisibilityToggleAction(action) // still runs after the route above
|
||||||
|
```
|
||||||
|
|
||||||
|
That final visibility-toggle call is unconditional within the normal input path;
|
||||||
|
it is not gated by whether the focus/active target consumed `KeyDown`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Conformance vectors to carry into Wave 1
|
||||||
|
|
||||||
|
1. **Text defaults:** construct Type 12 with no interaction properties; assert
|
||||||
|
editable=false, selectable=false, and no mouse claim absent tooltip/base role.
|
||||||
|
2. **Orthogonal capabilities:** cover all four `(editable, selectable)` pairs.
|
||||||
|
Editable-only places the caret but does not drag-select; selectable-only
|
||||||
|
selects/copies but cut/paste/delete do not mutate.
|
||||||
|
3. **Focus teardown:** disabling the last active text capability while focused
|
||||||
|
relinquishes focus; disabling one while the other remains does not.
|
||||||
|
4. **Button states:** golden table for states `1/2/3/6/7/8/13`, captured release
|
||||||
|
outside, re-entry, toggle on/off, and missing-state fallback.
|
||||||
|
5. **Hot click:** immediate message `2`, initial delay `0x10`, repeat interval
|
||||||
|
`0x11`, pause/reset while pointer is outside, and cancellation on mouse-up,
|
||||||
|
property disable, and destruction.
|
||||||
|
6. **Layout:** one vector per raw mode on each individual edge; include odd and
|
||||||
|
even parent/child sizes, negative intermediate centered values, a zero-sized
|
||||||
|
original parent, mode-0 preservation after initialization, and nested parents.
|
||||||
|
7. **Time order:** deletion/removal before global tick `3`; tooltip check before
|
||||||
|
tick `3`; capture release restarts the tooltip idle deadline.
|
||||||
|
8. **Focus order:** old loss message -> old map removal -> pointer assignment ->
|
||||||
|
new gain message -> new map registration; nested focus messages drain FIFO.
|
||||||
|
|
||||||
|
## Remaining uncertainties
|
||||||
|
|
||||||
|
- The recovered symbols do not name button properties `0x0B..0x13`; this note
|
||||||
|
records exact observed behavior but intentionally does not assert canonical
|
||||||
|
property names for all of them.
|
||||||
|
- Mouse action `7` is the normal primary-button path in these methods. Alternate
|
||||||
|
action `0x0A` is accepted by `UIElement_Button`; its user-facing input name was
|
||||||
|
not recovered in this bounded pass.
|
||||||
|
- The high text selection-direction bit used in `SetCursorPosition` is outside
|
||||||
|
`UIText_Flag` and remains unnamed.
|
||||||
|
- This pass did not reconstruct every `ICIDM` input action name, only the exact
|
||||||
|
routing and behavior needed to separate display/selectable/editable text.
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
# Retail AC Client GUI — Master Synthesis
|
# Retail AC Client GUI — Master Synthesis
|
||||||
|
|
||||||
|
> **Superseded timer interpretation (2026-07-10):** references below to a
|
||||||
|
> generic Device timer queue or `RegisterTimerEvent` are retained as research
|
||||||
|
> history, not an implementation oracle. Named-retail bodies instead prove
|
||||||
|
> tooltip-deadline polling followed by global UI time message `3`; see
|
||||||
|
> `docs/research/2026-07-10-retained-widget-foundations-pseudocode.md`.
|
||||||
|
|
||||||
> **Scope note (2026-04-24, updated 2026-04-25):** This document
|
> **Scope note (2026-04-24, updated 2026-04-25):** This document
|
||||||
> describes retail's Keystone UI toolkit — it is the research foundation
|
> describes retail's Keystone UI toolkit — it is the research foundation
|
||||||
> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a
|
> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# Retail AC Client — UI Input Routing and Event System
|
# Retail AC Client — UI Input Routing and Event System
|
||||||
|
|
||||||
|
> **Superseded timer interpretation (2026-07-10):** keep this document as
|
||||||
|
> historical research, but do not implement its inferred generic
|
||||||
|
> `Device::RegisterTimerEvent` queue. The named-retail bodies show
|
||||||
|
> `UIElementManager::UseTime` polling the hover tooltip deadline and then
|
||||||
|
> broadcasting global message `3`. The corrected oracle is
|
||||||
|
> `docs/research/2026-07-10-retained-widget-foundations-pseudocode.md`.
|
||||||
|
|
||||||
**Scope:** How Win32 WndProc, mouse, keyboard, and drag-drop are routed
|
**Scope:** How Win32 WndProc, mouse, keyboard, and drag-drop are routed
|
||||||
through the retail AC client's UI tree, down to individual widgets, and
|
through the retail AC client's UI tree, down to individual widgets, and
|
||||||
how modality, focus, hover, and hotkeys are implemented.
|
how modality, focus, hover, and hotkeys are implemented.
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ when the named decomp lacks a body.
|
||||||
already contains the retail node. Runtime-created nodes are allowed only when
|
already contains the retail node. Runtime-created nodes are allowed only when
|
||||||
retail creates them dynamically and the code cites that method.
|
retail creates them dynamically and the code cites that method.
|
||||||
6. **Typed behavior, not stringly behavior.** State names remain DAT data, but C#
|
6. **Typed behavior, not stringly behavior.** State names remain DAT data, but C#
|
||||||
policy uses enums/records (`UiTextInteractionMode`, button phase, cursor role,
|
policy uses typed capabilities/records (independent text `Editable` and
|
||||||
shortcut kind, window event). Controllers must not implement precedence by
|
`Selectable` flags, button phase, cursor role, shortcut kind, window event).
|
||||||
scattered state-name string lists.
|
Controllers must not implement precedence by scattered state-name string lists.
|
||||||
7. **Controllers have lifetimes.** Every subscribing controller implements
|
7. **Controllers have lifetimes.** Every subscribing controller implements
|
||||||
`IDisposable`; subscriptions use removable named handlers or a small
|
`IDisposable`; subscriptions use removable named handlers or a small
|
||||||
`SubscriptionBag`. `RetailUiRuntime.Dispose` tears down the complete tree.
|
`SubscriptionBag`. `RetailUiRuntime.Dispose` tears down the complete tree.
|
||||||
|
|
@ -106,32 +106,32 @@ This wave changes no runtime behavior.
|
||||||
|
|
||||||
### 0.1 Consolidate missing retail pseudocode
|
### 0.1 Consolidate missing retail pseudocode
|
||||||
|
|
||||||
- [ ] Write focused pseudocode notes for:
|
- [x] Write focused pseudocode notes for:
|
||||||
- `UIElement_Text` interaction/property modes;
|
- `UIElement_Text` interaction/property modes;
|
||||||
- `UIElement_Button::UpdateState_`, mouse transitions, enabled/toggle behavior;
|
- `UIElement_Button::UpdateState_`, mouse transitions, enabled/toggle behavior;
|
||||||
- `UIElement::UpdateForParentSizeChange` raw edge modes 0-4;
|
- `UIElement::UpdateForParentSizeChange` raw edge modes 0-4;
|
||||||
- Device timer delivery and focus notifications;
|
- UI manager tooltip deadlines, global time delivery, and focus notifications;
|
||||||
- `gmToolbarUI` global messages, drop branches, mana/stack behavior;
|
- `gmToolbarUI` global messages, drop branches, mana/stack behavior;
|
||||||
- `ItemHolder::UseObject` / `AttemptPlaceIn3D` decision trees;
|
- `ItemHolder::UseObject` / `AttemptPlaceIn3D` decision trees;
|
||||||
- chat maximize, alpha, tabs, talk-focus and squelch;
|
- chat maximize, alpha, tabs, talk-focus and squelch;
|
||||||
- paperdoll priority, Aetheria, hit testing and race updates;
|
- paperdoll priority, Aetheria, hit testing and race updates;
|
||||||
- character titles, luminance, and raise acknowledgement.
|
- character titles, luminance, and raise acknowledgement.
|
||||||
- [ ] Correct the roadmap premise around shortcuts: `ShortCutData` is
|
- [x] Correct the roadmap premise around shortcuts: `ShortCutData` is
|
||||||
`index_`, `objectID_`, `spellID_`, while `PlayerModule::favorite_spells_[8]`
|
`index_`, `objectID_`, `spellID_`, while `PlayerModule::favorite_spells_[8]`
|
||||||
is a separate subsystem. This retail toolbar restores only nonzero
|
is a separate subsystem. This retail toolbar restores only nonzero
|
||||||
`objectID_`; it must not grow invented spell-slot behavior.
|
`objectID_`; it must not grow invented spell-slot behavior.
|
||||||
- [ ] Pin every method address and DAT property id before implementation.
|
- [x] Pin every method address and DAT property id before implementation.
|
||||||
|
|
||||||
### 0.2 Expand committed DAT conformance fixtures
|
### 0.2 Expand committed DAT conformance fixtures
|
||||||
|
|
||||||
- [ ] Add skip-by-default generators and committed `ElementInfo` JSON fixtures for
|
- [x] Add skip-by-default generators and committed `ElementInfo` JSON fixtures for
|
||||||
toolbar `0x21000016`, inventory `0x21000023` plus mounted sublayouts,
|
toolbar `0x21000016`, inventory `0x21000023` plus mounted sublayouts,
|
||||||
paperdoll `0x21000024`, and character `0x2100002E`.
|
paperdoll `0x21000024`, and character `0x2100002E`.
|
||||||
- [ ] Include state media, cursor media, font ids, raw edge modes, default state,
|
- [x] Include state media, cursor media, font ids, raw edge modes, default state,
|
||||||
min/max attributes, numeric state ids, and runtime-relevant scalar/property arrays.
|
min/max attributes, numeric state ids, and runtime-relevant scalar/property arrays.
|
||||||
- [ ] Add inheritance cases proving derived explicit `false` and `0` override base
|
- [x] Add inheritance cases proving derived explicit `false` and `0` override base
|
||||||
values rather than being mistaken for absence.
|
values rather than being mistaken for absence.
|
||||||
- [ ] Add fixture assertions for every required controller role.
|
- [x] Add fixture assertions for every required controller role.
|
||||||
|
|
||||||
### 0.3 Repair the audit trail
|
### 0.3 Repair the audit trail
|
||||||
|
|
||||||
|
|
@ -148,34 +148,35 @@ This wave changes no runtime behavior.
|
||||||
|
|
||||||
## 4. Wave 1 -- exact retained-widget foundation
|
## 4. Wave 1 -- exact retained-widget foundation
|
||||||
|
|
||||||
### 1.1 Give Type-12 text an explicit interaction mode
|
### 1.1 Give Type-12 text independent retail capabilities
|
||||||
|
|
||||||
- [ ] Add `UiTextInteractionMode { Display, Selectable, Editable }`; default to
|
- [x] Default factory-imported DAT text to display-only behavior. Model Editable
|
||||||
`Display` for factory-imported DAT text.
|
`0x16` and Selectable `0x27` as independent capabilities, matching retail; do
|
||||||
- [ ] Import retail Editable `0x16`, OneLine `0x20`, and Selectable `0x27` properties
|
not collapse them into an exclusive interaction-mode enum.
|
||||||
|
- [x] Import retail Editable `0x16`, OneLine `0x20`, and Selectable `0x27` properties
|
||||||
through the typed property bag where the DAT supplies them.
|
through the typed property bag where the DAT supplies them.
|
||||||
- [ ] Make focus, key capture, selection, clipboard, and pointer-drag behavior derive
|
- [x] Make focus, key capture, selection, clipboard, and pointer-drag behavior derive
|
||||||
exclusively from this mode.
|
exclusively from those capabilities.
|
||||||
- [ ] Set the chat transcript to `Selectable`; keep the chat input explicitly
|
- [x] Set the chat transcript to `Selectable`; keep the chat input explicitly
|
||||||
`Editable`; all labels remain `Display`.
|
`Editable`; all labels remain `Display`.
|
||||||
- [ ] Bind the imported vitals labels `0x100000EB/ED/EF` directly and delete the
|
- [x] Bind the imported vitals labels `0x100000EB/ED/EF` directly and delete the
|
||||||
synthesized duplicates.
|
synthesized duplicates.
|
||||||
- [ ] Add tests proving display labels cannot steal focus, capture, or window drag.
|
- [x] Add tests proving display labels cannot steal focus, capture, or window drag.
|
||||||
|
|
||||||
**Oracle:** `UIElement_Text::OnSetAttribute @ 0x0046A640`, mouse/key methods
|
**Oracle:** `UIElement_Text::OnSetAttribute @ 0x0046A640`, mouse/key methods
|
||||||
`0x00469370..0x0046A4A0`, `gmVitalsUI::PostInit @ 0x004BFCE0`.
|
`0x00469370..0x0046A4A0`, `gmVitalsUI::PostInit @ 0x004BFCE0`.
|
||||||
|
|
||||||
### 1.2 Port the button state machine
|
### 1.2 Port the button state machine
|
||||||
|
|
||||||
- [ ] Extract a GL-free `UiButtonStateMachine` over enabled/hovered/pressed/
|
- [x] Extract a GL-free `UiButtonStateMachine` over enabled/hovered/pressed/
|
||||||
captured/toggled state and available DAT states.
|
captured/toggled state and available DAT states.
|
||||||
- [ ] Port retail transition precedence exactly; controller code supplies semantic
|
- [x] Port retail transition precedence exactly; controller code supplies semantic
|
||||||
click/toggle callbacks only.
|
click/toggle callbacks only.
|
||||||
- [ ] Preserve retail state ids `1/2/3`, selected `6/7/8`, and ghosted `13`; if the
|
- [x] Preserve retail state ids `1/2/3`, selected `6/7/8`, and ghosted `13`; if the
|
||||||
requested state is absent, retain the current custom semantic state rather than
|
requested state is absent, retain the current custom semantic state rather than
|
||||||
overwriting states such as `Maximized`, `Minimized`, or radar lock.
|
overwriting states such as `Maximized`, `Minimized`, or radar lock.
|
||||||
- [ ] Make cursor media follow the active button state.
|
- [x] Make cursor media follow the active button state.
|
||||||
- [ ] Cover normal, rollover, pressed, released-outside, disabled, toggle-on/off,
|
- [x] Cover normal, rollover, pressed, released-outside, disabled, toggle-on/off,
|
||||||
and capture-loss tests using real fixture state names.
|
and capture-loss tests using real fixture state names.
|
||||||
|
|
||||||
**Oracle:** `UIElement_Button::UpdateState_ @ 0x00471CF0`, `MouseDown
|
**Oracle:** `UIElement_Button::UpdateState_ @ 0x00471CF0`, `MouseDown
|
||||||
|
|
@ -183,33 +184,35 @@ This wave changes no runtime behavior.
|
||||||
|
|
||||||
### 1.3 Replace lossy anchors with the retail layout policy
|
### 1.3 Replace lossy anchors with the retail layout policy
|
||||||
|
|
||||||
- [ ] Introduce `UiLayoutPolicy` containing the five raw edge modes `0..4`, original child
|
- [x] Introduce `UiLayoutPolicy` containing the five raw edge modes `0..4`, original child
|
||||||
rect, and original parent rect. Preserve raw modes through `ElementReader`.
|
rect, and original parent rect. Preserve raw modes through `ElementReader`.
|
||||||
- [ ] Port `UpdateForParentSizeChange` as a pure function, including right-track,
|
- [x] Port `UpdateForParentSizeChange` as a pure function, including right-track,
|
||||||
center mode `3`, proportional mode `4`, and inclusive-pixel behavior for all four
|
center mode `3`, proportional mode `4`, and inclusive-pixel behavior for all four
|
||||||
edges.
|
edges.
|
||||||
- [ ] Keep `AnchorEdges` only as a compatibility adapter for programmatic widgets;
|
- [x] Keep `AnchorEdges` only as a compatibility adapter for programmatic widgets;
|
||||||
imported DAT elements use `UiLayoutPolicy` directly.
|
imported DAT elements use `UiLayoutPolicy` directly.
|
||||||
- [ ] Test every raw mode against hand-translated decomp vectors at 800x600,
|
- [x] Test every raw mode against hand-translated decomp vectors at 800x600,
|
||||||
widescreen, parent grow/shrink, and nested layouts.
|
widescreen, parent grow/shrink, and nested layouts.
|
||||||
|
|
||||||
**Oracle:** `UIElement::UpdateForParentSizeChange @ 0x00462640`.
|
**Oracle:** `UIElement::UpdateForParentSizeChange @ 0x00462640`.
|
||||||
|
|
||||||
### 1.4 Finish Device-level timing/focus behavior
|
### 1.4 Finish UI-manager timing/focus behavior
|
||||||
|
|
||||||
- [ ] Drain registered timers from `UiRoot.Tick` in deterministic due-time/order.
|
- [x] Poll the hovered element's tooltip deadline in `UiRoot.Tick`, then broadcast
|
||||||
- [ ] Remove a timer before dispatch for reentrancy safety and cancel subtree timers
|
retail global time message `3` to listeners in deterministic tree order.
|
||||||
when a window/widget is removed so stale callbacks cannot fire.
|
- [x] Tear down focus, capture, modal ownership, hover, drag, default input, and
|
||||||
- [ ] Add typed focus/capture/window events; eliminate polling where retail reacts to
|
window registration before a removed subtree can receive another time/input pulse.
|
||||||
|
- [x] Add typed focus/capture transition behavior; eliminate polling where retail reacts to
|
||||||
a transition.
|
a transition.
|
||||||
- [ ] Pin double-click, tooltip, capture-loss, modal, and timer ordering tests.
|
- [x] Pin tooltip-before-global-time, capture-loss, modal, and subtree-removal
|
||||||
|
ordering tests. Keep double-click coverage with the existing input suite.
|
||||||
|
|
||||||
### 1.5 Add a narrow DAT named-state bridge
|
### 1.5 Add a narrow DAT named-state bridge
|
||||||
|
|
||||||
- [ ] Bind imported named states such as `ShowDetail`, `HideDetail`, enabled/
|
- [x] Bind imported named states such as `ShowDetail`, `HideDetail`, enabled/
|
||||||
disabled button roles, and empty-cell art without globally re-registering Type 3
|
disabled button roles, and empty-cell art without globally re-registering Type 3
|
||||||
elements or teaching controllers how the importer stores states.
|
elements or teaching controllers how the importer stores states.
|
||||||
- [ ] Prove state changes preserve the imported rect, anchors, dimensions, and media
|
- [x] Prove state changes preserve the imported rect, anchors, dimensions, and media
|
||||||
using production LayoutDesc fixtures.
|
using production LayoutDesc fixtures.
|
||||||
|
|
||||||
**Gate:** vitals/chat/radar/toolbar UI Studio fixtures unchanged visually; buttons
|
**Gate:** vitals/chat/radar/toolbar UI Studio fixtures unchanged visually; buttons
|
||||||
|
|
@ -415,14 +418,19 @@ mana selections; full suite and user visual gate.
|
||||||
bottom pinning, and restore behavior on the **outer frame**.
|
bottom pinning, and restore behavior on the **outer frame**.
|
||||||
- [ ] Drive alpha from focus/lifecycle events and retail timing, not a fixed `0.75`.
|
- [ ] Drive alpha from focus/lifecycle events and retail timing, not a fixed `0.75`.
|
||||||
|
|
||||||
### 5.4 Implement tabs and filters
|
### 5.4 Confirm the retail chat surface, then port its filters
|
||||||
|
|
||||||
|
The named `gmMainChatUI` build proves independently filtered chat/floaty windows;
|
||||||
|
it does **not** prove that DAT elements `0x10000522..0x10000525` are an in-window
|
||||||
|
numbered-tab model. Do not implement tab switching from the older inference.
|
||||||
|
|
||||||
|
- [ ] Confirm the role of `0x10000522..0x10000525` from live retail behavior or a
|
||||||
|
stronger DAT/call-site trace before assigning tab semantics.
|
||||||
- [ ] Add a Core/VM chat-line classification carrying retail message type/channel.
|
- [ ] Add a Core/VM chat-line classification carrying retail message type/channel.
|
||||||
- [ ] Implement numbered tab models, active tab, display masks, unread state and
|
- [ ] Port the proven per-window display masks, unread state, and persistence only
|
||||||
retail-backed persistence if the PlayerModule/options payload proves it. If the
|
where the PlayerModule/options payload proves it; otherwise register the absence.
|
||||||
masks are not persisted by this build, register that fact instead of inventing a
|
- [ ] Route every retained/floating transcript through its filter without
|
||||||
per-character format.
|
duplicating `ChatLog`.
|
||||||
- [ ] Route each transcript through its active filter without duplicating ChatLog.
|
|
||||||
|
|
||||||
### 5.5 Complete talk-focus and moderation actions
|
### 5.5 Complete talk-focus and moderation actions
|
||||||
|
|
||||||
|
|
@ -447,10 +455,12 @@ visual gate.
|
||||||
|
|
||||||
## 9. Wave 6 -- inventory completion
|
## 9. Wave 6 -- inventory completion
|
||||||
|
|
||||||
- [ ] On inventory `Hidden/Closed`, have the controller send
|
- [ ] Split the owned inventory window from the external/ground-container window.
|
||||||
`NoLongerViewingContents` exactly once for the retail-eligible side/external
|
The proven `ClientUISystem.groundObject` lifecycle sends
|
||||||
container, clear the viewed-container and panel drag state, and restore main-pack
|
`NoLongerViewingContents` exactly once when an external view is replaced or
|
||||||
presentation. Main-pack close sends nothing; repeated hiding is idempotent.
|
closed. Owned side-bag navigation/closing sends nothing unless a live packet
|
||||||
|
trace proves otherwise. Both lifecycles clear their own panel drag/view state;
|
||||||
|
repeated hide is idempotent.
|
||||||
- [ ] Route grid/bag clicks through target mode and global `SelectionState`.
|
- [ ] Route grid/bag clicks through target mode and global `SelectionState`.
|
||||||
- [ ] Retire capacity/burden fallbacks only after wire captures prove
|
- [ ] Retire capacity/burden fallbacks only after wire captures prove
|
||||||
`ContainersCapacity`, `ItemsCapacity`, `EncumbranceVal`, and augmentation delivery.
|
`ContainersCapacity`, `ItemsCapacity`, `EncumbranceVal`, and augmentation delivery.
|
||||||
|
|
@ -516,12 +526,14 @@ user visual gate.
|
||||||
|
|
||||||
### 8.3 Reconcile stat raises
|
### 8.3 Reconcile stat raises
|
||||||
|
|
||||||
- [ ] Trace whether retail waits for authoritative state or predicts a raise before
|
- [ ] Remove `ApplyLocalRaise`: named retail sends one request, ghosts the clicked
|
||||||
choosing the implementation. If it waits, remove optimistic mutation. If it
|
button, and leaves XP/credits/ranks unchanged until the authoritative quality
|
||||||
predicts, add per-target pending snapshots for XP, credits, ranks, and advancement
|
update arrives.
|
||||||
class, then confirm/rollback on the exact retail/ACE paths.
|
- [ ] Permit only one raise request in flight; clear the awaiting/ghosted state on
|
||||||
- [ ] Test multiple in-flight raises, reordered echoes, rejection, caps, and relog/
|
the matching authoritative quality-change path. Pin the exact rejection cleanup
|
||||||
disconnect cleanup without retry loops.
|
with a live ACE trace before adding any extra timeout or recovery behavior.
|
||||||
|
- [ ] Test one-in-flight enforcement, authoritative acknowledgement, rejection,
|
||||||
|
caps, and relog/disconnect cleanup without optimistic mutation or retry loops.
|
||||||
|
|
||||||
**Oracles:** `gmStatManagementUI`, `gmAttributeUI`, `gmSkillUI`,
|
**Oracles:** `gmStatManagementUI`, `gmAttributeUI`, `gmSkillUI`,
|
||||||
`gmCharacterTitleUI @ 0x0049A610`, `UpdateExperience @ 0x004F0A70`.
|
`gmCharacterTitleUI @ 0x0049A610`, `UpdateExperience @ 0x004F0A70`.
|
||||||
|
|
@ -584,7 +596,7 @@ scenario, UI Studio fixture, live user visual gate, divergence cleanup.
|
||||||
| Imported display text steals focus/drag; duplicate vitals labels | 1 |
|
| Imported display text steals focus/drag; duplicate vitals labels | 1 |
|
||||||
| Buttons lack automatic retail states | 1 |
|
| Buttons lack automatic retail states | 1 |
|
||||||
| Edge mode 3 recenter and mode 4 policy lost | 1 |
|
| Edge mode 3 recenter and mode 4 policy lost | 1 |
|
||||||
| Device timers never fire | 1 |
|
| Tooltip/global UI time ordering and removed-subtree cleanup | 1 |
|
||||||
| Chat maximize changes content, not outer frame | 2 / 5 |
|
| Chat maximize changes content, not outer frame | 2 / 5 |
|
||||||
| Only radar persists position; windows lack lifecycle | 2 |
|
| Only radar persists position; windows lack lifecycle | 2 |
|
||||||
| Controller subscriptions are not disposed | 2 |
|
| Controller subscriptions are not disposed | 2 |
|
||||||
|
|
@ -599,14 +611,14 @@ scenario, UI Studio fixture, live user visual gate, divergence cleanup.
|
||||||
| Mana/stack selected-object behavior missing | 4 |
|
| Mana/stack selected-object behavior missing | 4 |
|
||||||
| Vitals detail state missing | 5 |
|
| Vitals detail state missing | 5 |
|
||||||
| Duplicate ChatVM lacks `/loc` and `/framerate` providers | 5 |
|
| Duplicate ChatVM lacks `/loc` and `/framerate` providers | 5 |
|
||||||
| Chat tabs, opacity, channel availability, squelch/actions incomplete | 5 |
|
| Chat filter windows, opacity, channel availability, squelch/actions incomplete; numbered-tab premise unproven | 5 |
|
||||||
| Chat scrollbar arrows reversed | 2 / 5 |
|
| Chat scrollbar arrows reversed | 2 / 5 |
|
||||||
| Inventory close skips `NoLongerViewingContents` | 2 / 6 |
|
| External-container close lifecycle missing; owned-side-bag `0x0195` premise disproved | 2 / 6 |
|
||||||
| Inventory capacity/burden/drag residuals | 6 |
|
| Inventory capacity/burden/drag residuals | 6 |
|
||||||
| Paperdoll Aetheria, priority, legality, dual wield, body interactions | 7 |
|
| Paperdoll Aetheria, priority, legality, dual wield, body interactions | 7 |
|
||||||
| Paperdoll empty art and per-race behavior | 7 |
|
| Paperdoll empty art and per-race behavior | 7 |
|
||||||
| Character Titles inert, live title/luminance missing | 8 |
|
| Character Titles inert, live title/luminance missing | 8 |
|
||||||
| Character raise authoritative/prediction semantics are unreconciled | 8 |
|
| Character raises mutate optimistically instead of using proven server-authoritative flow | 8 |
|
||||||
| Radar fellowship/allegiance state missing | 9 |
|
| Radar fellowship/allegiance state missing | 9 |
|
||||||
| Combat/powerbar/spellbook/effects/social residuals/floating chat/quests/map/vitae/options/smartbox/vendor/trade/salvage/tinkering/world HUD absent | 10 |
|
| Combat/powerbar/spellbook/effects/social residuals/floating chat/quests/map/vitae/options/smartbox/vendor/trade/salvage/tinkering/world HUD absent | 10 |
|
||||||
| Stale/missing/colliding divergence rows | 0, then every wave |
|
| Stale/missing/colliding divergence rows | 0, then every wave |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# D.2b — Chat-window re-drive (LayoutDesc importer, Plan 2 chat piece) — design
|
# D.2b — Chat-window re-drive (LayoutDesc importer, Plan 2 chat piece) — design
|
||||||
|
|
||||||
|
> **Oracle correction (2026-07-10):** this historical design inferred that DAT
|
||||||
|
> elements `0x10000522..0x10000525` are numbered in-window chat tabs. The named
|
||||||
|
> `gmMainChatUI` call graph proves separate filtered chat/floaty windows but does
|
||||||
|
> not prove that tab model. Do not implement the inferred tab behavior without
|
||||||
|
> live-retail or stronger DAT/call-site confirmation. See
|
||||||
|
> `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md`.
|
||||||
|
|
||||||
**Date:** 2026-06-15
|
**Date:** 2026-06-15
|
||||||
**Branch:** `claude/hopeful-maxwell-214a12` (D.2b retail-UI track; lighting/M1.5 is a separate branch off main)
|
**Branch:** `claude/hopeful-maxwell-214a12` (D.2b retail-UI track; lighting/M1.5 is a separate branch off main)
|
||||||
**Status:** design — approved scope, pending spec review
|
**Status:** design — approved scope, pending spec review
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# D.2b inventory container-switching — design
|
# D.2b inventory container-switching — design
|
||||||
|
|
||||||
|
> **Oracle correction (2026-07-10):** this historical design assigned
|
||||||
|
> `NoLongerViewingContents 0x0195` to owned side-bag navigation. Named retail
|
||||||
|
> instead sends it from the external `ClientUISystem.groundObject` lifecycle;
|
||||||
|
> the owned `gmInventoryUI` path does not statically prove such a send. Preserve
|
||||||
|
> this document as implementation history, but do not extend that packet behavior
|
||||||
|
> without a live trace. See
|
||||||
|
> `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md`.
|
||||||
|
|
||||||
**Date:** 2026-06-22
|
**Date:** 2026-06-22
|
||||||
**Branch:** `claude/hopeful-maxwell-214a12`
|
**Branch:** `claude/hopeful-maxwell-214a12`
|
||||||
**Phase:** D.2b retail-UI inventory arc — container-switching (the first inventory feature with a live two-way wire round-trip).
|
**Phase:** D.2b retail-UI inventory arc — container-switching (the first inventory feature with a live two-way wire round-trip).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue