fix(ui): night-round review — F8 House not-expired purchase-restriction text
gmHouseUI::DisplayPurchaseTimeText @0x004a3110's not-yet-expired
branch was wrongly marked "unrecoverable from this decomp dump" — a
direct capstone disassembly resolves all three concatenated pieces:
prefix "You may buy another landscape house at " @0x7ab790 (pushed
@0x004a3265), the strftime "%c" format literal @0x7ab7ec (pushed
@0x004a321d) applied to localtime(timestamp + 0x278d00) — the expiry
moment, 30 days after the purchase timestamp — and suffix ". This
restriction does not apply to apartments." @0x7ab7b8 (pushed
@0x004a3235).
Ported in RuntimeHouseState.Recompute, substituting .NET's
culture-default DateTime.ToString() for the CRT's strftime("%c", ...)
(different formatting engine, same "process locale, full date+time"
intent) — filed as register row IA-23 (an approximation, not a gap).
TimeProvider.LocalTimeZone (overridable, defaulting to
TimeZoneInfo.Local in production) keeps the conversion deterministically
testable while matching retail's own localtime() call.
Updated RuntimeHouseStateTests: the not-expired case now asserts the
composed prefix/suffix structure and the exact expiry instant (pinned
via a UTC-fixed test TimeProvider), replacing the old "renders nothing"
assertion. Un-claimed "unrecoverable" in ISSUES #413 item 2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ab84b54dfa
commit
df062d2eda
4 changed files with 110 additions and 22 deletions
|
|
@ -1,3 +1,4 @@
|
|||
using System.Globalization;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Properties;
|
||||
|
|
@ -61,10 +62,26 @@ namespace AcDream.Runtime.Gameplay;
|
|||
/// <see cref="ApplyHouseStatus"/>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The NOT-yet-expired branch of <c>DisplayPurchaseTimeText</c> (a
|
||||
/// <c>strftime</c>-formatted future date plus a BN-truncated suffix) is
|
||||
/// left unported per ISSUES #413 item 2's own scoping — its format string
|
||||
/// is unrecoverable from this decomp dump.
|
||||
/// The NOT-yet-expired branch of <c>DisplayPurchaseTimeText</c> is now
|
||||
/// ported (night-round review F8) — its literals were NOT unrecoverable;
|
||||
/// the "BN-truncated suffix" was another instance of Binary Ninja's
|
||||
/// operator-overload plumbing obscuring plain pushed string constants (the
|
||||
/// same artifact class TS-85/F3 hit). A direct capstone disassembly of the
|
||||
/// raw bytes resolves all three pieces retail concatenates: prefix
|
||||
/// <c>"You may buy another landscape house at "</c> @<c>0x7ab790</c>
|
||||
/// (pushed <c>@0x004a3265</c>), the <c>strftime</c> format literal
|
||||
/// <c>"%c"</c> @<c>0x7ab7ec</c> (pushed <c>@0x004a321d</c>) applied to
|
||||
/// <c>localtime(timestamp + 0x278d00)</c> — the expiry moment, not "now" —
|
||||
/// and suffix <c>". This restriction does not apply to apartments."</c>
|
||||
/// @<c>0x7ab7b8</c> (pushed <c>@0x004a3235</c>). <c>strftime</c>'s
|
||||
/// <c>"%c"</c> is the C runtime's locale-default full date+time
|
||||
/// representation; this port's honest analogue is .NET's own
|
||||
/// culture-default <c>DateTime.ToString()</c> (no explicit format) — NOT a
|
||||
/// byte-identical reproduction of the CRT's locale table, since .NET and
|
||||
/// the CRT do not share a formatting engine, but the same "whatever the
|
||||
/// process's own locale says" intent. Register row IA-23
|
||||
/// (<c>docs/architecture/retail-divergence-register.md</c>) records this
|
||||
/// approximation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RuntimeHouseState
|
||||
|
|
@ -152,8 +169,8 @@ public sealed class RuntimeHouseState
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary><c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c>'s
|
||||
/// expired branch, ported faithfully. Must hold <see cref="_gate"/>.</summary>
|
||||
/// <summary><c>gmHouseUI::DisplayPurchaseTimeText @0x004a3110</c>,
|
||||
/// both branches now ported. Must hold <see cref="_gate"/>.</summary>
|
||||
private void Recompute(uint selfGuid)
|
||||
{
|
||||
int timestamp = _objects?.Get(selfGuid)?.Properties
|
||||
|
|
@ -163,10 +180,30 @@ public sealed class RuntimeHouseState
|
|||
|
||||
if (!expired)
|
||||
{
|
||||
// Not-yet-expired branch: strftime-formatted future date + a
|
||||
// BN-truncated suffix, unrecoverable from this decomp dump.
|
||||
// ISSUES #413 item 2 — deferred, not guessed.
|
||||
_lines = Array.Empty<string>();
|
||||
// Not-yet-expired branch, byte-decoded (night-round review
|
||||
// F8): retail computes the EXPIRY moment (timestamp + 30 days,
|
||||
// @0x004a3212's `var_42c += 0x278d00`), formats it through
|
||||
// `localtime` + `strftime("%c", ...)` (@0x004a322c), and
|
||||
// concatenates prefix + date + suffix
|
||||
// (@0x004a3265/@0x004a321d/@0x004a3235). .NET's
|
||||
// culture-default DateTime.ToString() is the honest %c
|
||||
// analogue (see the class doc's own note — not byte-identical
|
||||
// to the CRT's locale table, same "process locale" intent).
|
||||
// TimeProvider.LocalTimeZone (not the ambient system zone
|
||||
// directly) keeps this deterministically testable while
|
||||
// matching retail's own `localtime()` (process-local time) in
|
||||
// production, where TimeProvider.System.LocalTimeZone IS
|
||||
// TimeZoneInfo.Local.
|
||||
DateTimeOffset expiryUtc = DateTimeOffset.FromUnixTimeSeconds(
|
||||
timestamp + PurchaseWaitPeriodSeconds);
|
||||
DateTime expiryLocal = TimeZoneInfo.ConvertTime(
|
||||
expiryUtc, _timeProvider.LocalTimeZone).DateTime;
|
||||
_lines = new[]
|
||||
{
|
||||
"You may buy another landscape house at "
|
||||
+ expiryLocal.ToString(CultureInfo.CurrentCulture)
|
||||
+ ". This restriction does not apply to apartments.",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue