fix(time): retail-canonical month enum + absolute Portal Year + title-bar calendar

Two bugs in calendar display (the CLOCK ITSELF was already correct):

1. **Month enum had wrong order + non-retail names.** Old enum:
   Snowreap=0, ColdMeet, Leafdawning, Seedsow, Rosetide, Solclaim, ...
   At day-of-year 83 this gave month index 2 = Leafdawning. Retail's
   @timestamp at the same moment shows "Seedsow 24". Fixed enum to
   chronological order starting at year-anchor month Morningthaw, with
   retail-canonical names:
     Morningthaw=0, Solclaim, Seedsow, Leafdawning, Verdantine,
     Thistledown, Harvestgain, Leafcull, Frostfell, Snowreap,
     Coldeve, Wintersebb.
   At day-of-year 83 → month 2 = Seedsow ✓

2. **ToCalendar returned relative year, not absolute Portal Year.**
   We had AbsoluteYear() = relative_year + ZeroYear (=10) but
   ToCalendar's Calendar.Year was the relative one. So acdream's
   title bar showed "PY 106" while retail's @timestamp at the same
   tick showed "PY 116". Fixed ToCalendar to add ZeroYear so the
   exposed Calendar.Year matches retail's display.

3. **GameWindow title bar now shows the calendar.** Format mirrors
   retail's @timestamp output:
     "PY<Year> <Month> <Day> <Hour> (df=<dayFraction>)"
   Lets the user read the same fields off both clients and confirm
   clock parity directly. Drift > 1 hour = real bug.

Tests:
- Updated ToCalendar_PY10Day1_Morningthaw (renamed from PY0Day1_Snowreap)
- Updated ToCalendar_AdvancesCorrectly (Snowreap→Morningthaw etc.)
- Added regression: ToCalendar_TickAtSeedsow24Year106_MatchesRetailFormat
  pinning a retail-known tick → retail-known calendar string.

The dayFraction formula (CalcDayBegin's `arg2 + zero_time_of_year`,
decomp 0x005a6400 line 434549) was already correct; an earlier-this-
session attempt to flip the sign was reverted in this same commit's
parent. The "few minutes drift" observed in dual-client comparisons
this session was a combination of:
  - calendar label mismatch (this fix addresses)
  - slot-boundary rounding (fixes itself)
  - 1-minute wall-clock interpolation drift (within tolerance)

NOT a clock-formula bug. ISSUE #3 in docs/ISSUES.md is now misnamed
("Client clock drifts from retail"); plan to re-title or close in a
follow-up commit after the visual-divergence investigation lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-27 14:43:49 +02:00
parent 449e9c3540
commit dbe6690a4e
3 changed files with 89 additions and 31 deletions

View file

@ -75,26 +75,56 @@ public sealed class DerethDateTimeTests
}
[Fact]
public void ToCalendar_PY0Day1_Snowreap()
public void ToCalendar_PY10Day1_Morningthaw()
{
// Tick 0 maps to PY 10 (= relative year 0 + ZeroYear=10),
// Morningthaw 1 — matches retail's calendar epoch
// (ACE DerethDateTime.cs: dayZeroTicks = 0; // Morningthaw 1, 10 P.Y.).
var cal = DerethDateTime.ToCalendar(0);
Assert.Equal(0, cal.Year);
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
Assert.Equal(DerethDateTime.ZeroYear, cal.Year);
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
Assert.Equal(1, cal.Day);
}
[Fact]
public void ToCalendar_AdvancesCorrectly()
{
// One year from start → PY 1, Snowreap 1.
// One year from start → PY (10 + 1) = 11, Morningthaw 1.
var cal = DerethDateTime.ToCalendar(DerethDateTime.YearTicks);
Assert.Equal(1, cal.Year);
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
Assert.Equal(DerethDateTime.ZeroYear + 1, cal.Year);
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
Assert.Equal(1, cal.Day);
// One month into year 1.
// One month into year 11 → Solclaim (next month after Morningthaw).
var cal2 = DerethDateTime.ToCalendar(DerethDateTime.YearTicks + DerethDateTime.MonthTicks);
Assert.Equal(1, cal2.Year);
Assert.Equal(DerethDateTime.MonthName.ColdMeet, cal2.Month);
Assert.Equal(DerethDateTime.ZeroYear + 1, cal2.Year);
Assert.Equal(DerethDateTime.MonthName.Solclaim, cal2.Month);
}
[Fact]
public void ToCalendar_TickAtSeedsow24Year106_MatchesRetailFormat()
{
// Regression guard for the 2026-04-27 dual-client comparison.
// Retail @timestamp output format is
// "Date: <Month> <Day>, <Year> P.Y."
// Pick a tick at the exact start of Seedsow 24 in relative year 106:
// shifted = 106 * YearTicks + 2 * MonthTicks + 23 * DayTicks
// Derived: 290,779,200 + 457,200 + 175,260 = 291,411,660. Subtract
// OriginOffsetTicks (3600 in Dereth dat) to get the input tick:
// 291,411,660 - 3600 = 291,408,060
// Expected output: PY 116 (= ZeroYear 10 + relative 106), Seedsow,
// day 24 1-indexed.
DerethDateTime.SetOriginOffsetFromDat(3600.0);
try
{
var cal = DerethDateTime.ToCalendar(291_408_060.0);
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
Assert.Equal(24, cal.Day);
}
finally
{
DerethDateTime.SetOriginOffsetFromDat(DerethDateTime.DayFractionOriginOffsetTicks);
}
}
}