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:
parent
449e9c3540
commit
dbe6690a4e
3 changed files with 89 additions and 31 deletions
|
|
@ -4565,9 +4565,18 @@ public sealed class GameWindow : IDisposable
|
||||||
int entityCount = _worldState.Entities.Count;
|
int entityCount = _worldState.Entities.Count;
|
||||||
int animatedCount = _animatedEntities.Count;
|
int animatedCount = _animatedEntities.Count;
|
||||||
|
|
||||||
_window!.Title = $"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | " +
|
// Calendar display matches retail's @timestamp output:
|
||||||
$"lb {visibleLandblocks}/{totalLandblocks} visible | " +
|
// "Date: <Month> <Day>, PY <Year> Time: <HourName>".
|
||||||
$"ent {entityCount} | anim {animatedCount}";
|
// 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;
|
_lastFps = fps;
|
||||||
_lastFrameMs = avgFrameTime;
|
_lastFrameMs = avgFrameTime;
|
||||||
_perfAccum = 0;
|
_perfAccum = 0;
|
||||||
|
|
|
||||||
|
|
@ -90,21 +90,30 @@ public static class DerethDateTime
|
||||||
GloamingAndHalf,
|
GloamingAndHalf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Derethian months (Snowreap..Frostfell, 12 total).</summary>
|
/// <summary>
|
||||||
|
/// Derethian months in chronological order. Year-0 begins at month 0
|
||||||
|
/// (<see cref="Morningthaw"/>) and progresses through the 12-month
|
||||||
|
/// cycle. Names + order match retail's calendar display
|
||||||
|
/// (<c>GameTime::CalcDayBegin</c> + <c>GetDateTimeString</c> at
|
||||||
|
/// <c>0x005a6530</c>) and ACE's <c>DerethDateTime.cs</c>. Verified
|
||||||
|
/// against retail's <c>@timestamp</c> output in 2026-04-27 dual-
|
||||||
|
/// client comparison: at day-of-year 83, retail shows
|
||||||
|
/// "Seedsow 24" — that fixes month index 2 = Seedsow.
|
||||||
|
/// </summary>
|
||||||
public enum MonthName
|
public enum MonthName
|
||||||
{
|
{
|
||||||
Snowreap = 0,
|
Morningthaw = 0,
|
||||||
ColdMeet,
|
|
||||||
Leafdawning,
|
|
||||||
Seedsow,
|
|
||||||
Rosetide,
|
|
||||||
Solclaim,
|
Solclaim,
|
||||||
|
Seedsow,
|
||||||
|
Leafdawning,
|
||||||
|
Verdantine,
|
||||||
Thistledown,
|
Thistledown,
|
||||||
Harvestgain,
|
Harvestgain,
|
||||||
Leaftrue,
|
Leafcull,
|
||||||
Reaptide,
|
|
||||||
Morningthaw,
|
|
||||||
Frostfell,
|
Frostfell,
|
||||||
|
Snowreap,
|
||||||
|
Coldeve,
|
||||||
|
Wintersebb,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -127,12 +136,15 @@ public static class DerethDateTime
|
||||||
/// for the boot window before the dat parses.
|
/// for the boot window before the dat parses.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Live Dereth dat value: <c>3600</c>. The +7/16 default is wrong
|
/// Live Dereth dat value: <c>3600</c>. Retail's
|
||||||
/// by 266.25 ticks (~33 Derethian minutes) and was the source of
|
/// <c>GameTime::CalcDayBegin</c> at <c>0x005a6400</c> (decomp line
|
||||||
/// the "acdream time is behind retail" + "wrong DayGroup picked"
|
/// 434549) computes <c>arg2 + zero_time_of_year</c> as the basis for
|
||||||
/// observations in the 2026-04-23 live verification session — see
|
/// year/day-of-year extraction, then derives <c>time_of_day_begin</c>
|
||||||
/// <c>docs/research/2026-04-23-daygroup-selection.md</c> §4 and
|
/// such that <c>(arg2 - time_of_day_begin) / day_length</c> in
|
||||||
/// the Phase 3f commit.
|
/// <c>CalcTimeOfDay</c> gives <c>(arg2 + zero_time_of_year) mod day_length / day_length</c>.
|
||||||
|
/// 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.)
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double OriginOffsetTicks { get; private set; } = DayFractionOriginOffsetTicks;
|
public static double OriginOffsetTicks { get; private set; } = DayFractionOriginOffsetTicks;
|
||||||
|
|
@ -186,7 +198,10 @@ public static class DerethDateTime
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Derethian calendar breakdown: (year, month, day, hour).
|
/// Derethian calendar breakdown: (year, month, day, hour).
|
||||||
/// Year starts at PY 0. Day is 1-based within the month (1..30).
|
/// <see cref="Year"/> is the absolute Portal Year (= relative-year +
|
||||||
|
/// <see cref="ZeroYear"/>) so the value matches retail's
|
||||||
|
/// <c>@timestamp</c> output ("Date: <Month> <Day>,
|
||||||
|
/// <Year> P.Y."). Day is 1-based within the month (1..30).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct Calendar(int Year, MonthName Month, int Day, HourName Hour);
|
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;
|
if (ticks < 0) ticks = 0;
|
||||||
double shifted = ticks + OriginOffsetTicks;
|
double shifted = ticks + OriginOffsetTicks;
|
||||||
int year = (int)(shifted / YearTicks);
|
int relativeYear = (int)(shifted / YearTicks);
|
||||||
double tYear = shifted - year * YearTicks;
|
double tYear = shifted - relativeYear * YearTicks;
|
||||||
int monthIdx = (int)(tYear / MonthTicks);
|
int monthIdx = (int)(tYear / MonthTicks);
|
||||||
if (monthIdx > 11) monthIdx = 11;
|
if (monthIdx > 11) monthIdx = 11;
|
||||||
double tMonth = tYear - monthIdx * MonthTicks;
|
double tMonth = tYear - monthIdx * MonthTicks;
|
||||||
int day = (int)(tMonth / DayTicks) + 1;
|
int day = (int)(tMonth / DayTicks) + 1;
|
||||||
if (day > DaysInAMonth) day = DaysInAMonth;
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -75,26 +75,56 @@ public sealed class DerethDateTimeTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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);
|
var cal = DerethDateTime.ToCalendar(0);
|
||||||
Assert.Equal(0, cal.Year);
|
Assert.Equal(DerethDateTime.ZeroYear, cal.Year);
|
||||||
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
|
||||||
Assert.Equal(1, cal.Day);
|
Assert.Equal(1, cal.Day);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToCalendar_AdvancesCorrectly()
|
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);
|
var cal = DerethDateTime.ToCalendar(DerethDateTime.YearTicks);
|
||||||
Assert.Equal(1, cal.Year);
|
Assert.Equal(DerethDateTime.ZeroYear + 1, cal.Year);
|
||||||
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
|
||||||
Assert.Equal(1, cal.Day);
|
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);
|
var cal2 = DerethDateTime.ToCalendar(DerethDateTime.YearTicks + DerethDateTime.MonthTicks);
|
||||||
Assert.Equal(1, cal2.Year);
|
Assert.Equal(DerethDateTime.ZeroYear + 1, cal2.Year);
|
||||||
Assert.Equal(DerethDateTime.MonthName.ColdMeet, cal2.Month);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue