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 =>
late.GameRuntime.AllegianceSetUpdateSubscription(on),
Trade: d.Runtime.Trade),
// Batch C (overnight hover/UI round, 2026-08-17): HouseLines/
// HouseShown now wired to the minimal RuntimeHouseState
// owner (see its class doc) — HousePosition (the Map tab's
// house marker) is deferred to #413's remaining owned-house
// work, since it needs HouseData's Position field, not yet
// consumed here.
// Batch C (overnight hover/UI round, 2026-08-17): HouseLines
// now wired to the minimal RuntimeHouseState owner (see its
// class doc) — HousePosition (the Map tab's house marker) is
// deferred to #413's remaining owned-house work, since it
// needs HouseData's Position field, not yet 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(
CurrentCalendar: d.CurrentCalendar,
PlayerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
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()),
HouseLines: () => d.Runtime.HouseOwner.Lines),
StackSplitQuantity: d.StackSplitQuantity,
Plugins: d.UiRegistry,
Persistence: persistence,

View file

@ -335,7 +335,20 @@ internal sealed class LiveSessionRuntimeFactory
_world.PlacementProjection,
_world.PlacementRetries,
_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.RemotePlacementDrive);
}

View file

@ -31,16 +31,19 @@ namespace AcDream.App.UI.Layout;
/// <c>ParseUpdateRentTime</c>/<c>ParseUpdateRentPayment</c>,
/// <c>GameEventWiring</c>'s four delegate holes, the outbound HouseQuery
/// action). The House-tab ownership-text closer session (also 2026-08-17)
/// wired <see cref="Bindings.Lines"/>/<see cref="Bindings.OnShown"/> to the
/// minimal <c>RuntimeHouseState</c> owner and ported
/// <c>DisplayPurchaseTimeText @0x004a3110</c>'s expired branch — a fresh
/// houseless character's House tab now shows the single decomp-verified
/// line "You may buy another house immediately." after the tab is opened,
/// wired <see cref="Bindings.Lines"/> to the minimal <c>RuntimeHouseState</c>
/// owner and ported <c>DisplayPurchaseTimeText @0x004a3110</c>'s expired
/// branch — a fresh houseless character's House tab shows the single
/// decomp-verified line "You may buy another house immediately.",
/// live-connected-gate-verified (screenshot + structural UI-tree dump
/// against the real <c>+Acdream</c> character on a local ACE server). The
/// other six <c>Display*</c> line builders <c>DisplayHouseData</c> calls
/// (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>
/// </summary>
public sealed class HousePageController
@ -49,12 +52,20 @@ public sealed class HousePageController
public sealed record Bindings(
Func<IReadOnlyList<string>> Lines,
// Fires once when the page transitions to visible — the seam that
// WILL send the outbound HouseQuery (0x021E, already implemented as
// WorldSession.SendHouseQuery) once a caller wires OnShown to it —
// see ISSUES #413. NOT a ported retail call site (PostInit never
// triggers a query) — an acdream convention, documented as such
// (recon doc open item).
// Fires once when the page transitions to visible. Night-round
// review F2 (2026-08-17): NOT wired to SendHouseQuery any more —
// retail's HouseQuery (0x021E, CM_House::Event_QueryHouse
// @0x006aaa00) is byte-decoded confirmed to fire exactly once at the
// client's login-complete edge (tail-called, unconditionally, from
// 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,
// Batch C House-ownership-text closer (2026-08-17): the ListBox's
// OWN row template (LayoutDesc 0x21000025 element 0x100001E7,

View file

@ -73,11 +73,16 @@ public sealed class MapHousePanelController : IRetainedPanelController
_map = map;
_house = house;
// House's outbound query is a fire-when-shown convenience (see
// HousePageController.Bindings.OnShown's own doc — not a ported
// retail trigger, an acdream one), gated the same "window shown AND
// my tab active" conjunction the social panel's Fellowship/
// Allegiance pages use for their own declarations.
// Night-round review F2 (2026-08-17): this used to be House's
// outbound-HouseQuery fire-when-shown trigger; that trigger is
// REMOVED (former register row AD-107) now that HouseQuery is
// byte-decoded confirmed to fire once at retail's login-complete
// 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();
_tabPanel.ActivePageChanged += _onActivePageChanged;
}

View file

@ -1266,6 +1266,14 @@ internal sealed class HeadlessSessionHost : IDisposable
// RuntimeLiveEntitySessionController's onLoginCompleteSent
// doc for the other two.
_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);
_eventRoute = eventRoute;

View file

@ -198,6 +198,14 @@ public sealed class RuntimeLiveEntitySessionController
// truthful terminal admission edge.
_initialLoginCompleteSent = true;
_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();
}
}

View file

@ -165,10 +165,18 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Spawn(playerGuid, incarnation: 1);
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(ClientCommandRequests.BuildHouseQuery(1u), gameActions[1]);
sink.Spawned(spawn);
Assert.Single(gameActions);
Assert.Equal(2, gameActions.Count);
gameActions.Clear();
sink.TeleportStarted(1u);