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]