fix(ui): morning gate — House tab renders retail's TWO houseless lines, not one
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>
This commit is contained in:
parent
9d9280a069
commit
ef567bfa20
3 changed files with 139 additions and 76 deletions
|
|
@ -17,26 +17,42 @@ namespace AcDream.Runtime.Gameplay;
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Retail behavior, exhaustively verified against the decomp before
|
||||
/// writing this class (all seven line builders read, not just the one
|
||||
/// ported here):</b> <c>gmHouseUI::PostInit @0x004a2710</c> never calls
|
||||
/// <c>Update</c>/<c>DisplayHouseData</c> — the House ListBox
|
||||
/// <b>Retail behavior:</b> <c>gmHouseUI::PostInit @0x004a2710</c> never
|
||||
/// calls <c>Update</c>/<c>DisplayHouseData</c> — the House ListBox
|
||||
/// (<c>0x100001e6</c>) starts genuinely empty (live-DAT-confirmed,
|
||||
/// <c>MapHousePanelSlotProbeTests</c>: <c>children=0</c>, and the whole
|
||||
/// House page <c>0x100001F7</c> has NO other static content besides that
|
||||
/// one empty ListBox). Content appears only after a server notice
|
||||
/// (0x0225-0x0228) arrives and <c>Update</c>/<c>DisplayHouseData</c> runs
|
||||
/// the seven <c>Display*</c> builders in order. SIX of them
|
||||
/// (<c>DisplayBuyPayment</c>, <c>DisplayRentPayment</c>,
|
||||
/// <c>DisplayBuyTime</c>, <c>DisplayRentTimes</c>, <c>DisplayLocation</c>,
|
||||
/// the seven <c>Display*</c> builders in fixed order. FIVE of them
|
||||
/// (<c>DisplayRentPayment</c>, <c>DisplayBuyTime</c>,
|
||||
/// <c>DisplayRentTimes</c>, <c>DisplayLocation</c>,
|
||||
/// <c>DisplayWarningText</c>) open with <c>if (this->m_pHouseData != 0)</c>
|
||||
/// and emit NOTHING when houseless — those remain unported, ISSUES #413
|
||||
/// item 3.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The SEVENTH, <c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c>, does
|
||||
/// NOT gate on <c>m_pHouseData</c> — it always runs, reading the LOCAL
|
||||
/// PLAYER's own <c>PropertyInt.HousePurchaseTimestamp</c> (0xC7 = 199
|
||||
/// <b>CORRECTED at the 2026-08-17 morning gate round (user finding 2: the
|
||||
/// retail House tab shows TWO lines for a houseless character; ours showed
|
||||
/// one):</b> the first builder, <c>gmHouseUI::DisplayBuyPayment
|
||||
/// @0x004a2b30</c>, is NOT houseless-silent — its <c>m_pHouseData</c> gate
|
||||
/// only selects WHICH text, and the ListBox emit
|
||||
/// (<c>AddItemFromTemplateList</c> + <c>SetTextWithFont</c>,
|
||||
/// <c>@0x004a2b80</c> onward) runs in BOTH branches. The houseless
|
||||
/// <c>else</c> branch (<c>@0x004a2b57</c>: <c>push 0x7ab688</c>,
|
||||
/// byte-decoded from the PDB-paired binary — Binary Ninja's pseudo-C
|
||||
/// rendered the operand as a spurious <c>&vftable.RecvNotice_*</c>
|
||||
/// symbol match, the same TS-85/F3 artifact class, which is how the night
|
||||
/// round's exe-pool sweep missed it) is the literal <b>"You do not
|
||||
/// currently own a house."</b> — exactly the user's retail screenshot. The
|
||||
/// owned branch (<c>@0x004a2b63</c>, <c>data_7ab65c</c>: <c>"The purchase
|
||||
/// price for this dwelling is:\n"</c> + <c>HousePaymentList::ComposeText</c>)
|
||||
/// remains #413 item-3 scope.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The SEVENTH builder, <c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c>,
|
||||
/// also does NOT gate on <c>m_pHouseData</c> — it always runs, reading the
|
||||
/// LOCAL PLAYER's own <c>PropertyInt.HousePurchaseTimestamp</c> (0xC7 = 199
|
||||
/// decimal) via <c>CBaseQualities::InqInt</c> and
|
||||
/// <c>HouseSystem::HasPurchaseWaitPeriodExpired</c>
|
||||
/// (<c>@0x005bb1d0</c>: <c>(Timer::get_real_time() - timestamp) >
|
||||
|
|
@ -44,22 +60,12 @@ namespace AcDream.Runtime.Gameplay;
|
|||
/// seconds; <c>0x278d00</c> = 2,592,000 s = 30 days). For a fresh/houseless
|
||||
/// character with no timestamp ever set (absent property reads as 0), this
|
||||
/// is trivially true, taking the "expired" branch, which reads
|
||||
/// <c>m_pHouseData == 0</c> (still houseless) and emits the ONE literal
|
||||
/// string at <c>data_7ab7f0</c>: <b>"You may buy another house
|
||||
/// immediately."</b> That is the exact, decomp-verified, single line of
|
||||
/// content a houseless character's House tab shows once queried — this
|
||||
/// class ports exactly that (and its owns-a-house sibling at
|
||||
/// <c>data_7ab818</c>, unreachable by a fresh character but faithfully
|
||||
/// ported alongside it). No other function, WeenieError-to-chat mapping,
|
||||
/// or authored LayoutDesc content anywhere in the decomp/live DAT produces
|
||||
/// a second line for the houseless case — a broader search for chat-scroll
|
||||
/// strings mentioning house ownership found only unrelated, differently
|
||||
/// worded command-error text (<c>"You do not own a house!"</c>,
|
||||
/// WeenieError <c>0x45E</c>/<c>0x45F</c>, and <c>"You must own a house to
|
||||
/// use this command."</c>, WeenieError <c>0x47F</c>) routed through the
|
||||
/// GENERIC WeenieError chat dispatcher, never through <c>gmHouseUI</c>'s
|
||||
/// own notice handlers (which discard the wire WeenieError entirely — see
|
||||
/// <see cref="ApplyHouseStatus"/>).
|
||||
/// <c>m_pHouseData == 0</c> (still houseless) and emits the literal at
|
||||
/// <c>data_7ab7f0</c>: <b>"You may buy another house immediately."</b>
|
||||
/// (owns-a-house sibling at <c>data_7ab818</c>, byte-re-verified this
|
||||
/// round). So a queried houseless character's House tab shows exactly TWO
|
||||
/// lines, in builder order: "You do not currently own a house." then the
|
||||
/// purchase-time line — which this class now renders.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The NOT-yet-expired branch of <c>DisplayPurchaseTimeText</c> is now
|
||||
|
|
@ -169,10 +175,27 @@ public sealed class RuntimeHouseState
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary><c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c>,
|
||||
/// both branches now ported. Must hold <see cref="_gate"/>.</summary>
|
||||
/// <summary>The ported share of <c>gmHouseUI::Update</c>'s fixed
|
||||
/// seven-builder order: <c>DisplayBuyPayment @0x004a2b30</c>'s
|
||||
/// houseless branch (first) and <c>DisplayPurchaseTimeText
|
||||
/// @0x004a3110</c> (last), both branches. The five houseless-silent
|
||||
/// builders between them, and DisplayBuyPayment's owned branch, are
|
||||
/// ISSUES #413 item 3. Must hold <see cref="_gate"/>.</summary>
|
||||
private void Recompute(uint selfGuid)
|
||||
{
|
||||
var lines = new List<string>(2);
|
||||
|
||||
// gmHouseUI::DisplayBuyPayment @0x004a2b30 — NOT houseless-silent
|
||||
// (2026-08-17 morning gate correction; see the class doc): the
|
||||
// m_pHouseData gate only selects WHICH text, and the emit runs in
|
||||
// both branches. Houseless (@0x004a2b57, byte-decoded data_7ab688):
|
||||
// this exact literal. Owned (@0x004a2b63, data_7ab65c "The purchase
|
||||
// price for this dwelling is:\n" + HousePaymentList::ComposeText):
|
||||
// unported, #413 item 3 — the owned case adds nothing here yet.
|
||||
if (!_ownsHouse)
|
||||
lines.Add("You do not currently own a house.");
|
||||
|
||||
// gmHouseUI::DisplayPurchaseTimeText @0x004a3110, both branches.
|
||||
int timestamp = _objects?.Get(selfGuid)?.Properties
|
||||
.GetInt((uint)PropertyInt.HousePurchaseTimestamp) ?? 0;
|
||||
long nowEpochSeconds = _timeProvider.GetUtcNow().ToUnixTimeSeconds();
|
||||
|
|
@ -198,20 +221,18 @@ public sealed class RuntimeHouseState
|
|||
timestamp + PurchaseWaitPeriodSeconds);
|
||||
DateTime expiryLocal = TimeZoneInfo.ConvertTime(
|
||||
expiryUtc, _timeProvider.LocalTimeZone).DateTime;
|
||||
_lines = new[]
|
||||
{
|
||||
lines.Add(
|
||||
"You may buy another landscape house at "
|
||||
+ expiryLocal.ToString(CultureInfo.CurrentCulture)
|
||||
+ ". This restriction does not apply to apartments.",
|
||||
};
|
||||
return;
|
||||
+ expiryLocal.ToString(CultureInfo.CurrentCulture)
|
||||
+ ". This restriction does not apply to apartments.");
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add(_ownsHouse
|
||||
? "You may buy another house immediately after you abandon this one."
|
||||
: "You may buy another house immediately.");
|
||||
}
|
||||
|
||||
_lines = new[]
|
||||
{
|
||||
_ownsHouse
|
||||
? "You may buy another house immediately after you abandon this one."
|
||||
: "You may buy another house immediately.",
|
||||
};
|
||||
_lines = lines;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue