Both attempts removed from the branch by user direction after live
gates (attempt 1 culled the interior — the 'fixed' exit was a
nothing-drawn false positive; attempt 2's luminosity port produced no
visible change, theory-vs-plumbing unresolved). Patches preserved in
the session scratchpad. Next attempt starts with apparatus: tunnel
freeze probe, RenderDoc capture, ACViewer oracle, retail side-by-side.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While a destination reservation hides the world behind the authored
tunnel, the streaming frame meter now runs a hold-widened profile
(StreamingWorkBudget.WidenForDestinationHold): the time ceiling rises
from the authored 2 ms to an absolute 8 ms default
(ACDREAM_STREAM_WORK_HOLD_DEST_MS is a measurement-only override), every
count/byte dimension scales by the same factor so elapsed time stays the
authoritative guard (the measured binder is Time at both ceilings), and
the reserve fraction is re-derived (0.75 -> 0.9375) so the
NON-destination lane's absolute per-frame caps are unchanged. The
widening keys off the existing BeginDestinationReservation/
EndDestinationReservation bracket only, is derived per-Tick from the
CURRENT budget (mid-hold quality swaps compose), and a frame with no
reservation uses the authored budget verbatim (test-pinned). Portal
holds ride the same bracket as login holds by construction - intended,
and pinned by a kind-parity test through the real coordinator plus a
live @telepoi portal hold (kind=portal gate-ready 3589 ms).
Why: issue #418's next-hypothesis (1). Measured result: the ~5 s
publication drip collapsed to ~2 s (loaded 625/625 at ~3.0 s, tunnel at
64-66 fps), the portal-hold gate-ready fell to ~3.6 s - and login
gate-ready/total stayed at 8.4-8.8 s / 12.6-12.7 s, exposing the real
remaining pacer: the login-cold render-thread upload/registration
barrier behind GpuWorldState.IsRenderReady, which ran concurrently under
the old drip. Full attribution appended to docs/ISSUES.md #418; no
divergence-register row (the streamed result and reveal gate are
byte-identical; only the scheduling rate during a hidden hold changed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's benchmark (retail ~6 s at a smaller window without our
complete-at-reveal guarantee) is now the issue's done bar. Next levers
in order: hold-scoped destination-lane budget widening (predicted
gate-ready ~2-3 s), then upload/composite overlap; retail's authored
tunnel exit is not a tuning target. Miss-branch closeout per the
session goal: acceptance measured 12689/12734 ms (> 12000), attribution
complete, no blind tuning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase-1 measurement (new [publish-timing]/[stream-tick] probe surfaces,
ACDREAM_PROBE_REVEAL_TIMING=1) refuted the ~31 ms-per-admission
hypothesis: the hold runs at ~64 fps with the streaming tick at ~32 Hz,
the whole 625-block window costs only ~500 ms of publication CPU (far
blocks ~0.17 ms, near 2-43 ms), and steady state showed ZERO meter
yields with ~0.22 ms of the 2 ms budget used - yet exactly one block
published per tick against a ~400-deep completion queue.
The real limiter: Runtime's collision-generation activation is a
deliberate two-poll transaction (the first
TryAcquireCollisionPrefixMutationPermission poll parks residents and
refuses by design), and LandblockPresentationPipeline.Advance's metered
arm returned Completed=false on ANY nonterminal commit, which
DrainAndApply treats as end-of-frame. One landblock per 32 Hz tick =
the flat 32/s, with the authored budget ~90% idle.
Fix: the metered arm now uses the same Runtime-owned gate the unmetered
arm and the synchronous CompletePublication API always used
(CanContinueMutationSynchronously). The second poll runs in the same
frame under the same meter, so the unchanged 2 ms elapsed-time ceiling
is now genuinely the authoritative per-frame bound; with any real debt
(live residents parked mid-game, pending withdrawals, dispatch backlog)
publication defers to the next frame exactly as before. No budget
values change, no reveal-gate/readiness change, and the streamed result
is byte-identical - only the frame scheduling of identical operations.
Measured A/B (this binary, two runs): totalMs 12689 / 12734 vs baseline
26728/27395/27503; loaded slope 32/s -> bursts of 100-360/s, 625/625 in
~6-7 s vs ~23 s. The remaining ~12.7 s floor is fully attributed in
docs/ISSUES.md: ~8 s of real budgeted readiness work plus retail's
authored tunnel exit (TunnelContinue 2-5 s + two 1 s fades, golden
constants), so the <12 s acceptance needs a lead decision on the
hold-time budget, not another hidden limiter.
New regression pin:
MeteredLoaded_NonterminalCommitWithoutDebt_CompletesInOneMeteredAdvance.
Gates: Release build 0 errors; App tests 5576/3 skips/0 failed;
Runtime tests 1756/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The identical flat 32/s across the serial and pooled binaries points at a
single ~31 ms indivisible per-landblock admission on the update thread
(one admission per frame under the 2 ms floor, frame stretched to ~31 ms,
~32 fps x 1/frame = 32/s); the old serial builder producing at the same
~31 ms/block masked it. Predicts ~31 ms tunnel frame time; fix shape is
splitting/off-threading the publication cost, not budgets (disproved
twice). Verify the frame-time prediction first next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Login publishes the 25x25 window at a flat 32 blocks/s (~27 s in the
tunnel). The reveal-timing probe A/B (695a27b4) showed the consumer
budget env ceilings change nothing, which was read as producer-limited:
one "acdream.streaming.worker" thread, ~31 ms/block. This replaces the
single worker with min(ProcessorCount-2, 8) workers, floor 1.
Design: striped/affinity dispatch. Each worker owns one unbounded lane
channel plus its own high/low priority queues; jobs route to
lane = ((id >> 16) * 2654435761) % N (the low word of a landblock id is
constant, so the id is mixed before reduction). Striping was chosen
over a shared queue + in-flight conflict tracker because it preserves
the per-landblock contract structurally rather than by bookkeeping:
every job for one id lives on one lane, so per-id enqueue order IS
execution and completion-arrival order, and the same-landblock
supersede rules (PromoteToNear removes queued LoadFar/Unload) keep
seeing every queued job for that id. Contract, point by point:
- Per-landblock ordering: same id -> same lane -> serial FIFO.
- ClearLoads: broadcast to every lane inside the same _inboxGate lock
that serializes enqueues, so any load enqueued before
ClearPendingLoads() returns sits ahead of its lane's ClearLoads copy
in that lane's FIFO and is dropped at read time, exactly like the
single-thread path. Already-dequeued builds still complete (now up
to one per worker instead of one total); StreamingController's
SweepCollapsed already unloads those uniformly.
- Priority: per-lane high/low split unchanged. Cross-lane, priority is
not globally ordered (a lane cannot run another lane's job), which
the contract permits; near-tier jobs hash-spread across lanes and
are preferred within each.
- Outbox: SingleWriter flipped to false; nothing assumed single-writer
(PublishResult already used TryWrite + an Interlocked backlog, and
the consumer's peek->read head-stability holds because only the
single reader ever moves the head). Cross-landblock arrival order
was verified arbitrary-tolerant before relying on it:
StreamingController.AdmitCompletions classifies each result
independently into per-priority FIFOs (generation staleness +
per-landblock retirement blocking); per-landblock arrival order is
preserved by striping.
- Crash surface: per-worker. The first real crash publishes
WorkerCrashed (prefixed "worker N:" in pools > 1), sets
_workerFailure, completes every lane, and cancels the pool (a crash
still ends all processing, as before); siblings that merely observe
the closed lanes (ChannelClosedException) exit quietly instead of
reporting spurious crashes; the outbox completes only when the LAST
worker exits so no in-flight completions are dropped.
- Disposal: joins every worker under the same _disposeGate; Start
stays idempotent and dispose-serialized.
Thread-safety audit of the production build closures
(SessionPlayerComposition), per shared object:
- DatCollection (every read in LandblockBuildFactory.BuildLocked:
LandblockLoader.Load, SceneryGenerator.Generate, SetupMesh.Flatten,
CellMesh.Build, GfxObjBounds.Get, GfxObjDegradeResolver): NOT
thread-safe; already serialized under the shared _datLock, which
BuildLocked holds for the whole read transaction. Unchanged; the
probe run measured hold 0-13 ms / wait <= 12 ms during the login
window, so the lock is not the new bottleneck and the build was NOT
serialized beyond it.
- PakPreparedAssetSource / PakReader (BuildPreparedCollisionClosure,
outside the lock): immutable TOC array + read-only
MemoryMappedViewAccessor random-access reads + ConcurrentDictionary
verdict caches - safe for N concurrent readers (Slice I3 design;
the headless SharedPreparedCollisionCache wrapper is fully
lock-protected).
- LandblockMesh.Build (outside the lock): pure math over the dat
record + the composition-time height table + the immutable
TerrainBlendingContext record; the shared SurfaceCache is a
ConcurrentDictionary and BuildSurface is deterministic, so its
lookup-or-build race is last-write-wins-benign (the code already
documented exactly this).
- PhysicsDiagnostics probe statics: read-only bools + thread-safe
Console writes.
MEASURED OUTCOME (gate 4): the timing acceptance did NOT pass, and per
the task contract that is reported, not tuned around. With 8 workers
on this 16-core machine all 625 builds complete in ~203 ms
(ACDREAM_PROBE_TELEPORT BUILD lines t=3475390..3475593) - the producer
is off the critical path - but loaded= still advances at exactly
+32/1000 ms and SUMMARY totalMs measured 27395 and 27503 across two
runs (baseline 26728). The 32/s pacer is in the consumer
admission/publication path and is not governed by the
StreamingWorkBudgetOptions env ceilings. #418 stays IN-PROGRESS on the
consumer side; see docs/ISSUES.md for the evidence chain.
Tests: per-landblock ordering under 4-worker contention, cross-lane
ClearLoads drop, per-lane near-before-far preference, pool-of-1 serial
equivalence, disposal joining every worker, lane-spread guard, and
worker-count validation (LandblockStreamerPoolTests). Two existing
tests asserted a GLOBAL cross-landblock execution order - a serial
implementation detail, not the contract - and now pin workerCount: 1
with justification comments (LoadNear_OvertakesQueuedFarLoads,
TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration).
Gates: Release build 0 errors; App suite 5575 passed / 3 skipped
(5568 + 7 new); Runtime suite 1756/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The character-session reset manifest had no audio step: retail's logoff
destroys the world's sound sources with the world, but our OpenAL world
pool and ambient scheduler are process-lifetime — the continuous ambient
beds played on at character select and the scheduler kept RE-FIRING
deadlines against the stale listener (Suspend/StopAll had zero callers;
WorldGenerationQuiescence only cycles around teleport-style generation
replaces).
New WorldAudioSessionGate: the reset manifest's 'world audio' step stops
all sixteen world-pool voices (SuspendWorldAudio) and drops every ambient
deadline (StopAll); the pool reopens at the entered-world edge through the
new default-null LiveSessionEnteredWorldBindings.ResumeWorldAudio binding,
invoked first in ApplyEnteredWorld. The ambient soundscape needs no
explicit resume — the next objcell observation rebuilds it exactly as a
cell change always did. Covers logout, reconnect, and full stop uniformly.
UI-pool sounds (interface bank, portal cues) untouched by design.
App tests 5568/3 skips, Runtime 1756/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#416 (char-select roster highlight never cleared on hover-leave): three
decomp-grounded mechanisms replace the media-keyed _availableStates
approximation.
- UIElement_Button::UpdateState_ @0x00471CF0: the button machine commits
ONLY states authored on the button's OWN ElementDesc (AccessStateDesc
gate); unauthored requests no-op, preserving custom semantic states.
- UIElement::SetState @0x00464E70: an unauthored state id is coerced to
state 0 (the unnamed base state) and committed — ported into
UiDatElement.TrySetRetailState with the base-descriptor PassToChildren
cascade arm.
- The SetState media rule @0x004651c0: a committed state replaces the
playing media ONLY when its media array is non-empty. UiButton now keeps
per-face-segment media states under that rule (segments model retail's
PassToChildren children), and LayoutImporter records the raw MediaCount
including the File=0 draw-nothing images the drawable filter drops —
the roster bar children's base state is exactly such an image, and it is
what clears the bar.
The row template truth (probe, installed DAT): the row authors EMPTY
Normal/rollover/Highlight descriptors with PassToChildren; the three bar
children author rollover/Highlight media, NO Normal state, and a File=0
base image. An empty-media Normal_pressed still never blanks a Normal-art
button (the media rule keeps the previous art — the exact behavior the
old gate approximated), and the Appearance spins' property-only Highlight
now genuinely commits: label recolors, arrow art lingers — the retail
split AP-222 approximated with a requested-keyed label hack, now retired.
Live-verified at char select: hover +alex shows the grey bar, moving off
clears it, the selected row keeps its amber bar.
#415 (probe wait world-* verbs dead): the filed snapshot-reset diagnosis
was wrong — the automation bridge simply never bound without
ACDREAM_AUTOMATION_ARTIFACT_DIR. A facts-only
WorldRevealFactsAutomationRuntime now binds whenever the retained UI
exists; checkpoint/screenshot verbs still require the artifact directory
and now report that instead of a generic timeout.
App tests 5568/3 skips, Runtime 1756/0, UI.Abstractions 926/0.
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>
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 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>
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>
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>
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>
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>