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
|
|
@ -75,12 +75,17 @@ public sealed class RuntimeHouseStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseStatus_TimestampWithinThirtyDayWindow_RendersNoLine()
|
||||
public void HouseStatus_TimestampWithinThirtyDayWindow_ShowsExpiryDateLine()
|
||||
{
|
||||
// HouseSystem::HasPurchaseWaitPeriodExpired: (now - timestamp) >
|
||||
// 0x278d00 (2,592,000 s = 30 days). Inside the window, retail takes
|
||||
// the strftime-formatted branch this session leaves unported
|
||||
// (ISSUES #413 item 2) — must render nothing, not a guess.
|
||||
// 0x278d00 (2,592,000 s = 30 days). Inside the window, retail's
|
||||
// gmHouseUI::DisplayPurchaseTimeText composes prefix + strftime("%c")
|
||||
// of (timestamp + 30 days) + suffix (night-round review F8 — the
|
||||
// "unrecoverable strftime branch" from ISSUES #413 item 2 was
|
||||
// byte-decoded and is now ported). The exact date substring is
|
||||
// locale/timezone-formatted (.NET's honest %c analogue), so this
|
||||
// pins the STRUCTURE (prefix/suffix, non-empty middle), not the
|
||||
// exact rendered date text.
|
||||
var clock = new ManualTimeProvider();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Self, Type = ItemType.Creature });
|
||||
|
|
@ -93,7 +98,34 @@ public sealed class RuntimeHouseStateTests
|
|||
clock.Advance(TimeSpan.FromDays(29));
|
||||
house.ApplyHouseStatus(weenieError: 0u, Self);
|
||||
|
||||
Assert.Empty(house.Lines);
|
||||
string line = Assert.Single(house.Lines);
|
||||
Assert.StartsWith("You may buy another landscape house at ", line);
|
||||
Assert.EndsWith(". This restriction does not apply to apartments.", line);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseStatus_TimestampWithinThirtyDayWindow_ExpiryDateIsTimestampPlusThirtyDays()
|
||||
{
|
||||
// Pins the actual computed expiry moment (retail: timestamp +
|
||||
// 0x278d00 = 2,592,000 s, formatted via localtime — this fixture's
|
||||
// LocalTimeZone is UTC, so the rendered date is exactly the UTC
|
||||
// expiry instant with no offset ambiguity).
|
||||
var clock = new ManualTimeProvider();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = Self, Type = ItemType.Creature });
|
||||
DateTimeOffset purchaseTime = clock.GetUtcNow();
|
||||
var bundle = new PropertyBundle();
|
||||
bundle.Ints[(uint)PropertyInt.HousePurchaseTimestamp] =
|
||||
(int)purchaseTime.ToUnixTimeSeconds();
|
||||
objects.UpsertProperties(Self, bundle);
|
||||
var house = new RuntimeHouseState(objects, clock);
|
||||
|
||||
clock.Advance(TimeSpan.FromDays(29));
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -156,6 +188,12 @@ public sealed class RuntimeHouseStateTests
|
|||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
// F8: pin LocalTimeZone to UTC so the not-yet-expired branch's
|
||||
// TimeZoneInfo.ConvertTime call is deterministic across machines —
|
||||
// the real production TimeProvider.System.LocalTimeZone is
|
||||
// TimeZoneInfo.Local, matching retail's own localtime() call.
|
||||
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
|
||||
|
||||
public void Advance(TimeSpan elapsed) => _now += elapsed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue