diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 66888b6..d0d1028 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -4565,9 +4565,18 @@ public sealed class GameWindow : IDisposable int entityCount = _worldState.Entities.Count; int animatedCount = _animatedEntities.Count; - _window!.Title = $"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | " + - $"lb {visibleLandblocks}/{totalLandblocks} visible | " + - $"ent {entityCount} | anim {animatedCount}"; + // Calendar display matches retail's @timestamp output: + // "Date: , PY Time: ". + // Use NowTicks (server-synced + wall-clock interpolation) so the + // user can read the same fields off both acdream and retail and + // confirm clock parity directly. Drift > 1 hour = real bug. + double tNow = WorldTime.NowTicks; + var titleCal = AcDream.Core.World.DerethDateTime.ToCalendar(tNow); + double df = WorldTime.DayFraction; + _window!.Title = + $"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | " + + $"lb {visibleLandblocks}/{totalLandblocks} | ent {entityCount}/anim {animatedCount} | " + + $"PY{titleCal.Year} {titleCal.Month} {titleCal.Day} {titleCal.Hour} (df={df:F4})"; _lastFps = fps; _lastFrameMs = avgFrameTime; _perfAccum = 0; diff --git a/src/AcDream.Core/World/DerethDateTime.cs b/src/AcDream.Core/World/DerethDateTime.cs index 592b868..a2e843d 100644 --- a/src/AcDream.Core/World/DerethDateTime.cs +++ b/src/AcDream.Core/World/DerethDateTime.cs @@ -90,21 +90,30 @@ public static class DerethDateTime GloamingAndHalf, } - /// Derethian months (Snowreap..Frostfell, 12 total). + /// + /// Derethian months in chronological order. Year-0 begins at month 0 + /// () and progresses through the 12-month + /// cycle. Names + order match retail's calendar display + /// (GameTime::CalcDayBegin + GetDateTimeString at + /// 0x005a6530) and ACE's DerethDateTime.cs. Verified + /// against retail's @timestamp output in 2026-04-27 dual- + /// client comparison: at day-of-year 83, retail shows + /// "Seedsow 24" — that fixes month index 2 = Seedsow. + /// public enum MonthName { - Snowreap = 0, - ColdMeet, - Leafdawning, - Seedsow, - Rosetide, + Morningthaw = 0, Solclaim, + Seedsow, + Leafdawning, + Verdantine, Thistledown, Harvestgain, - Leaftrue, - Reaptide, - Morningthaw, + Leafcull, Frostfell, + Snowreap, + Coldeve, + Wintersebb, } /// @@ -127,12 +136,15 @@ public static class DerethDateTime /// for the boot window before the dat parses. /// /// - /// Live Dereth dat value: 3600. The +7/16 default is wrong - /// by 266.25 ticks (~33 Derethian minutes) and was the source of - /// the "acdream time is behind retail" + "wrong DayGroup picked" - /// observations in the 2026-04-23 live verification session — see - /// docs/research/2026-04-23-daygroup-selection.md §4 and - /// the Phase 3f commit. + /// Live Dereth dat value: 3600. Retail's + /// GameTime::CalcDayBegin at 0x005a6400 (decomp line + /// 434549) computes arg2 + zero_time_of_year as the basis for + /// year/day-of-year extraction, then derives time_of_day_begin + /// such that (arg2 - time_of_day_begin) / day_length in + /// CalcTimeOfDay gives (arg2 + zero_time_of_year) mod day_length / day_length. + /// Net: the formula is ADD, not subtract — confirmed via the explicit + /// add at line 434549. (A 2026-04-26 attempt to flip the sign over- + /// corrected and broke DG selection; reverted in the same commit.) /// /// public static double OriginOffsetTicks { get; private set; } = DayFractionOriginOffsetTicks; @@ -186,7 +198,10 @@ public static class DerethDateTime /// /// Derethian calendar breakdown: (year, month, day, hour). - /// Year starts at PY 0. Day is 1-based within the month (1..30). + /// is the absolute Portal Year (= relative-year + + /// ) so the value matches retail's + /// @timestamp output ("Date: <Month> <Day>, + /// <Year> P.Y."). Day is 1-based within the month (1..30). /// public readonly record struct Calendar(int Year, MonthName Month, int Day, HourName Hour); @@ -194,15 +209,19 @@ public static class DerethDateTime { if (ticks < 0) ticks = 0; double shifted = ticks + OriginOffsetTicks; - int year = (int)(shifted / YearTicks); - double tYear = shifted - year * YearTicks; + int relativeYear = (int)(shifted / YearTicks); + double tYear = shifted - relativeYear * YearTicks; int monthIdx = (int)(tYear / MonthTicks); if (monthIdx > 11) monthIdx = 11; double tMonth = tYear - monthIdx * MonthTicks; int day = (int)(tMonth / DayTicks) + 1; if (day > DaysInAMonth) day = DaysInAMonth; - return new Calendar(year, (MonthName)monthIdx, day, CurrentHour(ticks)); + // Absolute Portal Year for display: retail's @timestamp shows + // PY-with-base (10 P.Y. == year 0 of the calendar epoch), so add + // ZeroYear here. Matches AbsoluteYear() and the retail decomp at + // FUN_005a7510:5300. + return new Calendar(relativeYear + ZeroYear, (MonthName)monthIdx, day, CurrentHour(ticks)); } /// diff --git a/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs b/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs index 19d44ef..86fb5a9 100644 --- a/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs +++ b/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs @@ -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: , 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); + } } }