sky(phase-3g): fix LCG multiplier — 360 (DaysPerYear), not 7620
Ran a live memory probe against retail acclient.exe (new tool:
tools/RetailTimeProbe/) to read the TimeOfDay struct at
DAT_008ee9c8 and compare against our computed values. The decompile
agent's identification of TimeOfDay+0x10 as "SecondsPerDay (int
copy)" turned out to be WRONG — the live value is **360**, which is
GameTime.DaysPerYear.
The retail FUN_00501990 LCG seed is:
seed = Year × (*+0x10) + DayOfYear
= Year × DaysPerYear + DayOfYear
= flat "total days since epoch" day-index
Our previous Phase 3c port passed 7620 (DayLength in ticks) as the
multiplier, producing seed=883,967 against retail's seed=41,807 —
completely different LCG outputs, completely different DayGroup
picks. That's why the user's retail kept showing stormy/rainy while
acdream showed sunny/clear (or vice versa) even after Phases 3c.1
and 3f aligned Year and DayOfYear.
Also confirmed by the probe:
- EpochBase / ZeroTimeOfYear = 3600 ✓ Phase 3f already correct
- BaseYear / ZeroYear = 10 ✓ DerethDateTime.ZeroYear
- Year=116, DayOfYear=47 ✓ our AbsoluteYear / DayOfYear
- SecondsPerDay float (+0x0C) = 7620 ✓ DayTicks
- SecondsPerYear = 2,743,200 ✓ YearTicks
One "finding that's not a fix": retail's +0x48 DayFraction is a
sub-period fraction (fraction through current day/night window)
NOT a full-day fraction. CurDayEnd - CurDayStart = 2857.5 = 0.375
of a day = 6 Dereth hours = night duration. Not relevant for our
keyframe bracket interpolation, which correctly uses a full-day
0..1 scale matching the SkyTime.Begin values. Documented in the
probe research doc so future work doesn't trip on it.
Changes:
- tools/RetailTimeProbe/ — new P/Invoke tool. Forced x86 target to
match retail's bitness so hardcoded DAT_xxxxxxxx addresses are
pointer-width-correct. Handles ASLR relocation via
Process.MainModule.BaseAddress.
- src/AcDream.App/Rendering/GameWindow.cs: RefreshSkyForCurrentDay
passes 360 (DaysInAMonth × MonthsInAYear) not 7620.
- src/AcDream.Core/World/SkyDescLoader.cs: ActiveDayGroup(ticks)
and DefaultDayGroup same.
- docs/research/2026-04-23-retail-memory-probe.md — full probe
results + decompile-agent correction.
- AcDream.slnx — add tools/ folder.
Build + 733 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd8a37a9c8
commit
1e1d3875f7
6 changed files with 374 additions and 12 deletions
|
|
@ -4352,19 +4352,26 @@ public sealed class GameWindow : IDisposable
|
|||
if (_loadedSkyDesc is null || _loadedSkyDesc.DayGroups.Count == 0)
|
||||
return;
|
||||
|
||||
// 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).
|
||||
// Retail FUN_00501990 seeds the LCG with the triple stored in
|
||||
// TimeOfDay +0x64 (Year), +0x10 (misc. int), +0x68 (DayOfYear)
|
||||
//
|
||||
// The decompile agent labeled +0x10 "SecondsPerDay (int copy)"
|
||||
// but a live memory probe of retail's acclient.exe (2026-04-23,
|
||||
// tools/RetailTimeProbe) shows the value is actually **360** —
|
||||
// semantically DaysPerYear, not seconds. So the LCG seed is
|
||||
// seed = Year × DaysPerYear + DayOfYear
|
||||
// which is literally "total days since epoch" (a flat day index),
|
||||
// confirmed against retail's Year=116, DayOfYear=47, seed=41807.
|
||||
//
|
||||
// Previously we passed 7620 (DayTicks), producing seed 883967 —
|
||||
// a completely different LCG output → wrong DayGroup pick →
|
||||
// user-observed weather mismatch (acdream clear while retail
|
||||
// stormy, 2026-04-23). The live probe nailed the fix.
|
||||
double ticks = WorldTime.NowTicks;
|
||||
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
|
||||
int secondsPerDay = AcDream.Core.World.DerethDateTime.DaysInAMonth
|
||||
* AcDream.Core.World.DerethDateTime.MonthsInAYear; // 360
|
||||
|
||||
// Composite day key for change-detection and logging only; the
|
||||
// LCG seed is computed inside SelectDayGroupIndex from (absYear,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,11 @@ public sealed class LoadedSkyDesc
|
|||
{
|
||||
int absYear = DerethDateTime.AbsoluteYear(serverTicks);
|
||||
int dayOfYear = DerethDateTime.DayOfYear(serverTicks);
|
||||
int secondsPerDay = (int)DerethDateTime.DayTicks; // 7620
|
||||
// Retail's TimeOfDay+0x10 is actually DaysPerYear (= 360 for Dereth,
|
||||
// live probe 2026-04-23), NOT SecondsPerDay as the decompile agent
|
||||
// mis-labeled. See GameWindow.RefreshSkyForCurrentDay for the full
|
||||
// citation.
|
||||
int secondsPerDay = DerethDateTime.DaysInAMonth * DerethDateTime.MonthsInAYear; // 360
|
||||
int idx = SelectDayGroupIndex(absYear, secondsPerDay, dayOfYear);
|
||||
return idx < DayGroups.Count ? DayGroups[idx] : null;
|
||||
}
|
||||
|
|
@ -254,7 +258,9 @@ public sealed class LoadedSkyDesc
|
|||
get
|
||||
{
|
||||
int idx = SelectDayGroupIndex(
|
||||
year: 0, secondsPerDay: (int)DerethDateTime.DayTicks, dayOfYear: 0);
|
||||
year: 0,
|
||||
secondsPerDay: DerethDateTime.DaysInAMonth * DerethDateTime.MonthsInAYear, // 360
|
||||
dayOfYear: 0);
|
||||
return DayGroups.Count > 0 ? DayGroups[idx] : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue