diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 2fb25f6..2d35076 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -4316,21 +4316,26 @@ public sealed class GameWindow : IDisposable
if (_loadedSkyDesc is null || _loadedSkyDesc.DayGroups.Count == 0)
return;
- // Retail FUN_00501990 uses the full (Year, SecondsPerDay, DayOfYear)
- // triple — NOT a flat dayIndex. The SecondsPerDay multiplier is
- // load-bearing: without it, adjacent years map to adjacent LCG
- // seeds and convergence to acdream != retail would recur every
- // year. We use the retail calendar constants from DerethDateTime.
+ // Retail FUN_00501990 seeds the LCG with
+ // (Year_absolute, SecondsPerDay, DayOfYear)
+ // where Year_absolute = TimeOfDay+0x64 = floor(...) + baseYear
+ // (baseYear=10 for Dereth per GameTime.ZeroYear). Our port uses
+ // AbsoluteYear which includes the +10 offset; without it, our
+ // seed would differ from retail's by `10 × SecondsPerDay` and we'd
+ // pick a different DayGroup (verified mismatch in the 2026-04-23
+ // live session — acdream picked "Rainy"[17] while retail showed
+ // "Sunny" at PY 116 ColdMeet 17).
double ticks = WorldTime.NowTicks;
- int year = AcDream.Core.World.DerethDateTime.Year(ticks);
+ int absYear = AcDream.Core.World.DerethDateTime.AbsoluteYear(ticks);
int dayOfYear = AcDream.Core.World.DerethDateTime.DayOfYear(ticks);
int secondsPerDay = (int)AcDream.Core.World.DerethDateTime.DayTicks; // 7620
- // Compute a composite "dayIndex" for our own change-detection
- // and logging (doesn't feed into the roll itself).
- long dayIndex = (long)year * 360 + dayOfYear;
+ // Composite day key for change-detection and logging only; the
+ // LCG seed is computed inside SelectDayGroupIndex from (absYear,
+ // secondsPerDay, dayOfYear).
+ long dayIndex = (long)absYear * 360 + dayOfYear;
- int idx = _loadedSkyDesc.SelectDayGroupIndex(year, secondsPerDay, dayOfYear);
+ int idx = _loadedSkyDesc.SelectDayGroupIndex(absYear, secondsPerDay, dayOfYear);
var grp = idx >= 0 && idx < _loadedSkyDesc.DayGroups.Count
? _loadedSkyDesc.DayGroups[idx]
: null;
@@ -4350,7 +4355,7 @@ public sealed class GameWindow : IDisposable
grp.SkyTimes.Select(s => s.Keyframe).ToList()));
Console.WriteLine(
- $"sky: PY{year} day{dayOfYear} → DayGroup[{idx}] \"{grp.Name}\" " +
+ $"sky: PY{absYear} day{dayOfYear} → DayGroup[{idx}] \"{grp.Name}\" " +
$"(Chance={grp.ChanceOfOccur:F2}, {grp.SkyObjects.Count} objects, " +
$"{grp.SkyTimes.Count} keyframes)");
}
diff --git a/src/AcDream.Core/World/DerethDateTime.cs b/src/AcDream.Core/World/DerethDateTime.cs
index 5168e04..0ee9c2d 100644
--- a/src/AcDream.Core/World/DerethDateTime.cs
+++ b/src/AcDream.Core/World/DerethDateTime.cs
@@ -57,6 +57,15 @@ public static class DerethDateTime
///
public const double MaxTicks = 1_073_741_828.0;
+ ///
+ /// Base/anchor year for the Portal Year calendar (retail
+ /// GameTime.ZeroYear = 10). Tick 0 corresponds to PY 10,
+ /// Morningthaw 1, Morntide-and-Half. Retail's TimeOfDay+0x64
+ /// ("absolute year") includes this offset, and SkyDesc::PickCurrentDayGroup
+ /// (FUN_00501990) feeds the absolute year into its LCG seed.
+ ///
+ public const int ZeroYear = 10;
+
///
/// The 16 named hour slots (r12 §1.2). Each one is half a Derethian
/// hour; the "-and-Half" variants are the second half.
@@ -178,13 +187,11 @@ public static class DerethDateTime
}
///
- /// Absolute year since tick-0 (PY 0, Snowreap 1, Morntide-and-Half).
- /// Matches retail's TimeOfDay + 0x64 field which is
- /// floor((worldTime + base) / secsPerYear) + baseYear. For
- /// acdream's purposes we treat ZeroYear=0 since only the RELATIVE
- /// year is needed for deterministic day-group rolling (retail and
- /// acdream agree as long as both apply the same derivation to the
- /// server's PortalYearTicks).
+ /// Year offset from tick-0 (number of Derethian years elapsed since
+ /// the PY anchor). Use for time-delta math.
+ /// For calendar display and for the retail
+ /// SkyDesc::PickCurrentDayGroup LCG seed, use
+ /// which adds the ZeroYear base.
///
public static int Year(double ticks)
{
@@ -192,6 +199,17 @@ public static class DerethDateTime
return (int)(ticks / YearTicks);
}
+ ///
+ /// Absolute Portal Year ( + relative year).
+ /// Matches retail's TimeOfDay + 0x64 field
+ /// (FUN_005a7510:5300:
+ /// floor((worldTime+base)/secsPerYear) + baseYear). This is
+ /// the value the retail DayGroup picker (FUN_00501990) feeds
+ /// into its LCG seed, so acdream must match for identical weather
+ /// picks vs retail.
+ ///
+ public static int AbsoluteYear(double ticks) => Year(ticks) + ZeroYear;
+
///
/// Day index within the current Derethian year in [0, 359] (360 days
/// per year = 12 months × 30). Matches retail's TimeOfDay + 0x68
diff --git a/src/AcDream.Core/World/SkyDescLoader.cs b/src/AcDream.Core/World/SkyDescLoader.cs
index e8084b9..fde70fc 100644
--- a/src/AcDream.Core/World/SkyDescLoader.cs
+++ b/src/AcDream.Core/World/SkyDescLoader.cs
@@ -235,10 +235,10 @@ public sealed class LoadedSkyDesc
///
public DayGroupData? ActiveDayGroup(double serverTicks)
{
- int year = DerethDateTime.Year(serverTicks);
+ int absYear = DerethDateTime.AbsoluteYear(serverTicks);
int dayOfYear = DerethDateTime.DayOfYear(serverTicks);
int secondsPerDay = (int)DerethDateTime.DayTicks; // 7620
- int idx = SelectDayGroupIndex(year, secondsPerDay, dayOfYear);
+ int idx = SelectDayGroupIndex(absYear, secondsPerDay, dayOfYear);
return idx < DayGroups.Count ? DayGroups[idx] : null;
}