Move the DAT sky, selected day group, world clock, weather, AdminEnvirons bridge, and debug cycles into a one-shot WorldEnvironmentController while preserving GameWindow's public aliases and accepted startup/session/render order. Correct the named retail citations and register the remaining environment audio and fog/radar gaps. Co-authored-by: Codex <codex@openai.com>
4.7 KiB
Retail acclient.exe Live Memory Probe — TimeOfDay Struct
Date: 2026-04-23
Tool: tools/RetailTimeProbe/ — P/Invoke ReadProcessMemory against a running retail client
Target: C:\Turbine\Asheron's Call\acclient.exe, pid 17980
Observed values
module base = 0x00400000 (no ASLR delta from preferred)
DAT_008ee9c8 relocated to 0x008EE9C8
TimeOfDay* = 0x00A5C1C0
EpochBase (+0x00 double) = 3600.000000 ← matches dat ZeroTimeOfYear ✓
BaseYear (+0x08 int) = 10 ← matches dat ZeroYear ✓
SecondsPerDay (+0x0C float) = 7620 ← matches dat DayLength ✓
+0x10 (int) = 360 ← see §1 — NOT seconds-per-day!
SecondsPerYear (+0x40 double) = 2743200.000000 ← DayLength × DaysPerYear ✓
DayFraction (+0x48 float) = 0.340492 ← see §2
CurDayStart (+0x50 double) = 291133740.000000
CurDayEnd (+0x58 double) = 291136597.500000
Year (+0x64 int) = 116 ← matches our AbsoluteYear ✓
DayOfYear (+0x68 int) = 47 ← matches our DayOfYear ✓
SeasonIndex (+0x6C int) = 1
Acdream at the same wall-clock moment:
AbsoluteYear = 116 ✓
DayOfYear = 47 ✓
1. The +0x10 field is DaysPerYear (360), not SecondsPerDay
The decompile agent's C trail
(docs/research/2026-04-23-sky-decompile-hunt-C.md §1 + §4) labeled
TimeOfDay+0x10 as "SecondsPerDay (int copy — source of iVar6)"
for the named SkyDesc::CalcPresentDayGroup @ 0x00500E10 LCG seed
(older-build cross-reference FUN_00501990 @ 0x00501990).
The live probe disproves that. The value is 360 — the same as
GameTime.DaysPerYear from the dat.
Implication for the retail LCG seed formula:
// SkyDesc::CalcPresentDayGroup @ 0x00500E10
// (older build: FUN_00501990 line 1296 of chunk_00500000.c):
iVar4 = (iVar3 * iVar6 + iVar4) * 0x6a42fdb2 + -0x7541e9ae;
// ^Year ^x0x10 ^DayOfYear
With iVar6 = 360 (DaysPerYear), the seed is:
seed = Year × DaysPerYear + DayOfYear
= total days since (Year 0, DayOfYear 0)
That's a flat day-index, which makes obvious sense for a per-day weather picker. Retail's engineers didn't pass a "seconds per day" at all — they passed "days per year" as the year-to-day multiplier so the final seed is a total-day count.
Actionable: our C# port must pass 360 (DaysInAMonth * MonthsInAYear)
as the LCG's secondsPerDay parameter, not the 7620 we previously used.
With 7620 we compute seed = 883,967; retail computes seed = 41,807.
Completely different LCG outputs → different DayGroup picks → weather
mismatch users observed against their retail client side-by-side.
2. +0x48 DayFraction is a sub-period fraction, not full-day
Live retail at the probed moment:
- Calendar:
PY116 ColdMeet 18 Dawnsong(hour 4 of 16, i.e. morning). - DayFraction: 0.340492
Our acdream at the same tick would compute dayFraction ≈ 0.13 using
the full-day formula ((tick + ZeroTimeOfYear) mod DayLength) / DayLength.
The stored CurDayStart/End bounds give it away:
CurDayEnd - CurDayStart = 2857.5 ticks = 0.375 × 7620 = 6 Dereth hours
6 hours = night duration (hours 0-3 + 14-15 = 6 hours of the 16-hour day).
So retail's +0x48 DayFraction is "fraction through current day/night
period", NOT "fraction through full calendar day". Using these fields:
retailDayFraction = (currentTick - CurDayStart) / (CurDayEnd - CurDayStart)
For our use-case (bracketing SkyTime keyframes whose Begin is on a
[0, 1] full-day scale), the full-day formula we have is correct — sky
keyframes don't care about retail's internal sub-period storage. The
difference is only visible if we reverse-engineer "what hour does retail
think it is" by reading its field; we wouldn't use retail's
+0x48 value for interpolation even if we could.
So: no fix required for our dayFraction path. The keyframe bracket works
correctly with our own (tick + 3600) mod 7620 / 7620 formula.
3. Confirmed no-change items
EpochBase/ZeroTimeOfYear= 3600 ✓ (Phase 3f commitcd8a37aalready adopted this fromRegion.GameTime.ZeroTimeOfYear).BaseYear/ZeroYear= 10 ✓ (DerethDateTime.ZeroYear).SecondsPerDayfloat (+0x0C) = 7620 ✓ (DayTicks).SecondsPerYear= 2,743,200 ✓ (YearTicks).Year= 116,DayOfYear= 47 — ourAbsoluteYearandDayOfYearhelpers already reproduce these from the server tick.
4. Follow-ups
None critical after the Phase 3g fix (DaysPerYear in LCG). If the
DayFraction sub-period storage becomes relevant (e.g. if we ever render
retail's "Xm until night" UI), we'd replicate the CurDayStart/End
computation from FUN_005a7800 in the decompile.