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>
130 lines
5.1 KiB
C#
130 lines
5.1 KiB
C#
using AcDream.Core.World;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.World;
|
|
|
|
public sealed class DerethDateTimeTests
|
|
{
|
|
// ACE calendar anchor: tick 0 = Morningthaw 1, 10 P.Y. at Morntide-and-Half
|
|
// (slot 7 on the 0-indexed 16-slot scale). The DayFraction function
|
|
// applies +7/16 offset so "what time is it in the day" reads correctly.
|
|
|
|
[Fact]
|
|
public void DayFraction_AtTick0_IsMorntideAndHalf()
|
|
{
|
|
// Tick 0 = Morntide-and-Half = slot 7 = 7/16 of the day.
|
|
Assert.Equal(7.0 / 16.0, DerethDateTime.DayFraction(0), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void DayFraction_AtHalfDayFromTick0_IsHalf()
|
|
{
|
|
// From tick 0 (slot 7) + half a day = slot 7 + 8 = slot 15 (Gloaming-and-Half).
|
|
// Verify the formula advances by 0.5 correctly: (0 + DayTicks/2) → 7/16 + 1/2 = 15/16.
|
|
Assert.Equal(15.0 / 16.0, DerethDateTime.DayFraction(DerethDateTime.DayTicks / 2), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void DayFraction_WrapsAfterOneDay()
|
|
{
|
|
// Full day from tick 0 returns to the same slot (Morntide-and-Half = 7/16).
|
|
Assert.Equal(7.0 / 16.0, DerethDateTime.DayFraction(DerethDateTime.DayTicks), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtTick0_IsMorntideAndHalf()
|
|
{
|
|
// ACE ref: DerethDateTime.cs:145 — `hour = (int)Hours.Morntide_and_Half;`
|
|
Assert.Equal(DerethDateTime.HourName.MorntideAndHalf, DerethDateTime.CurrentHour(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtMidnight_IsDarktide()
|
|
{
|
|
// ACE ref: DerethDateTime.cs:25 — `dayOneTicks = 0 + hourOneTicks + (hourTicks * 8)
|
|
// // Morningthaw 2, 10 P.Y. - Darktide`. From tick 0 to Darktide next day = 4020.
|
|
// Using our slot-based formula: from slot 7 -> slot 0 (wrap) = 9 slots = 9 * 476.25
|
|
// = 4286.25 ticks (slight difference because ACE snaps to quarter hours; our
|
|
// continuous formula is smoother). We verify slot 0 (Darktide) at 9/16 of a day
|
|
// past tick 0.
|
|
double ticks = DerethDateTime.DayTicks * (9.0 / 16.0);
|
|
Assert.Equal(DerethDateTime.HourName.Darktide, DerethDateTime.CurrentHour(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtNoon_IsMidsong()
|
|
{
|
|
// Midsong is slot 8 on the 16-slot scale. From tick 0 (slot 7) advance by 1 slot.
|
|
double ticks = DerethDateTime.DayTicks * (1.0 / 16.0);
|
|
Assert.Equal(DerethDateTime.HourName.Midsong, DerethDateTime.CurrentHour(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsDaytime_Tick0_True()
|
|
{
|
|
// Morntide-and-Half (slot 7) falls in the daytime band (slots 4..11).
|
|
Assert.True(DerethDateTime.IsDaytime(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsDaytime_Darktide_False()
|
|
{
|
|
// Darktide = slot 0. Need tick offset of 9/16 from tick 0 to reach it.
|
|
double ticks = DerethDateTime.DayTicks * (9.0 / 16.0);
|
|
Assert.False(DerethDateTime.IsDaytime(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
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(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 (10 + 1) = 11, Morningthaw 1.
|
|
var cal = DerethDateTime.ToCalendar(DerethDateTime.YearTicks);
|
|
Assert.Equal(DerethDateTime.ZeroYear + 1, cal.Year);
|
|
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
|
|
Assert.Equal(1, cal.Day);
|
|
|
|
// One month into year 11 → Solclaim (next month after Morningthaw).
|
|
var cal2 = DerethDateTime.ToCalendar(DerethDateTime.YearTicks + DerethDateTime.MonthTicks);
|
|
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);
|
|
}
|
|
}
|
|
}
|