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
|
|
@ -24,12 +24,14 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## #413 — House tab shows no content (owned-house display, six Display* line builders unported)
|
||||
## #413 — House tab shows no content (owned-house display, Display* line builders unported)
|
||||
|
||||
**Status:** NARROWED 2026-08-17 (House-tab ownership-text closer session);
|
||||
item 2's not-expired branch closed same-day at the night-round review fix
|
||||
round (F8). Items 1 and 2 below are DONE; item 3 (six owned-house-only
|
||||
builders) remains OPEN and is the entire remaining scope.
|
||||
round (F8); **the houseless case CORRECTED same-day at the morning gate
|
||||
round (user finding 2 — see the correction note inside item 2)**. Items 1
|
||||
and 2 below are DONE; item 3 (owned-house-only builder content) remains
|
||||
OPEN and is the entire remaining scope.
|
||||
|
||||
**What's shipped (this session, on top of Batch C's mount + parser
|
||||
groundwork).**
|
||||
|
|
@ -71,23 +73,34 @@ groundwork).**
|
|||
locale, full date+time" intent — filed as register row IA-23, an
|
||||
approximation, not a gap).
|
||||
|
||||
**Corrects a framing this session's task brief carried in from outside
|
||||
this doc**: the brief described retail as ALSO showing a preceding line
|
||||
"You do not currently own a house." No such string, in that or any close
|
||||
wording, exists anywhere in the 2013 EoR `acclient_2013_pseudo_c.txt`
|
||||
dump, in any `gmHouseUI`/`gmMapUI` method, in ACE's `GameEventHouseStatus`
|
||||
writer, or in the live-DAT House ListBox/page (re-confirmed empty this
|
||||
session — `MapHousePanelSlotProbeTests`, zero rows, zero sibling
|
||||
content). The closest strings found are UNRELATED generic command-error
|
||||
chat text ("You do not own a house!", WeenieError `0x45E`/`0x45F`; "You
|
||||
must own a house to use this command.", WeenieError `0x47F`), routed
|
||||
through the GENERIC WeenieError-to-chat dispatcher, never through
|
||||
`gmHouseUI`. This ISSUES entry's OWN pre-existing "Acceptance test once
|
||||
closed" line below (written before this session, by the same research
|
||||
pass that produced the recon doc) already named the single-line
|
||||
"You may buy another house immediately." text as the target — this
|
||||
session's mechanism derivation independently reached the same
|
||||
conclusion and is now the shipped, tested behavior.
|
||||
**CORRECTION (2026-08-17 morning gate round, user finding 2 — the
|
||||
paragraph this replaces was WRONG):** the earlier session had refuted
|
||||
the preceding line "You do not currently own a house." with the claim
|
||||
that "no such string exists anywhere in the 2013 EoR
|
||||
`acclient_2013_pseudo_c.txt` dump" — but the user's retail screenshot
|
||||
(their live client, houseless character) shows exactly that line ABOVE
|
||||
"You may buy another house immediately.", and the user's side-by-side
|
||||
retail reports are axioms. The re-derivation found the string DOES
|
||||
exist in the 2013 binary, at `data_7ab688`: it is
|
||||
`gmHouseUI::DisplayBuyPayment @0x004a2b30`'s HOUSELESS branch. Two
|
||||
compounding misreads hid it: (a) `DisplayBuyPayment` was mislabeled
|
||||
houseless-silent — its `m_pHouseData` gate only selects WHICH text
|
||||
(`jne 0x4a2b63`); the ListBox emit (`AddItemFromTemplateList` +
|
||||
`SetTextWithFont`, `@0x004a2b80` onward) runs in BOTH branches; and
|
||||
(b) the pseudo-C dump renders both `push <literal>` operands as
|
||||
spurious `&gmHouseUI::vftable'.RecvNotice_*` symbol matches (the same
|
||||
BN artifact class TS-85/F3 documented), so a TEXT sweep of the dump
|
||||
finds no house strings there — the raw string pool has them
|
||||
(byte-decoded via capstone this round: houseless `@0x004a2b57` `push
|
||||
0x7ab688` = "You do not currently own a house."; owned `@0x004a2b63`
|
||||
`push 0x7ab65c` = "The purchase price for this dwelling is:\n" +
|
||||
`HousePaymentList::ComposeText`, still item-3 scope). The morning-gate
|
||||
task brief's alternate hypothesis (a DAT string-table id) was also
|
||||
checked and is NOT the mechanism — it is a plain exe string-pool
|
||||
literal, same as every other gmHouseUI line. `RuntimeHouseState.
|
||||
Recompute` now renders the houseless case as retail's exact TWO lines
|
||||
in builder order: "You do not currently own a house." then the
|
||||
purchase-time line; `RuntimeHouseStateTests` updated to pin both.
|
||||
|
||||
**Also fixed in the same pass: `HousePageController.Bind` never wired
|
||||
`UiTemplateListBox.TemplateResolver`.** Without it,
|
||||
|
|
@ -101,10 +114,11 @@ groundwork).**
|
|||
|
||||
**What remains open — item 3, the entire surviving scope:**
|
||||
|
||||
3. **The six owned-house-only `Display*` line builders** (`DisplayBuyPayment`,
|
||||
`DisplayRentPayment`, `DisplayBuyTime`, `DisplayRentTimes`,
|
||||
`DisplayLocation`, `DisplayWarningText` — all called from
|
||||
`gmHouseUI::DisplayHouseData @0x004a3380`). Each is dozens-to-a-few-hundred
|
||||
3. **The owned-house-only `Display*` line content** (`DisplayBuyPayment`'s
|
||||
OWNED branch — its houseless branch shipped at the 2026-08-17 morning
|
||||
gate correction above — plus `DisplayRentPayment`, `DisplayBuyTime`,
|
||||
`DisplayRentTimes`, `DisplayLocation`, `DisplayWarningText`, all called
|
||||
from `gmHouseUI::DisplayHouseData @0x004a3380`). Each is dozens-to-a-few-hundred
|
||||
lines of heavily FPU/string-mangled BN pseudo-C (PStringBase sprintf
|
||||
chains, `HousePaymentList` iteration, `IsPaidInFull`/
|
||||
`ConstructRentWarningMessage`-style formatting) — genuinely sized as its
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ using AcDream.Runtime.Gameplay;
|
|||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// House-tab conformance (Batch C, 2026-08-17): the ONE decomp-verified
|
||||
/// line <c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c> emits for a
|
||||
/// houseless/fresh character, and the wait-period-not-expired case that
|
||||
/// stays empty (unrecoverable strftime format, ISSUES #413 item 2).
|
||||
/// House-tab conformance (Batch C, 2026-08-17; two-line correction at the
|
||||
/// same-day morning gate round): the houseless case renders retail's exact
|
||||
/// TWO lines in builder order — <c>gmHouseUI::DisplayBuyPayment
|
||||
/// @0x004a2b30</c>'s houseless branch ("You do not currently own a
|
||||
/// house.", byte-decoded <c>data_7ab688</c> — the m_pHouseData gate only
|
||||
/// selects WHICH text; the emit is unconditional) followed by
|
||||
/// <c>DisplayPurchaseTimeText @0x004a3110</c>'s wait-period line.
|
||||
/// </summary>
|
||||
public sealed class RuntimeHouseStateTests
|
||||
{
|
||||
|
|
@ -28,7 +31,7 @@ public sealed class RuntimeHouseStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseStatus_FreshCharacterWithNoTimestamp_ShowsBuyImmediatelyLine()
|
||||
public void HouseStatus_FreshCharacterWithNoTimestamp_ShowsBothHouselessLines()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Self, Type = ItemType.Creature });
|
||||
|
|
@ -40,7 +43,16 @@ public sealed class RuntimeHouseStateTests
|
|||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
Assert.True(house.HasReceivedNotice);
|
||||
Assert.Equal(["You may buy another house immediately."], house.Lines);
|
||||
// 2026-08-17 morning gate finding 2 (user retail screenshot):
|
||||
// BOTH lines, in gmHouseUI::Update's fixed builder order —
|
||||
// DisplayBuyPayment's houseless literal first, then
|
||||
// DisplayPurchaseTimeText's expired-branch literal.
|
||||
Assert.Equal(
|
||||
[
|
||||
"You do not currently own a house.",
|
||||
"You may buy another house immediately.",
|
||||
],
|
||||
house.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -98,7 +110,11 @@ public sealed class RuntimeHouseStateTests
|
|||
clock.Advance(TimeSpan.FromDays(29));
|
||||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
string line = Assert.Single(house.Lines);
|
||||
// Houseless -> DisplayBuyPayment's line precedes the purchase-time
|
||||
// line; the not-expired branch supplies the second.
|
||||
Assert.Equal(2, house.Lines.Count);
|
||||
Assert.Equal("You do not currently own a house.", house.Lines[0]);
|
||||
string line = house.Lines[1];
|
||||
Assert.StartsWith("You may buy another landscape house at ", line);
|
||||
Assert.EndsWith(". This restriction does not apply to apartments.", line);
|
||||
}
|
||||
|
|
@ -124,12 +140,14 @@ public sealed class RuntimeHouseStateTests
|
|||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
DateTime expectedExpiry = purchaseTime.AddSeconds(0x278d00).UtcDateTime;
|
||||
string line = Assert.Single(house.Lines);
|
||||
Assert.Contains(expectedExpiry.ToString(System.Globalization.CultureInfo.CurrentCulture), line);
|
||||
Assert.Equal(2, house.Lines.Count);
|
||||
Assert.Contains(
|
||||
expectedExpiry.ToString(System.Globalization.CultureInfo.CurrentCulture),
|
||||
house.Lines[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseStatus_TimestampPastThirtyDayWindow_ShowsBuyImmediatelyLine()
|
||||
public void HouseStatus_TimestampPastThirtyDayWindow_ShowsBothHouselessLines()
|
||||
{
|
||||
var clock = new ManualTimeProvider();
|
||||
var objects = new ClientObjectTable();
|
||||
|
|
@ -143,7 +161,12 @@ public sealed class RuntimeHouseStateTests
|
|||
clock.Advance(TimeSpan.FromDays(31));
|
||||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
Assert.Equal(["You may buy another house immediately."], house.Lines);
|
||||
Assert.Equal(
|
||||
[
|
||||
"You do not currently own a house.",
|
||||
"You may buy another house immediately.",
|
||||
],
|
||||
house.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -170,7 +193,12 @@ public sealed class RuntimeHouseStateTests
|
|||
|
||||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
Assert.Equal(["You may buy another house immediately."], house.Lines);
|
||||
Assert.Equal(
|
||||
[
|
||||
"You do not currently own a house.",
|
||||
"You may buy another house immediately.",
|
||||
],
|
||||
house.Lines);
|
||||
}
|
||||
|
||||
private static GameEvents.HouseData SampleHouseData() => new(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue