Teardown's dev fly-camera fallback raw-captured the OS cursor on the
character-select screen; teardown now lands on the orbit camera (the
fresh-boot state) and the pointer controller restores a normal cursor.
Live-verified both directions under GetCursorInfo sampling. Files #415
(broken 'wait world-visible' automation verb, apparatus only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session teardown (PlayerModeController.Exit/ResetSession ->
CameraController.ExitChaseMode) fell back to the dev free-fly camera, and
CameraPointerInputController.ApplyCursorForCameraMode faithfully applies
CursorMode.Raw (GLFW disabled cursor: hidden + captured) for fly mode —
so the character-select screen after an in-world logoff had no mouse.
Fresh boot starts in Orbit and never fires a mode change, which is why
only the post-logout path was affected.
Teardown now lands on Mode.Orbit — the exact state a fresh boot presents
at character select — and always notifies, so the pointer controller
restores CursorMode.Normal even when torn down from the dev fly camera.
The dev fly<->chase flow is untouched (it rides ToggleFly, never
ExitChaseMode).
Proven live both directions with a driven logout (UI probe 0x100000FA ->
dialog accept 0x17) under Win32 GetCursorInfo sampling: before, flags
flipped 1->0 exactly at the roster re-push that re-shows character select
and stayed hidden; after, zero hidden samples across the full timeline.
Files #415: the UI-probe 'wait world-visible' verb reads the reset
transit snapshot and is dead after reveal completion (test apparatus
only).
App tests 5564/3 skips (+3), Runtime 1756/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tunnel arms at the Enter click (user-directed improvement over retail's
black CreatePlayer gap; AD-109). The full retail logoff: 0xF653 with the
3s hold (+20s PK), server-driven LogOut motion with input disabled,
the wormhole entered in reverse order (no exit cue), live return to
character select on the kept connection with the pushed roster —
second Enter round-trips (AD-110 filed, AD-74 RETIRED: Options'
Exit-to-Character-Selection is now real). Escape normalization moved to
retail's own placement — StringTableMetaLanguage::UnescapeString ported
at the string source, five consumer patches retired, the 4,365-string
escape population sweep-pinned (AD-111 records the wire-domain
appraisal exception).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retires AD-74 (Exit to Character Selection 'behaves as Exit Game') and
files AD-110 (the composed handoff edge) — register rows in this commit.
Retail derivation (named decomp):
- gmGamePlayUI::UseTime @0x004EA3A0: confirmed Yes drains into
CPlayerSystem::LogOffCharacter(0) when grounded (transient_state &
CONTACT); the grounded three-way branch now also covers the
indicator-bar end-session control (it was Options-only).
- CPlayerSystem::LogOffCharacter @0x00563520: SaveToServer FIRST (the
existing pre-logoff flush hook), then RequestLogOff @0x00562DD0:
'Logging off...' chat (type 0), 0xF653 via Proto_UI::LogOffCharacter
@0x00546A20, logOffRequestTime = now + 3.0 (+20.0 when
IsPlayerKiller @0x0058C910 — PWD bits 0x20|0x2000000), and
CommandInterpreter::HandleLogOff @0x006B3330 -> Disable.
- The log-off ANIMATION is server-driven: ACE broadcasts
MotionCommand.LogOut (0x1000011E, Player.cs:596 SendMotionAsCommands)
and it plays on the local player through the existing inbound
unpack_movement funnel during the 3 s hold — retail plays nothing
locally; Disable() is the whole client-side effect.
- gmSmartBoxUI::UseTime @0x004D6E64: hold elapsed ->
BeginTeleportAnimation(TAS_WORLD_FADE_OUT) @0x004D6E83 (enter cue
@0x004D638E, unconditional) -> TunnelFadeIn -> Tunnel. The tunnel
plays the SAME forward 40 fps animation; nothing renders backwards,
and NO exit cue ever fires on logout (the char-select swap preempts
the TunnelContinue/FadeOut tail).
- Inbound 0xF653 echo (dispatch case 3 @0x0055C963) ->
ExecuteLogOff @0x0055D780: world teardown with the LOGON CONNECTION
KEPT (ExitWorldDisconnect @0x00541E00 removes every connection
except logonRecID_ — one connection against ACE) and
Proto_UI::SetEventCounter(0) @0x00541E79; the fresh CharacterList in
the same batch re-shows character management (gmGamePlayUI::Update
@0x004E9CD0 -> QueueUIMode(0x1000000a)). ACE mirrors it:
SendFinalLogOffMessages (Session.cs:249) sends 0xF653 + CharacterList
+ ServerName >=6 s after the request and leaves the session
AuthConnected — a second EnterWorld needs no re-handshake.
Implementation:
- RuntimeWorldTransitState: the canonical logout lifecycle
(Requested/PresentationActive/Confirmed, retail 3 s/+20 s holds,
cancel/reset/ownership convergence).
- WorldSession: RequestCharacterLogOff (non-blocking 0xF653),
IsCharacterLogOffConfirmed, ReturnToCharacterSelect (InWorld ->
InCharacterSelect + game-action sequence reset; transport untouched).
- LiveSessionController: BeginCharacterLogOff (flush-first request) and
CompleteCharacterLogOff — the return-to-selection transaction
(ReconnectCore minus the transport swap: retire the world
generation's routes, host reset, state flip, fresh generation
re-bind, roster re-applied from the pushed CharacterList; failures
degrade to the full StopCore teardown).
- RuntimeLocalPlayerMovementState.DisableCommandInterpreter +
DispatcherMovementInputSource gate: retail's Disable() — held keys
produce no movement while the server LogOut motion plays; cleared by
the generation reset.
- LocalPlayerTeleportController: the logout pump as the third arm of
the one wormhole machine (request/hold/wormhole/confirmed handoff;
teleport starts refused during logout; the handoff runs the session
transaction whose world reset retires the tunnel as the fresh
selection state re-shows the character screen).
- UI: both end-session surfaces share the retail three-way grounded
gate and now run the REAL flow; Options' Exit Game keeps the app
exit (window close -> the existing graceful-shutdown logoff).
Tests: +5 transit lifecycle, +4 session transaction, +7 logout pump.
Runtime 1756/0 (baseline 1747), App live-DAT 5523/3 (baseline 5512/3
+ 11 this round), Core.Net 1004/0, full solution green (0 failures).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
USER-DIRECTED deviation from retail (register row AD-109, same commit):
retail presents the empty pre-player gameplay screen — black behind the
retained UI — from the Enter click (CPlayerSystem::LogOnCharacter
@0x0055F890 -> CM_Login::SendNotice_BeginEnterWorld @0x006AD810, UI mode
0x10000008) until CreatePlayer raises SmartBox::teleport_in_progress
@0x00451C20 and gmSmartBoxUI::UseTime @0x004D6EAB begins TAS_TUNNEL. The
user prefers the tunnel to cover that whole wait.
- ILocalPlayerTeleportNetworkSink.ArmLoginTunnel: begins the login
wormhole presentation at the Enter click, consuming the sequencer's
begin-edge events SYNCHRONOUSLY (the Enter command blocks the update
thread for the whole ServerReady round trip, so a deferred first tick
would leave exactly the black window this deviation removes). The
enter cue plays at the click: retail's own rule is cue-at-animation-
begin (Sound_UI_EnterPortal @0x004D638E, unconditional inside
BeginTeleportAnimation), and the animation begin moved to the click.
- Armed pre-reveal pump: tunnel animates across the round trip
(worldReady pinned false, sequencer holds in Tunnel); the hold clock
accumulates from the click.
- Adoption: the Runtime login reveal ADOPTS the running presentation
(no re-Begin, no second cue); rejected EnterWorld (lifecycle back to
AwaitingSelection) disarms and retires the tunnel.
- Wired at the ONE host edge every entry route shares:
ILiveSessionLifecycleHost.ApplySelectedCharacter (direct connect,
roster Enter, enter-after-create) via
LiveSessionSelectionBindings.ArmLoginTunnel (default no-op keeps
headless and every existing construction site unchanged).
- ILocalPlayerLoginLifecycleSource: typed seam (not a stored delegate —
the frame-phase owner delegate-field guard) projecting the Runtime
character-selection lifecycle for the disarm edge.
- Frame contract update: [login-frames] over a login is tunnel -> world
from the click — no void, and no black between click and world.
Tests: 4 new armed-tunnel tests (arm/adopt/disarm/frame-shape); App
suite live-DAT 5516 passed / 3 skipped (baseline 5512/3 + 4 new).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found during the systemic escape-normalization round (967b9c57): retail's
ItemExamineUI::AddItemInfo @ 0x004AC050 appends wire strings straight to
UIElement_Text::AppendTextWithFont with no unescape — the escape decode
belongs exclusively to StringInfo resolution, which that round ported to
DatStringResolver/RetailStringEscapes. ItemAppraisalTextLayout.Shape's
literal-"\n"-to-line-break replace on server fragment text is therefore a
deviation, pre-existing inside the user-accepted assessment surface, now
filed instead of living only in a code comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exit-world confirmation (ID_Client_EndCharacterSessionConfirm, table
0x23000001 key 0x0EB1C41D) rendered its literal two-character "\n" escapes
because escape decoding lived in individual consumers — Batch E centralized
it for authored captions only (DatWidgetFactory.ResolveAuthoredString), and
each new string surface had to remember its own copy. The installed DAT
carries the escape in 4,365 of 7,050 strings; per-consumer normalization
was structurally guaranteed to keep leaking.
Retail's placement is the SOURCE, not the widget: every public StringInfo
resolution ends in StringTableMetaLanguage::UnescapeString @ 0x0067BDC0
(StringInfo::InqString @ 0x0042E490, GetLiteralValue @ 0x0042CA50), the
write side escapes (SetLiteralValue @ 0x0042C980; AddVariable_String
@ 0x0042E6C0 for template variables), and widgets receive decoded text.
Ported exactly:
- NEW RetailStringEscapes: UnescapeString/EscapeString + the
GetUnEscapedChar @ 0x0067B750 / GetEscapedChar @ 0x0067B6C0 tables
(\n \t \r \q + the ten metalanguage self-escapes []!{}#\|^$,
byte-verified against the PDB-paired 2013 binary at 0x3FE178;
unrecognized pairs stay verbatim).
- DatStringResolver.Resolve/ResolveAll unescape at the source;
ResolveTemplate escapes each variable on insert and unescapes the
composed whole — retail's round trip, so variable content (player
names) can never be corrupted by the final decode.
- RETIRED the consumer copies (double paths would corrupt an authored
"\n" into a line break): DatWidgetFactory.NormalizeEscapes + BuildText's
inline replace, RetailUiRuntime.NormalizeRetailNewlines + the
OpenCaptureInstructions inline replace, DatRichText.Compose's replace,
IndicatorDetailText.Shape's replace. ItemAppraisalTextLayout's replace
stays — WIRE-domain (server strings never pass the DAT source; retail's
ItemExamineUI::AddItemInfo @ 0x004AC050 appends wire text verbatim), now
documented as such.
- Consumer CR-strips retired with them: the installed DATs contain ZERO
real CR characters (sweep-measured) and UiText.WrapWords already drops
strays.
Tests: RetailStringEscapes conformance (escape set, unknown pairs,
round trip), DatStringResolver source-decode pins (including the exact
user-reported exit-world text shape and a backslash-carrying variable),
the installed-DAT escape sweep (7,050 strings; every resolution must equal
the retail unescape of the raw entry; inventory printed), and the existing
caption/rich-text/live-DAT pins relocated to the source contract.
App 5550/3 (live-DAT), Runtime 1747/0, complete Release solution green
across all suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The login tunnel now covers from the first world-facing frame (the
sky-void backdrop can never present pre-tunnel) and holds through an
atomic tunnel-to-world swap at reveal completion — the void is
structurally unreachable on both edges, pinned by frame-sequence tests
across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport-
Controller/RuntimeWorldTransitState. Vitals detail icons draw at their
authored centered offsets in both stacked and side-by-side layouts.
Implemented and live-probed by the fix agent; finalized by the lead
after the agent parked post-verification (gates re-run green:
App 5512/3, Runtime 1747/0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vitals: click toggles numeric/graphical per gmVitalsUI's exact state
machine (HideDetail/ShowDetail cascade, authored icon pairs with clipped
fill, first-click no-op retail quirk); Side By Side Vitals as retail's
two-window visibility swap on CharacterOptions1 bit 0x00200000, live on
option change, blob-persisted. Login portal-space: every world entry
(direct, char-select Enter, enter-after-create) now runs retail's
wormhole with Sound_UI_EnterPortal via the teleport_in_progress flag
edge; LoginComplete moved to retail's fade-in-end timing; TS-28
narrowed; a latent pre-publication portal-space crash fixed; the UI
probe's canvas-vs-window coordinate bug fixed. All live-verified on
both accounts with screenshots + cue evidence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vitals round's connected verify needed to click ONE specific
Character-tab option row, but every toggle row is a template instance
sharing the same dat element ids (0x10000218/0x10000219), so
`click element` (first-match by dat id) cannot address a row. The drive
script now reads the row's rect from its own `dump` line and clicks its
center — same synthetic UiRoot press/release route as ClickElement, never
the OS cursor (the same no-real-input constraint the morning gate's
hover/mousemove verbs follow).
Used live: the Side-By-Side Vitals checkbox + Apply choreography that
verified db8fa328's swap both directions over a real ACE session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The automation probe fed element-center CANVAS coordinates straight into
UiRoot.OnMouseMove/Down/Up, which take WINDOW coordinates and map
window->canvas internally (UiRoot.MapWindowToCanvas). The two spaces are
identical on every screen without UiRoot.FixedCanvasSize — every prior
probe gate passed — but the character-select/chargen screens stretch an
authored 800x600 canvas across the window, so every synthetic click and
hover landed at canvas*(canvas/window): nowhere near the target. The
enter-world connected gate's 'click element 0x100003A2' silently did
nothing for two full rounds. Element-derived pointer paths (ClickAt,
DragAt, HoverElement) now convert canvas->window via UiRoot.CanvasScale;
raw 'mousemove x y' stays a passthrough.
CharacterManagementUiController.EnterSelected also logs a once-per-click
outcome line ('[UI] character enter accepted/rejected status=...') — a
refused Enter was previously indistinguishable from a click that never
dispatched (both silent), which cost a connected-gate round to tell
apart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail runs the SAME TAS_TUNNEL wormhole at initial login as at an F751
teleport, with no F751 involved: SmartBox::teleport_in_progress
@0x00451C20 returns 1 the moment the login player exists with
position_update_complete == 0, gmSmartBoxUI::UseTime @0x004D6EAB
edge-detects it into BeginTeleportAnimation(TAS_TUNNEL) @0x004D6EC9
(playing Sound_UI_EnterPortal @0x004D638E), SmartBox::UseTime
@0x00455483 ends the hold once destination cells stop blocking,
Sound_UI_ExitPortal plays at the viewport swap @0x004D7405, and
LoginComplete goes out at the WorldFadeIn end @0x004D745D ->
CPlayerSystem::SendLoginCompleteNotification @0x00562E90 (ACE's own
GameActionLoginComplete comment names this contract: 'called when the
client player exits portal space. It includes initial login'). acdream
skipped all of it at login — every entry route (direct auto-select,
character-select Enter, enter-after-create) dropped onto the sky-only
'waiting for login' backdrop until the world reveal completed.
The fix engages the EXISTING F751 presentation machinery on Runtime's
login reveal — no duplicated presentation code, no timers:
- LocalPlayerTeleportController gains a login arm keyed off the
Runtime-owned login reveal generation (RuntimeWorldTransitState
.BeginLoginReveal, begun on the first accepted local-player position
on every entry route). It drives the same TeleportAnimSequencer/
PortalTunnelPresentation lifecycle and the same enter/exit cues; the
Place edge is a no-op at login (the first-entry conductor already
committed the canonical placement — retail's analogue only flips
position_update_complete), and FireLoginComplete now performs
EnterWorld + the single LoginComplete send + reveal Complete, exactly
like the F751 pump. worldReady is latched on BOTH canonical first
placement (OnLocalPlayerFirstEntryCompleted, the repointed
GraphicalSessionEventRoute completion callback that used to send
LoginComplete immediately) AND destination reveal readiness.
ActiveDestinationCell now also reports the login destination so the
render frame's reveal-preparation arm keeps running after portal-space
entry flips ChaseModeEverEntered.
- PlayerModeController.TryEnterPortalSpaceForLogin performs the
player-mode presentation attach (the same BuildControllerAndCamera the
post-reveal auto-entry used to run) before flipping into portal space
— at login no player-mode entry has happened yet. TryEnterPortalSpace
itself now refuses (retryable) on a constructed-but-unpublished
Runtime controller via the documented CanExecuteLiveMovement skip
predicate instead of faulting — the first connected run crashed on
exactly that pre-publication State write.
- HouseQuery stays at first-entry completion (retail: tail-called from
CPlayerSystem::InitializePlayer @0x00563570, an object-arrival edge,
not a tunnel edge).
- An F751 arriving mid-login-tunnel withdraws the login claim and hands
the presentation to the portal pump, which owns the single
LoginComplete — matching retail's one teleportInProgress flag.
TS-28 narrowed: the graphical host now runs the full login wormhole;
the residual is headless-only (no presentation; placement-edge send).
Live gates (testaccount2/+Horan vs local ACE, Release): the
character-select Enter route and the --session-config direct auto-select
route both play the wormhole with Sound_UI_EnterPortal at animation
begin, hold with retail's 'In Portal Space - Please Wait...' notice
until readiness, fade out with the view-plane warp, send LoginComplete
at the WorldFadeIn end, and materialize in Holtburg; ACE-confirmed
graceful logout. Tests: App 5493/3 skips (baseline 5490 + 3 new login
tests), Runtime 1744/0, full solution green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two end-to-end additions to the fixture toggle suite: a body press through
UiRoot.OnMouseDown's actual hit test + bubble (left toggles, right toggles),
and a press on the authored top drag bar (0x1000063C) arming the window
move WITHOUT toggling — the retail Dragbar-consumes-the-press semantics
proven against the real input path, not just injected OnEvent calls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The press toggle returned false directly, silently swallowing the base
class's Click dispatch (OnClick/OnClickAt) for any future controller wiring
on a vitals root. Retail's handler falls through to the base listener the
same way (@0x004BFC47). No behavior change today — nothing sets OnClick on
a vitals root.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bookkeeping for the vitals retail-modes round (306a1670 + db8fa328): the
side-by-side vitals row joins IA-15's production LayoutDesc import list.
No new divergence class — the window shell, layout persistence, and
whole-surface drag regions the two vitals windows ride are already
registered under IA-12/IA-15/AP-98.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of retail's SideBySideVitals character option, derived end-to-end:
retail authors TWO complete vitals windows and swaps their VISIBILITY on
the option bit — nothing is rearranged in place.
- gmFloatySideVitalsUI (0x10000056, Register @0x004D0490) is a second
full vitals window from LayoutDesc 0x21000075: 460x26, the same three
meters (0x100000E6/EC/EE), cur/max labels (0x100000EB/ED/EF), and
detail-icon overlays authored id-for-id with the stacked window — so
the same VitalsController.Bind and the inherited UiVitalsRoot click
toggle apply unchanged. Authored constraints ride the root's
0x3C..0x3F (fixed 26 height, width 360..3000) through
DatConstraintSource.
- Visibility ownership: gmFloatyVitalsUI::UpdateFromPlayerModule
@0x004CF140 shows the stacked window iff PlayerModule::SideBySideVitals
== 0; gmFloatySideVitalsUI::UpdateFromPlayerModule @0x004D0810 shows
the side row iff set; gmGamePlayUI::RecvNotice_PlayerOptionChanged
@0x004E9DA0 flips both live on option id 0x13.
- The bit: PlayerModule::SideBySideVitals @0x005D3070 =
(options_ >> 0x15) & 1 — CharacterOptions1 0x00200000, ACE-confirmed;
CharacterOptionTable already carried the exact row (PlayerModule-blob
group, not a 0x0005 auto-save id).
acdream shape: MountSideVitals mounts the second window hidden;
VitalsSideBySideController polls the borrowed J4 option bit once per
frame from RetailUiRuntime.Tick and applies BOTH windows' visibility on
the edge — covering the mount default, the PlayerModule blob arriving
after mount, and the Character tab's live checkbox with one mechanism.
Both window names join stateManagedVisibilityWindows so the saved layout
never restores a visibility the option owns. The Character tab's
SideBySideVitals row un-dims (StoreOnly → Live) with a real reader —
33 dimmed / 17 live.
4 new controller tests (initial apply both directions, live edge swap
both directions, steady-bit non-reassertion). App suite Release live-DAT
5499 passed / 3 skips; Runtime 1744/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of gmVitalsUI's press toggle, derived end-to-end from the named
retail decomp + the authored DAT data (installed-DAT probe 2026-08-17):
- gmVitalsUI::ListenToElementMessage @0x004BFC00: mouse press (msg 0x1C,
dwParam1 7=left or 0xA=right — the same param pair the spellbook's
select/favorite handler @0x0048C033 disambiguates) flips
SetState(m_state == HideDetail ? ShowDetail : HideDetail). Both floaty
subclasses (gmFloatyVitalsUI 0x1000004D / gmFloatySideVitalsUI
0x10000056) inherit it verbatim.
- UIElement::SetState @0x00464E70 cascades through the authored
PassToChildren chain: root and meters author media-less
HideDetail/ShowDetail StateDescs with PassToChildren=true.
- HideDetail (0x10000006) = the NUMERIC mode: the cur/max labels author
{0x3B:false} (0x3B = invisible; UIElement::OnSetAttribute case 8
@0x00462DAE is SetVisible(value == 0)), the 0x100004A9 overlays author
File=0.
- ShowDetail (0x10000007) = the GRAPHICAL mode: labels author {0x3B:true}
(numbers hidden); each bar shows its authored icon pair — dim back icon
unclipped over the track, bright front icon clipped with the front
container to the fill fraction (UIElement_Meter::DrawChildren
@0x0046FBD0 clips the whole element-id-2 child; m_pcChildImage =
GetChildRecursive(this, 2) @0x0046F7E3). Health heart 0x06007490/91
(18x16 @66,0), stamina sword 0x06007492/93 (85x16 @32,0), mana scepter
0x06007494/95 (100x16 @25,0) — identical authoring in both 0x2100006C
and 0x21000075.
- Initial state is the authored Undef (numbers visible, no icons —
visually HideDetail); retail's first press lands on HideDetail, then
the pair toggles forever. NOT persisted: SaveScreenLayout @0x004EAD50
writes window rects only, and no PlayerModule option is touched — the
mode resets per session, per window.
- Presses on drag bars / resize grips do not toggle: retail's
UIElement_Dragbar @0x0046C850 and UIElement_Resizebar @0x0046B930
consume the press (return 2) before it can bubble to the root.
Implementation: new UiVitalsRoot behavioral widget registered for the
three gmVitals class ids (press handler + state flip over the existing
UiDatElement state machine); UiMeter absorbs the two 0x100004A9 overlays
(ConfigureDetailOverlay + ShowDetail-keyed draw, back unclipped / front
fill-clipped) and forwards the detail states to its absorbed text child;
UiText.ApplyDatState gains the same named-state-only 0x3B honor
UiDatElement already had (the DirectState 0x3B class stays gated — #408).
8 new fixture-driven conformance tests (toggle sequence, right-press,
label cascade, chrome exclusions, per-window independence, overlay
extraction). App suite Release live-DAT: 5495 passed / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The instant world-tooltip path is drag-only (@0x004E5D8E gate) — ordinary
hover stages the name and rides CheckTooltip's mouse-idle dwell; ported.
DisplayBuyPayment emits in BOTH branches — houseless push 0x7ab688 = 'You
do not currently own a house.' (BN vftable-mislabel hid it); the tab now
shows retail's two lines. The map hotspot template authors its own popup
locator (fourth skin 0x10000398, font 0x40000015, P0x50=0 instant) plus a
pure-green highlight frame via PassToChildren state cascade — override
removed, cascade ported (named-states-only P0x3B honor, #408-scoped).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Morning gate live-verify apparatus: 'hover element <datId>' and
'mousemove <x> <y>' drive UiRoot.OnMouseMove synthetically (no click, no
real cursor theft — a user is present at the machine during this round,
unlike the overnight rounds whose drive scripts moved the physical
cursor). Deliberately no probe-clock Advance: hover dwell and the
world-tooltip timing must ride the production frame tick's real
monotonic clock, which keeps running between script commands — an
Advance would stamp the idle timestamp with the probe's tiny private
counter.
(Committed from a re-attached worktree: the round's original worktree
was pruned from git's registry mid-session by an external cleanup; the
branch and all three finding commits were unaffected.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User finding 3 (retail screenshot): hovering a town on the Map tab turns
its marker GREEN and shows the name on a special-font tooltip — clearly
not our generic 0x10000395 popup skin, and we had no hover highlight at
all.
Re-derivation (live-DAT probe + raw ElementDesc dump + surface
byte-decode; MapNoteLiveDatTests pins all of it):
- m_pMap (0x100001EC)'s P0x47/P0x48 = 0x100001F0 @ 0x21000026 are the
note CONSTRUCTION template (AddMapNote @0x004a1bb0's
CreateChildElement args) — that part we had right.
- The TEMPLATE's own DirectState authors the note's tooltip popup
locator P0x47=0x10000398/P0x48=0x21000041 — the FOURTH popup skin,
whose incorporated text child 0x10000396 fonts 0x40000015 where the
other three skins font 0x40000002 (the user's "special font") — plus
P0x50=0.0 (zero per-element tooltip delay: town tooltips fire the
instant the dwell arms; UiRoot already honors it), P0x4B TooltipOn,
and P0x13 RolloverEnabled. Batch C's "the template authors no locator
of its own" claim was WRONG, and BuildTownMarkers' hardcoded
shared-skin override was clobbering the authored values — removed.
- The hover highlight: the template's Normal/Normal_rollover states are
PassToChildren descriptors driving the swallowed highlight child
0x100001F1 (base 0x100002B7@0x21000042 — a four-piece frame all
drawing 0x06004CC9, byte-decoded PURE GREEN A=FF R=00 G=FF B=00) via
per-state P0x3B (Invisible): hidden at rest, green on rollover.
Port:
- UiButton.CascadeStateToChildren — retail UIElement::SetState
@0x00464E70's PassToChildren cascade, keyed off the REQUESTED state id
(properties commit unconditionally; only the sprite draw is art-gated,
the existing #382/AP-222 distinction).
- UiDatElement.TrySetRetailState honors per-state P0x3B for NAMED states
(OnSetAttribute @0x00462d80 case 8: SetVisible(value==0)). The
unnamed-DirectState case is explicitly excluded — honoring it would
un-gate ISSUES #408 (1,083 authored-invisible elements) through
BuildWidget's post-children state reapply; measured breaking the
spell-favorite drag tests before the scoping (note added to #408).
- MapPageController.BuildTownMarkers rebuilds the button-swallowed
highlight child per marker through the AD-108 IconBuilder seam
(Bindings.TemplateInfoResolver, backed by
RowTemplateResolver.ResolveInfo — same cache) and arms it with the
initial Normal cascade.
Register TS-85's Batch C paragraph corrected; RetailTooltipPresenter's
F10 shared-skin remark updated (MapPageController no longer a consumer).
Tests: 3 installed-DAT pins (locator/delay/rollover; per-state P0x3B +
green frame; the four-skin font sweep), UiButton cascade + UiDatElement
P0x3B units, MapHousePanel marker no-clobber + hover-highlight fixture.
App suite 5487 passed / 3 skips (5490 total, +11 over baseline);
Runtime 1744/1744.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User finding 2 (retail screenshot, houseless character): the House tab
shows "You do not currently own a house." ABOVE "You may buy another
house immediately." — ours showed only the second line, and the prior
session had REFUTED the first line outright ("no such string exists
anywhere in the 2013 dump").
Re-derivation: the string exists in the binary at data_7ab688 — it is
gmHouseUI::DisplayBuyPayment @0x004a2b30's HOUSELESS branch. Two
compounding misreads hid it: (a) DisplayBuyPayment was mislabeled
houseless-silent, but its m_pHouseData gate only selects WHICH text
(jne 0x4a2b63) — the ListBox emit (@0x004a2b80 onward,
AddItemFromTemplateList + SetTextWithFont) runs in BOTH branches; and
(b) BN's pseudo-C renders both push-literal operands as spurious
&vftable.RecvNotice_* symbol matches (the TS-85/F3 artifact class), so
text sweeps of the dump find nothing — capstone byte-decode of the
PDB-paired binary resolves houseless @0x004a2b57 push 0x7ab688 =
"You do not currently own a house." and owned @0x004a2b63 push
0x7ab65c = "The purchase price for this dwelling is:\n" (+
HousePaymentList::ComposeText, still #413 item-3 scope). The morning
brief's alternate DAT-string-table hypothesis was checked and is NOT
the mechanism — plain exe string-pool literal.
RuntimeHouseState.Recompute now renders the houseless case as retail's
exact two lines in gmHouseUI::Update's fixed builder order
(DisplayBuyPayment first, DisplayPurchaseTimeText last); the owned case
is unchanged (its DisplayBuyPayment content needs ComposeText, #413
item 3). Class doc + ISSUES #413 corrected honestly — the user's retail
evidence supersedes the earlier refutation. Runtime house tests updated
to pin both lines; 9/9 pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User finding 1 (side-by-side vs retail): our world-object tooltips popped
the instant the found object changed; retail's "lag". The night round's
derivation from RecvNotice_SmartBoxObjectFound @0x004E5AD0 misread the
notice as edge-MOUNTING: its immediate StartTooltipAtMouse @0x004E5DFB is
inside `if (s_pInstance->m_dragElement != 0)` (@0x004E5D8E) — and
m_dragElement is a real, distinct PDB field in acclient.h's
UIElementManager (separate from the m_pTooltipElement family), so the
immediate mount is DRAG-AND-DROP ONLY. The ordinary hover path merely
STAGES the name (SetTooltip @0x004E5D74 + the |=0x20 TooltipOn bit) and
the display rides the SAME UIElementManager::CheckTooltip @0x0045B6E0
mouse-idle dwell as UI tooltips: 250 ms (m_tooltipDelay @0x0045f75d)
since m_lastMouseMoveTime (stamped on EVERY move, MouseMoveHandler
@0x0045e736). Found swaps under an IDLE mouse replace the popup the same
frame (SetTooltip's own text-change teardown @0x004617FF -> ResetTooltip
@0x0045C360 tail-calling CheckTooltip); the 10 s duration expiry
(@0x0045b78a) requires a fresh mouse move before re-arming
(SwitchMouseOver(null) @0x0045b7b2 clears m_pElementLastEntered).
Port: UiRoot gains the unconditional last-mouse-move stamp
(m_lastMouseMoveTime 1:1 — the existing _hoverStartedMs stamps are
deliberately conditional) exposed as MouseIdleMs/NowMs;
RetailTooltipPresenter.UpdateWorldHoverTooltip now stages text at the
notice edge (ShowTooltips gate + name resolve read there, @0x004E5D21/
@0x004E5D3B, empty-name SetTooltip skip @0x004E5D48 included) and mounts
via the CheckTooltip dwell block (no-capture gate @0x0045b715,
m_tooltipEnable via MouseHover @0x0046254C — which the drag-immediate
branch faithfully bypasses). Session reset also forgets the staged text.
Tests: the world-hover fixture section rewritten to the corrected model —
found edge stages but never mounts before the dwell; a continuously
moving mouse never mounts until it rests; idle found-swap replaces
same-frame without stacking; duration auto-hide needs a move + fresh
dwell to remount; drag-in-progress mounts immediately. 38/38 pass.
Register TS-85 and ISSUES item 2 corrected honestly: the "edge-fired
(no dwell)" conclusion is superseded by the user's retail evidence and
the m_dragElement branch read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch A: world tooltips orphaned per hover transition (one-popup invariant,
live-verified 103 mounts/102 removes) + the UiTemplateListBox viewport
baseline-capture race clipping the Config tab. Batch B: cast-button +
character-panel attribute/vitals/skill tooltips (InqSkillFormula recovered
from unlabeled fragments; 34 skills live-verified). Batch C+closers: the
retail Map/House toolbar panel — panel id 16 at slot 0x1000018C, button
0x1000019A, the byte-decoded PlaceMarkerOnMap projection (span-normalized,
north-up), the verbatim 53-town table with hover tooltips, the Dereth
date/time line, coords readout, login-time HouseQuery + RuntimeHouseState
with retail's single houseless sentence. Opus round review F1-F15 fixed
(the marker formula was a BN FPU-elision misread — byte-re-derived;
three 'unrecoverable' strings recovered; AD-107 retired, AD-108/IA-23
filed). Final numeric live verification: ring at exactly the computed
pixel (226,125), Holtburg hover tooltip, House text, graceful logout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two mechanisms, both live-verified (register row AD-108 updated to match):
1. RESOLUTION. The player/house icons (0x100001ED/0x100001EE) are authored
as nested dat children of m_pMap (0x100001EC), itself a Type-1 button
whose UiButton.ConsumesDatChildren swallows them at build. The old
ResolveSwallowedIcon re-imported them standalone via
ImportInfos(hostLayout, iconId) — which returns null on the live DAT:
FindDesc walks the raw top-level Elements table (one entry for
0x2100006E) and never reaches them. Their ElementInfos only materialize
inside the full panel-slot resolve (ImportInfos(0x2100006E, 0x1000018C))
that MountMapHousePanel already imports — the pageInfo Bind already
receives. The fix finds each icon's info under m_pMap's own resolved
info subtree and BUILDS it through the new Bindings.IconBuilder seam
(production: LayoutImporter.Build under the DAT lock — the build half
of RowTemplateResolver's shape). An icon the normal walk DID build is
preferred (FindDescendant first), so a future ConsumesDatChildren
policy change cannot double-build.
2. POSITION. Found by this fix's own F1 live verification: the resolved
ring rendered pinned to m_pMap's top-left. PlaceMarker owns marker
position outright (retail's gmMapUI::Update re-places every tick;
retail's UpdateForParentSizeChange runs only on real parent resize),
but acdream re-runs ApplyAnchor per frame and the icon's compatibility
anchor had captured the authored (0,0) rect while the panel was still
hidden, re-asserting it over PlaceMarker's writes every frame.
PrepareIcon now sets Anchors=None (clearing any imported LayoutPolicy),
the established runtime-positioned-element convention.
Live numeric gate (session character +Acdream, cell 0xF07E003F):
independent computation (gid_to_lcoord -> display (90.8E, 0.5S) ->
byte-decoded PlaceMarkerOnMap formula, 17x16 icon, marker area
(6,8)-(247,258)) predicts local pixel (226,125); the connected client's
UI-tree dump shows the icon at screen (1166,195) under m_pMap (940,70) =
local (226,125) — exact match in both panel-open dumps. Coordinate text
"0.5S,90.8E", Holtburg town-marker tooltip (real-mouse hover), and the
House tab's "You may buy another house immediately." sentence all
confirmed on screen; ACE-confirmed graceful logout.
New pin: MapHousePanelLiveDatMountTests ([InstalledDatFact]) reproduces
the production mount recipe against the installed DATs — the test that
would have caught this at Batch C: pins the cold-import null, the
panel-slot resolution of both icons with non-degenerate extents, AND
that PlaceMarker's writes survive the per-frame ApplyAnchor pass.
Gates: Release build green; App suite (live-DAT mode) 5479/3 skips
(baseline 5478 + the new pin); Runtime 1744/0; full solution green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live verification against the connected client (part of the F1-F15
gate) found the Map tab's player/house icons never mount:
"[D.2b] Map tab: icon 0x100001ED did not resolve" / "...0x100001EE did
not resolve". AD-108 (filed earlier this session for F9) had described
the standalone re-import mechanism as working; it does not.
A throwaway diagnostic (not committed) confirmed the root cause:
LayoutImporter.ImportInfos(dats, hostLayoutId, elementId)'s FindDesc
walks the LayoutDesc's raw top-level Elements table (one entry) and
recurses through ElementDesc.Children with no tab-page/state resolution
— calling it directly with these icon ids returns null. Resolving the
panel's own slot first (what MountMapHousePanel actually does) and
searching THAT tree finds m_pMap with both icon children present, so
the icons are real, just unreachable via a cold standalone import.
This is pre-existing (predates this session, confirmed via git log)
and unrelated to any F1-F15 fix — it means F1's byte-decoded
PlaceMarkerOnMap formula could not be visually confirmed against the
running client this round; it remains verified only at the unit-test/
golden-pixel level. Filed a follow-up task for the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F11: logs when a Map tab town-marker's template resolves to something
other than a UiButton — that path previously silently skipped the
TooltipText write with no diagnostic, leaving a mounted-but-empty
tooltip popup indistinguishable from "no template configured".
F13: RetailSkillFormula.FormatFormula now reads Attribute1Multiplier/
Attribute2Multiplier/AdditiveBonus/Divisor through the SAME unsigned
reinterpretation TryCalculate already uses (this class's own doc
comment already stated the invariant; FormatFormula just didn't follow
it). A high-bit-set value would previously both mis-gate hasAttr1/
hasAttr2 and print a negative number, out of sync with what
TryCalculate actually computes with for the same formula. Added
regression tests, empirically verified to fail without the fix.
F14: documented the RefreshHouseMarker gap rather than guessing at the
byte-decode — Position::get_outside_cell_id @0x004527b0 is itself
BN-mangled (its `(eax_2 - eax_2) & objcell_id` return is the same
decompiler-obscures-a-real-conditional artifact class this round hit
elsewhere) and depends on LandDefs::adjust_to_outside, a genuinely
larger port than this round's other findings. HousePosition is wired
() => null in production today (ISSUES #413's remaining scope), so
this method is currently unreachable; left a TODO citing the retail
call chain for whenever that lands.
F15: fixed RefreshCoordinatesAndPlayerMarker's gate to AND-on-both-
present, matching gmMapUI::Update @0x004a2078's exact
`if (m_pCoordinateText != 0 && m_pPlayerLocationIcon != 0)` condition.
The prior `_coordinateText is null && _playerIcon is null` check only
skipped when BOTH were absent (proceeding whenever EITHER was
present), letting coordinate text and the player marker update
independently instead of as the single gated unit retail treats them
as. Added a regression test (player-icon template resolution failure
must also skip the coordinate-text write), empirically verified to
fail without the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F9: filed register row AD-108 for MapPageController.ResolveSwallowedIcon
— the standalone re-import of the Map tab's player/house icons, which
m_pMap's own Type-1 UiButton authoring swallows as dat children
(UiButton.ConsumesDatChildren). This adaptation was implemented but
never had a register row.
F10: extracted the popup-locator pair (0x10000395/0x21000041),
previously duplicated as three separately-cited private constants
across UiItemSlot.cs, RetailTooltipPresenter.cs, and
MapPageController.cs, into ONE public pair on RetailTooltipPresenter
(SharedPopupSkinRootElementId/SharedPopupSkinLayoutDid) with a single
canonical citation. The other two sites now reference it instead of
carrying their own copy.
F12: fixed TS-85's SetTooltip-site arithmetic. The register (and a
mirrored ISSUES.md log entry) claimed "15 known sites, all accounted
for" — recounting the row's own enumerated list finds 17 distinct
sites (the tally had dropped gmPaperDollUI::UpdateItemSlotTooltip
@0x004A52EF and undercounted by one more), of which 16 are ported and
one — UIElement_Text::RecalculateTruncation @0x00466F80, the headline
highest-volume site sub-mechanism (1) itself named as deliberately
deferred — was never actually closed. The "all 15 accounted for"
close was wrong twice over: wrong count, and a site the row's own text
already scoped as open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gmHouseUI::DisplayPurchaseTimeText @0x004a3110's not-yet-expired
branch was wrongly marked "unrecoverable from this decomp dump" — a
direct capstone disassembly resolves all three concatenated pieces:
prefix "You may buy another landscape house at " @0x7ab790 (pushed
@0x004a3265), the strftime "%c" format literal @0x7ab7ec (pushed
@0x004a321d) applied to localtime(timestamp + 0x278d00) — the expiry
moment, 30 days after the purchase timestamp — and suffix ". This
restriction does not apply to apartments." @0x7ab7b8 (pushed
@0x004a3235).
Ported in RuntimeHouseState.Recompute, substituting .NET's
culture-default DateTime.ToString() for the CRT's strftime("%c", ...)
(different formatting engine, same "process locale, full date+time"
intent) — filed as register row IA-23 (an approximation, not a gap).
TimeProvider.LocalTimeZone (overridable, defaulting to
TimeZoneInfo.Local in production) keeps the conversion deterministically
testable while matching retail's own localtime() call.
Updated RuntimeHouseStateTests: the not-expired case now asserts the
composed prefix/suffix structure and the exact expiry instant (pinned
via a UTC-fixed test TimeProvider), replacing the old "renders nothing"
assertion. Un-claimed "unrecoverable" in ISSUES #413 item 2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F5: moved the unconditional RemovePopup() call into
RetailTooltipPresenter.TryBuildAndMountPopup itself so the single-
popup invariant (retail's own single m_pTooltipElement slot) is
enforced structurally rather than relying on every caller to have
already cleared a stale popup. Closes a real hole: UpdateWorldHoverTooltip's
own clear is gated on _worldTooltipShowing (only true when the WORLD
path itself mounted the current popup), and its "a UI popup cannot be
showing here" comment assumed the host's hover query is null whenever
that branch runs — an assumption that breaks the instant a modal opens
over a stationary cursor. UiRoot.Modal claims EXCLUSIVE hit-testing, so
Pick(MouseX, MouseY) can return null even though a UI-dwell tooltip is
still mounted underneath; UpdateWorldHoverTooltip would then mount a
second popup on top without ever clearing the first.
F6: fixed WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks to
actually exercise the transition with a follow-up presenter.Tick()
(the old test only proved OnTooltipShow's own clear worked, never
checked the world-side bookkeeping after). Added
UiDwellTooltip_ThenModalStealsHitTesting_WorldHoverReplacesRatherThanStacks
for F5's own case, using UiRoot.Modal to reproduce the exclusive-hit-
testing hole precisely — empirically verified this new test fails
(2 popups instead of 1) with the structural RemovePopup() reverted,
confirming it is a real regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F3: TS-85 had claimed the plain-spell branch's three SetTooltip format
strings were "genuine gmNoticeHandler vtable SLOTS" and unrecoverable
from the decomp dump. That was itself the artifact — Binary Ninja's
pseudo-C rendering of PStringBase::sprintf's second argument as
"&gmSpellcastingUI::`vftable'.RecvNotice_XXX" was a spurious symbol
match, not the true operand. A direct capstone disassembly of the raw
bytes at gmSpellcastingUI::UpdateCastButtonTooltip @0x004c6a30's four
call sites (0x4c6e48/0x4c6ea4/0x4c6f18/0x4c6f5d) resolves the actual
pushed literals: "CAST %hs" @0x7b63a4 (untargeted/self-cast, and
targeted+compatible with " on %s" @0x7b6464 appended), "You must
select an appropriate target for %hs" @0x7b6348 (incompatible target),
"You must select a target for %hs" @0x7b63b8 (no target). %hs is the
spell's own name throughout.
Added RuntimeSpellCastState.EvaluateCastGate (SpellCastGate: NoTarget-
Needed/TargetCompatible/TargetIncompatible/NoTargetSelected/Unknown),
refactoring IsTargetReady to use it, and wired
SpellcastingUiController.ComputeSpellCastState to the four-state
tooltip text, replacing the bare-spell-name fallback.
F4: the endowment branch's "USE the %s" (and both select-target
strings) vararg is NOT the bare item name — retail composes
"%s (%hs)" @0x7b64d8 (item name, spell name) once at @0x004c6bb6-ef
and reuses it for all three format strings, byte-confirmed by all
three sprintf call sites (0x4c6c7f/0x4c6ca4/0x4c6d46) reading the
identical stack slot. Added ComposeEndowmentName and wired it in place
of the bare item name.
F7: added test coverage for the two genuinely NEW disabled states
(needs-target, needs-appropriate-target) neither branch had any
coverage for before, plus the enabled untargeted/targeted-compatible
states and both endowment-branch composed-name cases.
Corrected the register's TS-85 row (the "cannot be recovered" claim
and the endowment operand claim) with the byte-decoded findings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CM_House::Event_QueryHouse @0x006aaa00 (opcode 0x21e) tail-calls
unconditionally from the end of CPlayerSystem::InitializePlayer
@0x00563570 — the same once-per-session function
AttemptSendLoginCompleteNotification lives in (both guarded by the
player_initialized flag), right after that notification. Retail never
sends it from gmHouseUI::PostInit or gmMapUI::PostInit on House-tab
activation.
Moved WorldSession.SendHouseQuery() to the direct (non-portal)
first-entry completion edges — the same places acdream already sends
the analogous "initial session bootstrap" LoginComplete:
- graphical: LiveSessionRuntimeFactory's RuntimeFirstEntryDriveController
localPlayerCompleted callback
- headless: HeadlessSessionHost's equivalent callback
- headless content-less direct host: RuntimeLiveEntitySessionController.OnSpawned
Portal-space re-entries (LocalPlayerTeleportController's F751 path,
RuntimeLiveEntitySessionController.TryAdvancePortalCompletion) do NOT
resend it, matching retail's single-shot guard.
Removed the invented House-tab-open -> SendHouseQuery trigger
(InteractionRetainedUiComposition's HouseShown binding) and retired
register row AD-107, which had documented that adaptation.
Updated RuntimeLiveEntitySessionControllerTests' exact game-action
assertions for the content-less path, which now also captures the
HouseQuery send alongside LoginComplete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior PlaceMarker() reading ("center at markerX0+x") was wrong.
Binary Ninja elides gmMapUI::PlaceMarkerOnMap @0x004a18b0's entire FPU
chain to bare, operand-less _ftol2() calls, so the pseudo-C
under-specifies the function. A capstone disassembly of the raw bytes
in the PDB-paired acclient.exe recovers the real formula: retail
projects the AC display coordinate (range ~-102.4..102.4) onto the
marker-area rect via a fixed-point-style transform, not a raw pixel
add:
X = m_x0 - w/2 - (int)((m_x1-m_x0+1) * (x*10+1024) * (-1/2048))
Y = m_y0 - h/2 - (int)((m_y1-m_y0+1) * (2047-(y*10+1024)) * (-1/2048))
Constants read directly from .rdata: 0x79bac8=10.0, 0x7aac78=1024.0,
0x7aac70=-1/2048, 0x7aac68=2047.0. The Y axis's FSUBR is retail's
north-up flip. w/h halve with truncating integer division (matching
retail's cdq;sub;sar idiom), not float division.
Extracted the pure math into MapPageController.ComputeMarkerPosition
so it's directly testable, and retargeted MapPageControllerTests to
GOLDEN PIXEL values computed independently from the formula (never
from the port's own output): the reviewer's canonical (0,0)->(122,128)
case, a far-west and far-north case, and a real town-table entry
(Arwic's landblock, cross-checked against RadarCoordinates). Applies
to the green ring, house pin, and all 53 static town hotspots, which
all resolve through the same PlaceMarker call.
Corrected the recon doc's "accepted as-is" note, which had mistaken
"the FPU argument-passing is BN-mangled" for a narrow issue instead of
the whole-formula elision it actually was.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derived the mechanism from the decomp before writing code: neither
gmHouseUI::PostInit @0x004a2710 nor gmMapUI::PostInit @0x004a1c70 sends a
HouseQuery, and six of gmHouseUI's seven Display* builders early-return on
m_pHouseData == 0. The only text a houseless character's House tab shows is
gmHouseUI::DisplayPurchaseTimeText @0x004a3110's expired branch (it doesn't
gate on m_pHouseData) — the local player's PropertyInt.HousePurchaseTimestamp
plus HouseSystem::HasPurchaseWaitPeriodExpired renders exactly "You may buy
another house immediately." for a fresh character. Exhaustive search of the
2013 EoR decomp, ACE, and the live DAT found zero support for a second
"You do not currently own a house." line the task brief described — this
commit ports what the decomp actually shows.
Ships:
- RuntimeHouseState: a minimal (no disposal, no construction-transaction
Fault() point) Runtime owner per ISSUES #413's own sizing note, wired
through GameEventWiring's existing HouseData/HouseStatus delegate holes,
LiveSessionEventRouter, and GameRuntime.HouseOwner. Participates in
RuntimeGenerationReset (new House stage) since a fresh login must not
show a stale character's house state.
- HousePageController.Bindings.Lines/OnShown wired to real data; OnShown
fires WorldSession.SendHouseQuery() on tab-open (AD-107: an acdream
trigger, not a ported retail call site — filed in the divergence
register).
- Fixed a real bug found along the way: HousePageController.Bind never
wired UiTemplateListBox.TemplateResolver, so no row could ever render
regardless of Lines content. Now reuses the Map tab's generic hotspot
resolver.
Live-verified against a real local ACE server and the +Acdream character
(--session-config auto-select + a UI automation script): screenshot and
structural UI-tree dump both confirm the House tab renders exactly "You may
buy another house immediately." Graceful logout confirmed both launches.
ISSUES #413 narrowed to its one remaining piece: the six owned-house-only
Display* builders (DisplayBuyPayment/RentPayment/BuyTime/RentTimes/
Location/WarningText), unexercisable without a test character that owns a
house.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row committed alongside the panel shell (8799acd2) described the
Batch C map-marker tooltip as using AuthoredTooltipText/Enabled — that
was the pre-live-verification code. Slice 5 found it never rendered
live and the actual fix (commit e5629d71) uses UiButton.TooltipText
(retail's runtime m_TTText/SetTooltip mechanism) plus a hardcoded
popup-skin locator matching UiItemSlot's precedent. Updates the row
to describe the shipped mechanism instead of the abandoned one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live verification (slice 5) found town-marker tooltips never appeared:
RetailTooltipPresenter.OnTooltipShow gates unconditionally on
AuthoredTooltipRootElementId == 0 -> return, with no fallback, but the
markers only set AuthoredTooltipText/Enabled (the DAT-authored P0x49
path). gmMapUI::AddMapNote's UIElement::SetTooltip call is retail's
RUNTIME m_TTText/SetTooltip mechanism, not the authored path — the
correct seam is UiButton.TooltipText (backing GetTooltipText()'s
override), which ResolveTooltipText consults before authored text.
The popup-skin locator (AuthoredTooltipRootElementId/LayoutDid) is
still required even on the runtime-text path with no built-in
fallback, so markers now hardcode the same shared popup skin
UiItemSlot already uses (0x10000395/0x21000041) — matching that
established precedent exactly.
Verified live: hovering a town marker (Aerlinthe Island) now renders
its tooltip correctly. 21/21 Map/House controller tests still pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The class doc referenced RuntimeHouseState.PurchaseAvailabilityText as
already wired this session; it isn't (deferred to #413, the
RuntimeHouseState owner integration). Corrected to accurately describe
what shipped (mount + wire parsing groundwork) vs what's still open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gmMapUI::AddMapNote's 53 town-hotspot tooltips are now ported
(MapPageController.BuildTownMarkers), closing the last remaining
SetTooltip call site TS-85's sub-mechanism (1) enumeration tracked.
Sub-mechanism (2) (the P0x3D wrap-width override) remains open and
unrelated to this batch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Precise scope note for the remainder of the House tab wire, per the task's
pre-authorized fallback: RuntimeHouseState owner integration,
DisplayPurchaseTimeText's port (the one builder simple enough to have
landed this session but deferred for time), and the other six Display*
line builders (only exercisable once a house is actually owned).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the outbound HouseQuery action (0x021E, ClientCommandRequests.
BuildHouseQuery / WorldSession.SendHouseQuery — ACE GameActionHouseQuery.
Handle reads no payload) and inbound parsers for all four House wire
opcodes GameEventType already defined (0x0225-0x0228, gmHouseUI::PostInit's
registered notice handlers): GameEvents.ParseHouseData (BuyTime/RentTime/
Type/MaintenanceFree/Buy list/Rent list/Position — the Position field
reuses CreateObject.ServerPosition's existing 32-byte Cell+Pos.XYZ+
Rotation.WXYZ shape rather than a new type), ParseHouseStatus (WeenieError
u32), ParseUpdateRentTime, ParseUpdateRentPayment. Wire shapes verified
against ACE's HouseDataExtensions/HousePaymentExtensions (references/ACE/
Source/ACE.Server/Network/Structure/HouseData.cs, HousePayment.cs) — noted
that ACE's own UpdateRentTime/UpdateRentPayment writers are stubs (always
0u / always an empty list), captured as such rather than assumed live.
GameEventWiring.WireAll gets four new optional delegate holes
(onHouseData/onHouseStatus/onHouseUpdateRentTime/onHouseUpdateRentPayment)
following the exact trade-family precedent — registered only when non-null,
every existing caller compiles unchanged.
This is the "enum/parser groundwork" half of Slice 4's pre-authorized
fallback. NOT included (filed as an ISSUES entry): a RuntimeHouseState
GameRuntime owner (construction-transaction ceremony, fault-injection
points, disposal/convergence tracking — the same weight as
RuntimeTradeState's integration, judged disproportionate for tonight
alongside the completed Map tab), HousePageController's real Lines/
OnShown wiring, the DisplayPurchaseTimeText port, and the six other
Display* line builders. The House tab currently mounts with genuinely
empty content, matching retail's own PostInit (verified via
MapHousePanelSlotProbeTests' live-DAT probe, not assumed).
9 new HouseEventsTests (parser round-trips + truncation), 1 new
GameEventWiringTests case (all four opcodes reach their callbacks).
Core.Net.Tests: 1004/1004 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mounts host 0x2100006E slot 0x1000018C (RetailPanelCatalog.MapHouse = 16)
as a two-tab UiTabPanel (Map default, House second) through the OP3/FA3
recipe (LayoutImporter.Build -> Bind -> ActivateTabBehavior). Toolbar
button 0x1000019A un-ghosts (added to both RetailPanelCatalog.Mounted and
.Toolbar). Combined into one commit because MapHousePanelController.Bind
depends on both MapPageController and HousePageController existing —
splitting them would mean landing dead code first.
Map tab (gmMapUI, MapPageController):
- Calendar formatter matching gmMapUI::Update's "Date: %s\nTime: %s"
shape, reusing WorldTimeService.CurrentCalendar (new
Func<DerethDateTime.Calendar> dependency threaded through
InteractionRetainedUiDependencies/GameWindow — a stable long-lived
service, not routed through the deferred-binding machinery Radar's
per-session state needs). MonthName enum values already match retail
display text; HourName's "AndHalf" suffix is rewritten to "-and-Half".
- Coordinate math + marker placement reuse RadarCoordinates/
LandDefs.GidToLcoord verbatim (both already byte-exact ports of
CPlayerSystem::InqPlayerCoords/LandDefs::gid_to_lcoord) — no re-port.
PlaceMarkerOnMap's centering math (m_x0 + x - w/2) ported from
gmMapUI::PlaceMarkerOnMap @0x004a18b0. Indoor gating clears the
coordinate text and hides the player marker, matching
gmMapUI::Update's else branch.
- 53-town s_rgLocations table ported verbatim into MapLocations.cs.
Markers built once at bind time via the panel's own RowTemplateResolver
against m_pMap's authored hotspot-template attrs (0x47/0x48), with
literal-string tooltips through AuthoredTooltipText/Enabled
(RetailTooltipPresenter) — closes divergence-register row TS-85's last
item, gmMapUI::AddMapNote @0x004A1C51.
- Structural finding: m_pMap (0x100001EC) is itself authored as a Type-1
BUTTON (the GM click-to-teleport hook at
gmMapUI::ListenToElementMessage), and the player/house icons
(0x100001ED/EE) are its own NESTED children, not siblings —
UiButton.ConsumesDatChildren swallows them from the normally-built
tree. Both are re-resolved standalone through the same template
resolver the town hotspots use and reattached under m_pMap.
House tab (gmHouseUI, HousePageController): mounts the ListBox
(0x100001E6) with its authored row template, wired to an empty Lines()
source by default — genuinely empty until Slice 4's wire lands, matching
retail's own PostInit (no Update call, no static content).
21 new tests (7 MapHousePanelControllerTests, 14 MapPageControllerTests):
tab table pairing, close button, town-hotspot count/tooltips, calendar
formatter golden values (Frostfell 27/119 P.Y., every HourName incl.
AndHalf), player/house marker placement and indoor-gating reproduced
against the real fixture via already-tested RadarCoordinates (no
re-derivation). Fixture map_house_2100006E_1000018C.json captured via
the shared RetailLayoutFixtureGenerator (other 34 fixtures deliberately
NOT regenerated — out of scope for this batch, would touch unrelated
panels' schema drift).
Full solution builds clean; App suite 5391/0 failed/71 skipped (non-live;
one earlier flaky streaming failure unrelated to this change, confirmed
pre-existing on the branch before these commits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-DAT probe confirming the desk-verified facts before implementation:
host 0x2100006E slot 0x1000018C carries panelId 16 (gmMapUI::PostInit
signature children 0x100001EB-EF; gmHouseUI's ListBox 0x100001E6),
tabTableCount=2 with Map (button 0x100001F3 -> page 0x100001F6) as the
authored default and House (0x100001F4 -> 0x100001F7) second, close button
0x100001F5. Toolbar button 0x1000019A carries the matching panelId 16 —
the Map/House entry among the toolbar's three ghosted buttons. m_pMap's
own marker-area rect is (6,8)-(247,258); its hotspot template attrs
(0x47/0x48) resolve to element 0x100001F0 in LayoutDesc 0x21000026, a
10x10 Type-1 button with 3 states. The House ListBox authors exactly one
row template (LayoutDesc 0x21000025 element 0x100001E7, a bare
UIElement_Text row, no scrollbar) and zero static child rows — the box is
genuinely empty until the first server notice, refuting the recon's
"authored default content" hypothesis for the no-house case.
Kept as a permanent env-gated pin (ACDREAM_PROBE_LIVE_MOUNT=1), matching
FaPanelSlotProbeTests' precedent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Saves the overnight-round recon (embedded findings + this session's desk
verification) for auditability before implementation starts. Corrects two
handoff claims: the panel id is 16 (already resolved by the existing FA
campaign's full 16-slot gmPanelUI::SetupChildren dump, not a guess from
{1,2,6,14}), and GameEventType already defines all four House opcodes
(0x0225-0x0228) — what's missing is routing, not the enum. Identifies that
LandDefs.GidToLcoord/LcoordToGid (src/AcDream.Core/Physics/LandDefs.cs) is
an existing tested port of LandDefs::gid_to_lcoord, reusable for both the
Map tab's coordinate math and the House location display — no re-port
needed. Cites the toolbar button (0x1000019A, panel id 16), the 53-entry
s_rgLocations marker table verbatim, the ServerPosition wire struct reuse
for HouseData.Position, and the AuthoredTooltipText/RetailTooltipPresenter
seam that will close register row TS-85's last item (gmMapUI::AddMapNote).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TS-85 remainder batch (hover/UI overnight round, batch B). Audit found
three of the four listed spellcasting SetTooltip sites (endowment icon,
favorite, submenu) were already correct via UiCatalogSlot's pre-existing
Label-driven GetTooltipText; only the cast button (UiButton, no tooltip
wiring at all) was a real gap. Ports the verified literal states
("Select a spell to cast" / "You have no spells ready to cast" / the
full endowment-item USE-the-%s branch) plus a documented, narrower
fallback (spell name only) for the one sub-branch whose exact wording
sits behind a genuine gmNoticeHandler vtable-slot collision in the
pseudo-C dump rather than the unlabeled-string-pool class the rest of
this batch recovered.
Character panel: new UiClickablePanel.TooltipText seam (same pattern as
UiButton.TooltipText) carries the six hardcoded attribute descriptions
and three pair-shared vitals descriptions (byte-decoded from the retail
string pool) plus skill tooltips composed from the already-DAT-parsed
SkillBase.Description/.Formula — no hand-transcription needed for the
~30+ skill strings. The formula-to-text algorithm itself
(SkillSystem::InqSkillFormula) was recovered by byte-decoding six short
fragments Binary Ninja left completely unlabeled between two
gmSpellcastingUI vtable declarations.
Live-verified against the local ACE server: 34 real skills' composed
tooltips and both reachable cast-button states captured via a temporary
probe (stripped before this commit). Full solution suite green
(14,647 tests, 0 failures) both before and after the probe strip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Config tab's footer sat mid-panel with further rows drawing below the
window's bottom edge. Live-DAT measured: the mounted tab-host root is
authored 300x362 (retail's real default window size), but the Config page
slot underneath keeps its own larger design geometry (298x575 against a
300x600 canvas) until retail's real four-edge UiLayoutPolicy
(UIElement::UpdateForParentSizeChange @0x00462640) shrinks it on the first
ApplyAnchor pass -- verified stable, this part already worked.
The actual bug: UiTemplateListBox.Viewport (the UiScrollablePanel that
hosts + clips every row) is a programmatic C# element seeded at Bind time,
BEFORE the tree's first draw frame -- before the ListBox has ever shrunk.
Its legacy anchor baseline is captured lazily on its own first ApplyAnchor
call, which lands AFTER the ListBox has already shrunk earlier in that same
frame (parent-before-child draw order). That capture measures a negative
bottom margin the stretch math preserves forever: the viewport stayed
locked at its original 560px design height, clipping rows to a bound
retail never actually gave the window on screen.
Fix: force the viewport's anchor capture to happen immediately after
seeding it, while its Width/Height still exactly equal a zero-margin
baseline against the CURRENT (pre-shrink) parent, instead of lazily on the
first draw frame against an already-shrunk parent. This is #372's sequel --
#372 fixed the 0x0 collapse case; this is the "ListBox itself later
shrinks" case #372's own fixture never exercised.
Three new tests (UiTemplateListBoxViewportTests using the live-DAT-measured
298x575/276x560 numbers, plus two ConfigOptionsPageControllerTests against
the real production Bind path and the committed host fixture) all fail
pre-fix, confirmed by temporarily reverting the change. Scoped to
UiTemplateListBox's own viewport; UiScrollablePanel/ApplyAnchor/
ComputeAnchoredRect are untouched, so chat's transcript scrolling and every
other UiScrollablePanel/UiItemList consumer are unaffected.
fix#412
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RetailTooltipPresenter.UpdateWorldHoverTooltip only called RemovePopup()
on the found-object-LOST edge (found == 0u). An A->B found-object CHANGE
(walking past a run of NPCs/doors/lifestones with no intervening "nothing
found" frame) skipped straight to TryBuildAndMountPopup with the previous
popup still mounted as a child of _host -- only the _popupRoot reference
got overwritten, so every earlier popup was orphaned in the tree and never
removed. Matches the user's screenshot of 15+ stacked name boxes.
Fix: clear any showing world popup on ANY found-object edge -- change or
loss -- before evaluating whether to mount a new one, mirroring
OnTooltipShow's own unconditional RemovePopup() at its top.
Live-verified against local ACE (testaccount/+Acdream, session-config
launch): a temporary probe logged 103 mount/102 remove events across many
direct object-to-object transitions (Silver Tusker, Armored Tusker,
+Acdream); hostChildren never exceeded baseline+1 and popupSkinChildren
never exceeded 1 -- confirmed at most one tooltip ever exists. Probe
stripped before landing; two new fixture regressions
(WorldHover_FoundObjectChangesDirectly_ReplacesThePopupWithoutStacking,
WorldHover_ThenUiDwellTooltip_ReplacesRatherThanStacks) both fail pre-fix.
fix#409 (follow-on)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the round that shipped item-cell tooltips, the world-object
hover tooltip, and the #411 cursor-swap fix: #409's write-up gains a
"hover-feedback completion round" section covering all three items
with live-verification notes; #411 is closed with the corrected
decomp reading; register row TS-85 is narrowed to reflect the two
newly-ported SetTooltip call sites (UIElement_UIItem::UpdateTooltip,
UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound) and the
still-open ones (spellcasting endowment/cast-button/favorite/submenu,
map notes, character-panel attribute/skill info).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>