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:
parent
6ee3d88863
commit
353ae3bb0c
8 changed files with 89 additions and 35 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue