diff --git a/src/AcDream.Core/World/DerethDateTime.cs b/src/AcDream.Core/World/DerethDateTime.cs
index 1d94303..2d5ecf4 100644
--- a/src/AcDream.Core/World/DerethDateTime.cs
+++ b/src/AcDream.Core/World/DerethDateTime.cs
@@ -98,14 +98,38 @@ public static class DerethDateTime
Frostfell,
}
+ ///
+ /// In ACE's calendar, tick 0 is defined as "Morningthaw 1,
+ /// 10 P.Y. — Morntide-and-Half", NOT midnight
+ /// (references/ACE/Source/ACE.Common/DerethDateTime.cs:23,
+ /// private const double dayZeroTicks = 0; // Morntide-and-Half
+ /// + private int hour = (int)Hours.Morntide_and_Half;).
+ ///
+ ///
+ /// Morntide-and-Half is slot 7 on the 16-slot (0-indexed) scale,
+ /// i.e. late morning just before noon. Without this offset, a server
+ /// that just booted would have tick ≈ 0 and our sky renderer would
+ /// believe it's midnight — visibly wrong by roughly half a day.
+ /// Applying +7/16 of a full day (3333.75 ticks) in the day-fraction
+ /// computation re-aligns tick 0 to its real calendar slot, putting
+ /// noon at tick +476.25 / 2 (approximately) and Darktide at
+ /// tick +4286.25, which matches ACE's
+ /// dayOneTicks = 0 + 210 + (hourTicks * 8) // Morningthaw 2, Darktide.
+ ///
+ ///
+ public const double DayFractionOriginOffsetTicks = (7.0 / 16.0) * DayTicks; // 3333.75
+
///
/// Day fraction [0, 1): 0 = Darktide (midnight), 0.5 =
- /// Midsong-and-Half (noon-ish), 1.0 wraps to 0.
+ /// Midsong-and-Half (noon-ish), 1.0 wraps to 0. Corrected for the
+ /// ACE tick-0 = Morntide-and-Half convention (see
+ /// ).
///
public static double DayFraction(double ticks)
{
if (ticks < 0) ticks = 0;
- double rem = ticks - Math.Floor(ticks / DayTicks) * DayTicks;
+ double shifted = ticks + DayFractionOriginOffsetTicks;
+ double rem = shifted - Math.Floor(shifted / DayTicks) * DayTicks;
return rem / DayTicks;
}
diff --git a/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs b/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs
index 09ec90a..19d44ef 100644
--- a/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs
+++ b/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs
@@ -5,58 +5,72 @@ 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_AtMidnight_IsZero()
+ public void DayFraction_AtTick0_IsMorntideAndHalf()
{
- Assert.Equal(0.0, DerethDateTime.DayFraction(0));
+ // 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_AtHalfDay_IsHalf()
+ public void DayFraction_AtHalfDayFromTick0_IsHalf()
{
- Assert.Equal(0.5, DerethDateTime.DayFraction(DerethDateTime.DayTicks / 2), 4);
+ // 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()
{
- Assert.Equal(0.0, DerethDateTime.DayFraction(DerethDateTime.DayTicks), 6);
- Assert.Equal(0.25, DerethDateTime.DayFraction(DerethDateTime.DayTicks * 1.25), 4);
+ // 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()
{
- Assert.Equal(DerethDateTime.HourName.Darktide, DerethDateTime.CurrentHour(0));
- }
-
- [Fact]
- public void CurrentHour_AtDawn_IsDawnsong()
- {
- // Dawnsong is the 5th slot (0-indexed slot 4) — day-fraction 4/16 = 0.25.
- double ticks = DerethDateTime.DayTicks * (4.0 / 16.0);
- Assert.Equal(DerethDateTime.HourName.Dawnsong, DerethDateTime.CurrentHour(ticks));
+ // 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 0-indexed slot 8 → day-fraction 0.5.
- double ticks = DerethDateTime.DayTicks * 0.5;
+ // 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_Dawn_True()
+ public void IsDaytime_Tick0_True()
{
- double ticks = DerethDateTime.DayTicks * (4.0 / 16.0); // Dawnsong start
- Assert.True(DerethDateTime.IsDaytime(ticks));
+ // Morntide-and-Half (slot 7) falls in the daytime band (slots 4..11).
+ Assert.True(DerethDateTime.IsDaytime(0));
}
[Fact]
- public void IsDaytime_Night_False()
+ public void IsDaytime_Darktide_False()
{
- double ticks = DerethDateTime.DayTicks * (1.0 / 16.0); // Darktide-and-Half
+ // 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));
}
diff --git a/tests/AcDream.Core.Tests/World/SkyStateTests.cs b/tests/AcDream.Core.Tests/World/SkyStateTests.cs
index 74a9127..272bdc5 100644
--- a/tests/AcDream.Core.Tests/World/SkyStateTests.cs
+++ b/tests/AcDream.Core.Tests/World/SkyStateTests.cs
@@ -80,10 +80,12 @@ public sealed class SkyStateTests
public void WorldTimeService_DayFraction_RespectsSync()
{
var service = new WorldTimeService(SkyStateProvider.Default());
- // Sync to exactly noon of day 0.
- service.SyncFromServer(DerethDateTime.DayTicks * 0.5);
+ // Need to aim for dayFraction 0.5 (Gloaming-and-Half, slot 15 since tick 0 = slot 7).
+ // Sync to (0.5 - 7/16) * DayTicks = (1/16) * DayTicks — 1 slot past Morntide-and-Half = Midsong.
+ // Actually simpler: target fraction 7/16 (slot 7 = Morntide-and-Half) by syncing to tick 0.
+ service.SyncFromServer(0);
- Assert.InRange(service.DayFraction, 0.499, 0.501);
+ Assert.InRange(service.DayFraction, 0.43, 0.44); // 7/16 = 0.4375
Assert.True(service.IsDaytime);
}
}
diff --git a/tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs b/tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs
index c0d5df7..b1d6c24 100644
--- a/tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs
+++ b/tests/AcDream.Core.Tests/World/WorldTimeDebugTests.cs
@@ -10,21 +10,32 @@ public sealed class WorldTimeDebugTests
public void SetDebugTime_OverridesDayFraction()
{
var service = new WorldTimeService(SkyStateProvider.Default());
- service.SyncFromServer(0); // midnight
+ service.SyncFromServer(0); // server tick 0 (= Morntide-and-Half)
- service.SetDebugTime(0.5f); // force noon
+ service.SetDebugTime(0.5f); // force noon (Midsong-and-Half)
Assert.InRange(service.DayFraction, 0.499, 0.501);
}
[Fact]
public void ClearDebugTime_RestoresServerTime()
{
+ // Post tick-0-offset fix: DayFraction(tick) = ((tick + 7/16 * DayTicks) % DayTicks) / DayTicks.
+ // Pick a server tick whose real-world meaning is straightforward to verify.
+ // Sync to (0.25 - 7/16) * DayTicks negative means "3 slots before midnight
+ // past Morntide-and-Half", which in positive terms is 13/16 of the day
+ // past Morntide-and-Half, but simpler: sync to "1/16 past midnight" =
+ // ticks giving fraction 1/16. Required tick offset from 0 to land at
+ // fraction 1/16: solve (t + 7/16*D) mod D = 1/16*D
+ // → t = (1/16 - 7/16) * D mod D = -6/16 * D mod D = 10/16 * D.
+ double targetFraction = 1.0 / 16.0; // Darktide-and-Half
+ double syncTick = (targetFraction - (7.0 / 16.0) + 1.0) * DerethDateTime.DayTicks;
+
var service = new WorldTimeService(SkyStateProvider.Default());
- service.SyncFromServer(DerethDateTime.DayTicks * 0.25); // dawn
+ service.SyncFromServer(syncTick);
service.SetDebugTime(0.5f);
service.ClearDebugTime();
- Assert.InRange(service.DayFraction, 0.24, 0.26);
+ Assert.InRange(service.DayFraction, targetFraction - 0.01, targetFraction + 0.01);
}
[Fact]
@@ -32,9 +43,9 @@ public sealed class WorldTimeDebugTests
{
var service = new WorldTimeService(SkyStateProvider.Default());
service.SetDebugTime(0.75f);
- service.SyncFromServer(0); // midnight — this should clear the override
+ service.SyncFromServer(0); // tick 0 = Morntide-and-Half → fraction 7/16
- Assert.InRange(service.DayFraction, 0.0, 0.01);
+ Assert.InRange(service.DayFraction, 7.0 / 16.0 - 0.01, 7.0 / 16.0 + 0.01);
}
[Fact]