fix(world): DerethDateTime tick-0 offset — sky was 7/16 of a day wrong
User observed: 'time is flipped — supposed to be day/evening, but shows night/morning.' That's a ~half-day offset. Root cause in ACE DerethDateTime.cs line 23: private const double dayZeroTicks = 0; // Morningthaw 1, 10 P.Y. - Morntide-and-Half ACE anchors tick 0 to Morntide-and-Half (slot 7 on the 0-indexed 16-slot scale) — NOT Darktide (slot 0 = midnight) as our DayFraction function assumed. Confirmed by DerethDateTime.cs:145: private int hour = (int)Hours.Morntide_and_Half; Fix: shift DayFraction by +7/16 * DayTicks (3333.75) so tick 0 maps to its real calendar slot. Exposed as DayFractionOriginOffsetTicks constant for documentation + downstream referencing. Effect on sun: previously, server tick ~0 (just-booted ACE) produced dayFraction 0 → midnight sky → night colors at noon real-time. Now dayFraction 7/16 = 0.4375 → late morning sky → noon-ish colors within 1/16 of a day, which matches what a user actually sees when launching during daytime. Tests updated for the corrected convention: - DerethDateTime.DayFraction(0) = 7/16 (not 0). - CurrentHour(0) = MorntideAndHalf (not Darktide). - IsDaytime(0) = true. - Midnight (Darktide, slot 0) is 9/16 of a day past tick 0. - SkyState + WorldTimeDebug tests retargeted to the new frame. Build green, 711 tests pass. Ref: references/ACE/Source/ACE.Common/DerethDateTime.cs:23-25 + :145. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
187226f504
commit
bd184e1afd
4 changed files with 84 additions and 33 deletions
|
|
@ -98,14 +98,38 @@ public static class DerethDateTime
|
||||||
Frostfell,
|
Frostfell,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In ACE's calendar, <c>tick 0</c> is defined as "Morningthaw 1,
|
||||||
|
/// 10 P.Y. — Morntide-and-Half", NOT midnight
|
||||||
|
/// (<c>references/ACE/Source/ACE.Common/DerethDateTime.cs:23</c>,
|
||||||
|
/// <c>private const double dayZeroTicks = 0; // Morntide-and-Half</c>
|
||||||
|
/// + <c>private int hour = (int)Hours.Morntide_and_Half;</c>).
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// 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
|
||||||
|
/// <c>dayOneTicks = 0 + 210 + (hourTicks * 8) // Morningthaw 2, Darktide</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public const double DayFractionOriginOffsetTicks = (7.0 / 16.0) * DayTicks; // 3333.75
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Day fraction [0, 1): 0 = Darktide (midnight), 0.5 =
|
/// 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
|
||||||
|
/// <see cref="DayFractionOriginOffsetTicks"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DayFraction(double ticks)
|
public static double DayFraction(double ticks)
|
||||||
{
|
{
|
||||||
if (ticks < 0) ticks = 0;
|
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;
|
return rem / DayTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,58 +5,72 @@ namespace AcDream.Core.Tests.World;
|
||||||
|
|
||||||
public sealed class DerethDateTimeTests
|
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]
|
[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]
|
[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]
|
[Fact]
|
||||||
public void DayFraction_WrapsAfterOneDay()
|
public void DayFraction_WrapsAfterOneDay()
|
||||||
{
|
{
|
||||||
Assert.Equal(0.0, DerethDateTime.DayFraction(DerethDateTime.DayTicks), 6);
|
// Full day from tick 0 returns to the same slot (Morntide-and-Half = 7/16).
|
||||||
Assert.Equal(0.25, DerethDateTime.DayFraction(DerethDateTime.DayTicks * 1.25), 4);
|
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]
|
[Fact]
|
||||||
public void CurrentHour_AtMidnight_IsDarktide()
|
public void CurrentHour_AtMidnight_IsDarktide()
|
||||||
{
|
{
|
||||||
Assert.Equal(DerethDateTime.HourName.Darktide, DerethDateTime.CurrentHour(0));
|
// 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
|
||||||
[Fact]
|
// = 4286.25 ticks (slight difference because ACE snaps to quarter hours; our
|
||||||
public void CurrentHour_AtDawn_IsDawnsong()
|
// continuous formula is smoother). We verify slot 0 (Darktide) at 9/16 of a day
|
||||||
{
|
// past tick 0.
|
||||||
// Dawnsong is the 5th slot (0-indexed slot 4) — day-fraction 4/16 = 0.25.
|
double ticks = DerethDateTime.DayTicks * (9.0 / 16.0);
|
||||||
double ticks = DerethDateTime.DayTicks * (4.0 / 16.0);
|
Assert.Equal(DerethDateTime.HourName.Darktide, DerethDateTime.CurrentHour(ticks));
|
||||||
Assert.Equal(DerethDateTime.HourName.Dawnsong, DerethDateTime.CurrentHour(ticks));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CurrentHour_AtNoon_IsMidsong()
|
public void CurrentHour_AtNoon_IsMidsong()
|
||||||
{
|
{
|
||||||
// Midsong is 0-indexed slot 8 → day-fraction 0.5.
|
// Midsong is slot 8 on the 16-slot scale. From tick 0 (slot 7) advance by 1 slot.
|
||||||
double ticks = DerethDateTime.DayTicks * 0.5;
|
double ticks = DerethDateTime.DayTicks * (1.0 / 16.0);
|
||||||
Assert.Equal(DerethDateTime.HourName.Midsong, DerethDateTime.CurrentHour(ticks));
|
Assert.Equal(DerethDateTime.HourName.Midsong, DerethDateTime.CurrentHour(ticks));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsDaytime_Dawn_True()
|
public void IsDaytime_Tick0_True()
|
||||||
{
|
{
|
||||||
double ticks = DerethDateTime.DayTicks * (4.0 / 16.0); // Dawnsong start
|
// Morntide-and-Half (slot 7) falls in the daytime band (slots 4..11).
|
||||||
Assert.True(DerethDateTime.IsDaytime(ticks));
|
Assert.True(DerethDateTime.IsDaytime(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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));
|
Assert.False(DerethDateTime.IsDaytime(ticks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,12 @@ public sealed class SkyStateTests
|
||||||
public void WorldTimeService_DayFraction_RespectsSync()
|
public void WorldTimeService_DayFraction_RespectsSync()
|
||||||
{
|
{
|
||||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||||
// Sync to exactly noon of day 0.
|
// Need to aim for dayFraction 0.5 (Gloaming-and-Half, slot 15 since tick 0 = slot 7).
|
||||||
service.SyncFromServer(DerethDateTime.DayTicks * 0.5);
|
// 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);
|
Assert.True(service.IsDaytime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,32 @@ public sealed class WorldTimeDebugTests
|
||||||
public void SetDebugTime_OverridesDayFraction()
|
public void SetDebugTime_OverridesDayFraction()
|
||||||
{
|
{
|
||||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
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);
|
Assert.InRange(service.DayFraction, 0.499, 0.501);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClearDebugTime_RestoresServerTime()
|
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());
|
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||||
service.SyncFromServer(DerethDateTime.DayTicks * 0.25); // dawn
|
service.SyncFromServer(syncTick);
|
||||||
service.SetDebugTime(0.5f);
|
service.SetDebugTime(0.5f);
|
||||||
service.ClearDebugTime();
|
service.ClearDebugTime();
|
||||||
|
|
||||||
Assert.InRange(service.DayFraction, 0.24, 0.26);
|
Assert.InRange(service.DayFraction, targetFraction - 0.01, targetFraction + 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -32,9 +43,9 @@ public sealed class WorldTimeDebugTests
|
||||||
{
|
{
|
||||||
var service = new WorldTimeService(SkyStateProvider.Default());
|
var service = new WorldTimeService(SkyStateProvider.Default());
|
||||||
service.SetDebugTime(0.75f);
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue