acdream/docs/research/2026-04-23-retail-memory-probe.md
Erik d09e246d3a refactor(world): own live environment state
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>
2026-07-22 10:54:33 +02:00

119 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```c
// 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 commit cd8a37a already
adopted this from `Region.GameTime.ZeroTimeOfYear`).
- `BaseYear` / `ZeroYear` = 10 ✓ (`DerethDateTime.ZeroYear`).
- `SecondsPerDay` float (+0x0C) = 7620 ✓ (`DayTicks`).
- `SecondsPerYear` = 2,743,200 ✓ (`YearTicks`).
- `Year` = 116, `DayOfYear` = 47 — our `AbsoluteYear` and `DayOfYear`
helpers 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.