fix(ui): night-round review — F2 HouseQuery fires at login, not tab-open

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>
This commit is contained in:
Erik 2026-08-17 04:28:46 +02:00
parent 6ee3d88863
commit 353ae3bb0c
8 changed files with 89 additions and 35 deletions

File diff suppressed because one or more lines are too long

View file

@ -978,22 +978,24 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
AllegianceSetUpdateSubscription: on => AllegianceSetUpdateSubscription: on =>
late.GameRuntime.AllegianceSetUpdateSubscription(on), late.GameRuntime.AllegianceSetUpdateSubscription(on),
Trade: d.Runtime.Trade), Trade: d.Runtime.Trade),
// Batch C (overnight hover/UI round, 2026-08-17): HouseLines/ // Batch C (overnight hover/UI round, 2026-08-17): HouseLines
// HouseShown now wired to the minimal RuntimeHouseState // now wired to the minimal RuntimeHouseState owner (see its
// owner (see its class doc) — HousePosition (the Map tab's // class doc) — HousePosition (the Map tab's house marker) is
// house marker) is deferred to #413's remaining owned-house // deferred to #413's remaining owned-house work, since it
// work, since it needs HouseData's Position field, not yet // needs HouseData's Position field, not yet consumed here.
// consumed here. // Night-round review F2: the tab-open HouseShown ->
// SendHouseQuery trigger (former AD-107) is REMOVED — retail
// sends HouseQuery once, unconditionally, at
// CM_House::Event_QueryHouse @0x006aaa00 (tail-called from
// CPlayerSystem::InitializePlayer's login-complete path), not
// on House-tab activation; neither gmHouseUI::PostInit nor
// gmMapUI::PostInit sends one on tab-open. HouseShown now
// defaults to null (HousePageController.OnShown's
// _bindings.OnShown?.Invoke() no-ops).
MapHouse: new MapHouseRuntimeBindings( MapHouse: new MapHouseRuntimeBindings(
CurrentCalendar: d.CurrentCalendar, CurrentCalendar: d.CurrentCalendar,
PlayerCellId: () => d.PlayerController.Controller?.CellId ?? 0u, PlayerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
HouseLines: () => d.Runtime.HouseOwner.Lines, HouseLines: () => d.Runtime.HouseOwner.Lines),
// Not a ported retail call site — neither gmHouseUI::
// PostInit nor gmMapUI::PostInit sends an outbound
// HouseQuery; this is the acdream "fire when the House
// tab is shown" convenience HousePageController.Bindings.
// OnShown's own doc already documents.
HouseShown: () => late.Session.CurrentSession?.SendHouseQuery()),
StackSplitQuantity: d.StackSplitQuantity, StackSplitQuantity: d.StackSplitQuantity,
Plugins: d.UiRegistry, Plugins: d.UiRegistry,
Persistence: persistence, Persistence: persistence,

View file

@ -335,7 +335,20 @@ internal sealed class LiveSessionRuntimeFactory
_world.PlacementProjection, _world.PlacementProjection,
_world.PlacementRetries, _world.PlacementRetries,
_world.FirstEntryDrive, _world.FirstEntryDrive,
_ => session.SendGameAction(GameActionLoginComplete.Build()), _ =>
{
session.SendGameAction(GameActionLoginComplete.Build());
// Night-round review F2: CM_House::Event_QueryHouse @0x006aaa00
// is tail-called, unconditionally, from the end of
// CPlayerSystem::InitializePlayer @0x00563570 — the SAME
// once-per-session function AttemptSendLoginCompleteNotification
// lives in (guarded by player_initialized), right after that
// notification. This is the graphical host's direct
// (non-portal) first-entry completion edge — the exact
// analogue. Portal-space re-entries (LocalPlayerTeleportController)
// do NOT resend it, matching retail's single-shot guard.
session.SendHouseQuery();
},
_world.AcceptedPositionDrive, _world.AcceptedPositionDrive,
_world.RemotePlacementDrive); _world.RemotePlacementDrive);
} }

View file

@ -31,16 +31,19 @@ namespace AcDream.App.UI.Layout;
/// <c>ParseUpdateRentTime</c>/<c>ParseUpdateRentPayment</c>, /// <c>ParseUpdateRentTime</c>/<c>ParseUpdateRentPayment</c>,
/// <c>GameEventWiring</c>'s four delegate holes, the outbound HouseQuery /// <c>GameEventWiring</c>'s four delegate holes, the outbound HouseQuery
/// action). The House-tab ownership-text closer session (also 2026-08-17) /// action). The House-tab ownership-text closer session (also 2026-08-17)
/// wired <see cref="Bindings.Lines"/>/<see cref="Bindings.OnShown"/> to the /// wired <see cref="Bindings.Lines"/> to the minimal <c>RuntimeHouseState</c>
/// minimal <c>RuntimeHouseState</c> owner and ported /// owner and ported <c>DisplayPurchaseTimeText @0x004a3110</c>'s expired
/// <c>DisplayPurchaseTimeText @0x004a3110</c>'s expired branch — a fresh /// branch — a fresh houseless character's House tab shows the single
/// houseless character's House tab now shows the single decomp-verified /// decomp-verified line "You may buy another house immediately.",
/// line "You may buy another house immediately." after the tab is opened,
/// live-connected-gate-verified (screenshot + structural UI-tree dump /// live-connected-gate-verified (screenshot + structural UI-tree dump
/// against the real <c>+Acdream</c> character on a local ACE server). The /// against the real <c>+Acdream</c> character on a local ACE server). The
/// other six <c>Display*</c> line builders <c>DisplayHouseData</c> calls /// other six <c>Display*</c> line builders <c>DisplayHouseData</c> calls
/// (owned-house-only content: buy/rent payments and times, location, /// (owned-house-only content: buy/rent payments and times, location,
/// warning text) remain unported — ISSUES #413's surviving scope. /// warning text) remain unported — ISSUES #413's surviving scope. The
/// night-round review (F2, 2026-08-17) moved the outbound HouseQuery send
/// from a House-tab-open trigger to retail's real login-complete edge (see
/// <see cref="Bindings.OnShown"/>'s own doc), so by the time a player opens
/// the House tab the data has usually already arrived.
/// </para> /// </para>
/// </summary> /// </summary>
public sealed class HousePageController public sealed class HousePageController
@ -49,12 +52,20 @@ public sealed class HousePageController
public sealed record Bindings( public sealed record Bindings(
Func<IReadOnlyList<string>> Lines, Func<IReadOnlyList<string>> Lines,
// Fires once when the page transitions to visible — the seam that // Fires once when the page transitions to visible. Night-round
// WILL send the outbound HouseQuery (0x021E, already implemented as // review F2 (2026-08-17): NOT wired to SendHouseQuery any more —
// WorldSession.SendHouseQuery) once a caller wires OnShown to it — // retail's HouseQuery (0x021E, CM_House::Event_QueryHouse
// see ISSUES #413. NOT a ported retail call site (PostInit never // @0x006aaa00) is byte-decoded confirmed to fire exactly once at the
// triggers a query) — an acdream convention, documented as such // client's login-complete edge (tail-called, unconditionally, from
// (recon doc open item). // CPlayerSystem::InitializePlayer @0x00563570, right after
// AttemptSendLoginCompleteNotification — both guarded by the SAME
// once-per-session player_initialized flag), never from House-tab
// activation; neither gmHouseUI::PostInit nor gmMapUI::PostInit
// sends one on tab-open. See WorldSession.SendHouseQuery's own
// production call sites (the graphical/headless first-entry-
// completion edges) for where it's actually sent now. This hook
// remains available for a genuinely page-shown concern, but no
// current caller wires it.
Action? OnShown = null, Action? OnShown = null,
// Batch C House-ownership-text closer (2026-08-17): the ListBox's // Batch C House-ownership-text closer (2026-08-17): the ListBox's
// OWN row template (LayoutDesc 0x21000025 element 0x100001E7, // OWN row template (LayoutDesc 0x21000025 element 0x100001E7,

View file

@ -73,11 +73,16 @@ public sealed class MapHousePanelController : IRetainedPanelController
_map = map; _map = map;
_house = house; _house = house;
// House's outbound query is a fire-when-shown convenience (see // Night-round review F2 (2026-08-17): this used to be House's
// HousePageController.Bindings.OnShown's own doc — not a ported // outbound-HouseQuery fire-when-shown trigger; that trigger is
// retail trigger, an acdream one), gated the same "window shown AND // REMOVED (former register row AD-107) now that HouseQuery is
// my tab active" conjunction the social panel's Fellowship/ // byte-decoded confirmed to fire once at retail's login-complete
// Allegiance pages use for their own declarations. // edge instead (see HousePageController.Bindings.OnShown's own
// updated doc). The "window shown AND my tab active" plumbing stays
// — it mirrors the social panel's Fellowship/Allegiance page-shown
// convention and remains available for a genuinely page-shown
// concern — but HousePageController.Bindings.OnShown is unwired
// (null) in production, so this call chain is currently a no-op.
_onActivePageChanged = (_, _) => FireHouseShownIfActive(); _onActivePageChanged = (_, _) => FireHouseShownIfActive();
_tabPanel.ActivePageChanged += _onActivePageChanged; _tabPanel.ActivePageChanged += _onActivePageChanged;
} }

View file

@ -1266,6 +1266,14 @@ internal sealed class HeadlessSessionHost : IDisposable
// RuntimeLiveEntitySessionController's onLoginCompleteSent // RuntimeLiveEntitySessionController's onLoginCompleteSent
// doc for the other two. // doc for the other two.
_optionsSeeder?.NoteLoginCompleteSent(); _optionsSeeder?.NoteLoginCompleteSent();
// Night-round review F2: CM_House::Event_QueryHouse
// @0x006aaa00 tail-calls unconditionally from the end of
// CPlayerSystem::InitializePlayer — the same once-per-session
// function AttemptSendLoginCompleteNotification lives in
// (guarded by player_initialized) — right after that
// notification. This is the headless direct (non-portal)
// first-entry completion edge.
session.SendHouseQuery();
}, },
_acceptedPositionDrive); _acceptedPositionDrive);
_eventRoute = eventRoute; _eventRoute = eventRoute;

View file

@ -198,6 +198,14 @@ public sealed class RuntimeLiveEntitySessionController
// truthful terminal admission edge. // truthful terminal admission edge.
_initialLoginCompleteSent = true; _initialLoginCompleteSent = true;
_session.SendGameAction(GameActionLoginComplete.Build()); _session.SendGameAction(GameActionLoginComplete.Build());
// Night-round review F2: CM_House::Event_QueryHouse
// @0x006aaa00 tail-calls unconditionally from the end of
// CPlayerSystem::InitializePlayer — the same once-per-session
// function AttemptSendLoginCompleteNotification lives in
// (guarded by player_initialized) — right after that
// notification. This is the content-less direct host's own
// terminal admission edge, its equivalent moment.
_session.SendHouseQuery();
_onLoginCompleteSent?.Invoke(); _onLoginCompleteSent?.Invoke();
} }
} }

View file

@ -165,10 +165,18 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Spawn(playerGuid, incarnation: 1); Spawn(playerGuid, incarnation: 1);
sink.Spawned(spawn); sink.Spawned(spawn);
Assert.Single(gameActions); // Night-round review F2: the content-less direct host's terminal
// admission edge now also sends HouseQuery (0x021E), matching
// retail's CM_House::Event_QueryHouse tail-call from
// CPlayerSystem::InitializePlayer right after
// AttemptSendLoginCompleteNotification. It's the session's first
// NextGameActionSequence() call (LoginComplete doesn't consume one),
// so its sequence is deterministically 1.
Assert.Equal(2, gameActions.Count);
Assert.Equal(GameActionLoginComplete.Build(), gameActions[0]); Assert.Equal(GameActionLoginComplete.Build(), gameActions[0]);
Assert.Equal(ClientCommandRequests.BuildHouseQuery(1u), gameActions[1]);
sink.Spawned(spawn); sink.Spawned(spawn);
Assert.Single(gameActions); Assert.Equal(2, gameActions.Count);
gameActions.Clear(); gameActions.Clear();
sink.TeleportStarted(1u); sink.TeleportStarted(1u);