// RetailTimeProbe — read the live retail acclient.exe process memory and // dump its TimeOfDay struct + sky-lighting global block so we can compare // against acdream's computed calendar / SkyKeyframe values. // // Decompile provenance (docs/research/2026-04-23-sky-decompile-hunt-C.md // §4 and the daygroup-selection research): // // DAT_008ee9c8 — absolute VA of a pointer to TimeOfDay // TimeOfDay +0x00 double — EpochBase // TimeOfDay +0x08 int — BaseYear // TimeOfDay +0x0C float — SecondsPerDay // TimeOfDay +0x10 int — SecondsPerDay (int copy, LCG input) // TimeOfDay +0x40 double — SecondsPerYear // TimeOfDay +0x48 float — DayFraction (0..1) — live authority! // TimeOfDay +0x50 double — CurrentDay startTick // TimeOfDay +0x58 double — CurrentDay endTick // TimeOfDay +0x64 int — Year (absolute) // TimeOfDay +0x68 int — DayOfYear // TimeOfDay +0x6C int — SeasonIndex // // Sky-lighting globals (hunt-C §1, with 2026-04-24 label correction — the // DirColor/AmbColor labeling in §1/§2/§5 was backwards; we use the // corrected mapping): // // DAT_00842778 4 ARGB DirColor (directional / sun color) // DAT_0084277c 4 ARGB AmbColor (ambient color) // DAT_00842780 4 float AmbBright (ambient brightness scalar, also fog-start offset) // DAT_00842784 4 ARGB FogSecondary // DAT_00842788 4 ARGB FogPrimary // DAT_00842950 12 3×flt sunDir XYZ (|v| = DirBright, NOT a unit vector) // DAT_0084295c 4 float DirBright floor (MinWorldFog clamp) // DAT_0079a1e8 4 float fog-distance scale factor (used in // fogDist = |sunDir| * _DAT_0079a1e8 + AmbBright) // // Cached D3D light struct (written by FUN_00505f30:6058-6065 and // FUN_004530e0:2083-2086 — see chunk_00500000.c / chunk_00450000.c): // // DAT_008682b0 12 3×flt light.Ambient pre-mul = fogTint * AmbBright // (set inside FUN_004530e0 via FUN_00451a60(DirColor)) // DAT_008682bc 12 3×flt sunDir copy (fVar1/2/3 = X/Y/Z) // DAT_008682c8 12 3×flt sunDir primary // DAT_008682d4 4 uint reserved (written 0) // DAT_008682d8 4 uint light type (3 = directional) // // The acclient.exe referenced in the decompile has preferred image base // 0x00400000 (standard Win32 default). If ASLR is enabled the actual // load address will differ — we compute relative to Process.MainModule // .BaseAddress to stay robust. using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace RetailTimeProbe; internal static class Program { // Decompile-derived constants. private const uint PreferredImageBase = 0x00400000u; private const uint DatTimeOfDayPtr = 0x008ee9c8u; // DAT_008ee9c8 // TimeOfDay struct offsets. private const int Off_EpochBase = 0x00; // double private const int Off_BaseYear = 0x08; // int private const int Off_SecondsPerDay = 0x0C; // float private const int Off_SecondsPerDayI = 0x10; // int private const int Off_SecondsPerYear = 0x40; // double private const int Off_DayFraction = 0x48; // float private const int Off_CurDayStart = 0x50; // double private const int Off_CurDayEnd = 0x58; // double private const int Off_Year = 0x64; // int private const int Off_DayOfYear = 0x68; // int private const int Off_SeasonIndex = 0x6C; // int // Sky-lighting globals (static VAs in acclient.exe image). private const uint SkyBlockBase = 0x00842778u; // DirColor / start of sky block private const uint SkyBlockSize = 72u; // 0x00842778..0x008427c0 = 72 bytes private const uint DAT_DirColor = 0x00842778u; // ARGB private const uint DAT_AmbColor = 0x0084277cu; // ARGB private const uint DAT_AmbBright = 0x00842780u; // float private const uint DAT_FogSecondary = 0x00842784u; // ARGB private const uint DAT_FogPrimary = 0x00842788u; // ARGB private const uint DAT_SunDirX = 0x00842950u; // float private const uint DAT_SunDirY = 0x00842954u; // float private const uint DAT_SunDirZ = 0x00842958u; // float private const uint DAT_DirBrightMin = 0x0084295cu; // float (MinWorldFog / DirBright floor) private const uint DAT_FogScale = 0x0079a1e8u; // float (|sun|·scale factor) // Cached D3D light struct. private const uint DAT_LightAmbient = 0x008682b0u; // 3×float (light.Ambient pre-mul) private const uint DAT_LightDirCopy = 0x008682bcu; // 3×float (sunDir copy) private const uint DAT_LightDirMain = 0x008682c8u; // 3×float (sunDir primary) private const uint DAT_LightReserved = 0x008682d4u; // uint private const uint DAT_LightType = 0x008682d8u; // uint (3 = directional) // Process access rights needed: read memory + query info. private const uint PROCESS_VM_READ = 0x0010u; private const uint PROCESS_QUERY_INFORMATION = 0x0400u; private static int Main(string[] args) { // Retail's process name is "acclient" (.exe stripped by Process API). // args[0] = process name OR "pid=NNNN" to target a specific pid. string processName = "acclient"; int? requestedPid = null; foreach (var a in args) { if (a.StartsWith("pid=", StringComparison.OrdinalIgnoreCase) && int.TryParse(a.Substring(4), out var pidParsed)) requestedPid = pidParsed; else processName = a; } Process target; if (requestedPid is int pid) { try { target = Process.GetProcessById(pid); } catch (Exception ex) { Console.Error.WriteLine($"no process with pid={pid}: {ex.Message}"); return 2; } Console.WriteLine($"RetailTimeProbe — targeting pid={pid} ({target.ProcessName})"); } else { Console.WriteLine($"RetailTimeProbe — scanning for process \"{processName}\"..."); Process[] procs = Process.GetProcessesByName(processName); if (procs.Length == 0) { Console.Error.WriteLine( $"no process named \"{processName}\" is running. Launch the retail AC client " + "and log in to a character first, then re-run this probe."); return 2; } if (procs.Length > 1) { Console.WriteLine($"(found {procs.Length} matching processes — use `pid=NNNN` to target a specific one)"); foreach (var p in procs) { Console.WriteLine($" pid={p.Id} start={p.StartTime:HH:mm:ss} title=\"{p.MainWindowTitle}\""); } Console.WriteLine("(probing the first)"); } target = procs[0]; } Console.WriteLine( $"pid={target.Id} name={target.ProcessName} start={target.StartTime:HH:mm:ss} " + $"mainmodule={target.MainModule?.FileName ?? ""}"); IntPtr moduleBase; try { moduleBase = target.MainModule!.BaseAddress; } catch (Exception ex) { Console.Error.WriteLine( $"failed to read main module (maybe bitness mismatch or access denied): {ex.Message}\n" + " if you're running this probe from an x64 process against an x86 target,\n" + " make sure RetailTimeProbe is built with x86."); return 3; } Console.WriteLine( $"module base=0x{moduleBase.ToInt64():X8} " + $"(preferred 0x{PreferredImageBase:X8}; ASLR delta={moduleBase.ToInt64() - PreferredImageBase:+0;-0;0})"); // Open a handle with read + query perms. IntPtr handle = OpenProcess( PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, (uint)target.Id); if (handle == IntPtr.Zero) { int err = Marshal.GetLastPInvokeError(); Console.Error.WriteLine( $"OpenProcess failed (error {err}). If this is \"Access is denied\" (0x5), try running " + "as the same user that owns the acclient.exe process, or as administrator."); return 4; } try { // Compute the relocated address of DAT_008ee9c8. long datAddress = moduleBase.ToInt64() + (long)(DatTimeOfDayPtr - PreferredImageBase); Console.WriteLine($"DAT_008ee9c8 relocated to 0x{datAddress:X8}"); // Read the pointer at DAT_008ee9c8 → TimeOfDay* uint timeOfDayPtr = ReadUInt32(handle, (IntPtr)datAddress); Console.WriteLine($"TimeOfDay* = 0x{timeOfDayPtr:X8}"); if (timeOfDayPtr == 0) { Console.Error.WriteLine( "TimeOfDay pointer is NULL. The retail client hasn't entered the world yet — " + "log in to a character fully, then re-run the probe."); return 5; } // Dump the TimeOfDay struct. double epochBase = ReadDouble(handle, (IntPtr)(timeOfDayPtr + Off_EpochBase)); int baseYear = (int)ReadUInt32(handle, (IntPtr)(timeOfDayPtr + Off_BaseYear)); float secsPerDayF = ReadSingle(handle, (IntPtr)(timeOfDayPtr + Off_SecondsPerDay)); int secsPerDayI = (int)ReadUInt32(handle, (IntPtr)(timeOfDayPtr + Off_SecondsPerDayI)); double secsPerYear = ReadDouble(handle, (IntPtr)(timeOfDayPtr + Off_SecondsPerYear)); float dayFraction = ReadSingle(handle, (IntPtr)(timeOfDayPtr + Off_DayFraction)); double curDayStart = ReadDouble(handle, (IntPtr)(timeOfDayPtr + Off_CurDayStart)); double curDayEnd = ReadDouble(handle, (IntPtr)(timeOfDayPtr + Off_CurDayEnd)); int year = (int)ReadUInt32(handle, (IntPtr)(timeOfDayPtr + Off_Year)); int dayOfYear = (int)ReadUInt32(handle, (IntPtr)(timeOfDayPtr + Off_DayOfYear)); int seasonIndex = (int)ReadUInt32(handle, (IntPtr)(timeOfDayPtr + Off_SeasonIndex)); Console.WriteLine(); Console.WriteLine("=========== TimeOfDay (retail acclient.exe, live) ==========="); Console.WriteLine($" EpochBase (+0x00 double) = {epochBase:F6}"); Console.WriteLine($" BaseYear (+0x08 int) = {baseYear}"); Console.WriteLine($" SecondsPerDay (+0x0C float) = {secsPerDayF}"); Console.WriteLine($" SecondsPerDay (+0x10 int) = {secsPerDayI} <-- LCG input"); Console.WriteLine($" SecondsPerYear (+0x40 double) = {secsPerYear:F6}"); Console.WriteLine($" DayFraction (+0x48 float) = {dayFraction:F6} <-- authoritative hour-of-day"); Console.WriteLine($" CurDayStart (+0x50 double) = {curDayStart:F6}"); Console.WriteLine($" CurDayEnd (+0x58 double) = {curDayEnd:F6}"); Console.WriteLine($" Year (+0x64 int) = {year} <-- LCG seed year"); Console.WriteLine($" DayOfYear (+0x68 int) = {dayOfYear} <-- LCG seed day"); Console.WriteLine($" SeasonIndex (+0x6C int) = {seasonIndex}"); Console.WriteLine("============================================================="); // Derive "the tick retail is treating as current". // Retail stores CurDayStart/CurDayEnd as absolute tick bounds of the current day. // Given DayFraction in [0, 1), the absolute tick retail thinks we're at = // CurDayStart + DayFraction * (CurDayEnd - CurDayStart). double inferredTick = curDayStart + dayFraction * (curDayEnd - curDayStart); Console.WriteLine($" inferred retail tick = {inferredTick:F3}"); Console.WriteLine($" retail LCG seed = year*secsPerDay + dayOfYear = {year}*{secsPerDayI}+{dayOfYear} = {(long)year * secsPerDayI + dayOfYear}"); // ---------------- Sky-lighting block dump ---------------- DumpSkyBlock(handle, moduleBase); return 0; } finally { CloseHandle(handle); } } private static void DumpSkyBlock(IntPtr handle, IntPtr moduleBase) { // Helper to relocate a preferred-image-base VA onto the live module. IntPtr Reloc(uint va) => (IntPtr)(moduleBase.ToInt64() + (long)(va - PreferredImageBase)); Console.WriteLine(); Console.WriteLine("=========== Sky globals (retail acclient.exe, live) ==========="); // Raw block dump for the contiguous 72-byte region at 0x00842778. byte[] block = ReadBytes(handle, Reloc(SkyBlockBase), (int)SkyBlockSize); Console.Write($" [raw {SkyBlockBase:X8}..{SkyBlockBase + SkyBlockSize - 1:X8}]"); for (int i = 0; i < block.Length; i++) { if ((i % 16) == 0) Console.Write($"\n +{i:X2}:"); Console.Write($" {block[i]:X2}"); } Console.WriteLine(); Console.WriteLine(); // Primary field-by-field decode. uint dirColor = ReadUInt32(handle, Reloc(DAT_DirColor)); uint ambColor = ReadUInt32(handle, Reloc(DAT_AmbColor)); float ambBright = ReadSingle(handle, Reloc(DAT_AmbBright)); uint fogSecondary = ReadUInt32(handle, Reloc(DAT_FogSecondary)); uint fogPrimary = ReadUInt32(handle, Reloc(DAT_FogPrimary)); float sunX = ReadSingle(handle, Reloc(DAT_SunDirX)); float sunY = ReadSingle(handle, Reloc(DAT_SunDirY)); float sunZ = ReadSingle(handle, Reloc(DAT_SunDirZ)); float dirBrightMin = ReadSingle(handle, Reloc(DAT_DirBrightMin)); float fogScale = ReadSingle(handle, Reloc(DAT_FogScale)); double dirBright = Math.Sqrt((double)sunX * sunX + (double)sunY * sunY + (double)sunZ * sunZ); Console.WriteLine($" [retail sky] DirColor = {FormatArgb(dirColor)}"); Console.WriteLine($" [retail sky] AmbColor = {FormatArgb(ambColor)}"); Console.WriteLine($" [retail sky] AmbBright = {ambBright:F4} (@0x{DAT_AmbBright:X8})"); Console.WriteLine($" [retail sky] FogPrimary = {FormatArgb(fogPrimary)} (@0x{DAT_FogPrimary:X8})"); Console.WriteLine($" [retail sky] FogSecondary = {FormatArgb(fogSecondary)} (@0x{DAT_FogSecondary:X8})"); Console.WriteLine($" [retail sky] sunDir = ({sunX,7:F4},{sunY,7:F4},{sunZ,7:F4}) |dir|=DirBright={dirBright:F4}"); Console.WriteLine($" [retail sky] DirBrightMin = {dirBrightMin:F4} (@0x{DAT_DirBrightMin:X8}, MinWorldFog clamp)"); Console.WriteLine($" [retail sky] 0x0079a1e8 = {fogScale:F6} (fog |sun|-scale factor)"); // Derived fog distance (matches FUN_00505f30:6067-6069): // fogDist = |sunDir| * _DAT_0079a1e8 + AmbBright double fogDist = dirBright * fogScale + ambBright; Console.WriteLine($" [retail sky] derived fogDist = |sun|*scale + AmbBright = {fogDist:F4}"); // ---- Cached D3D light struct at 0x008682b0..0x008682d8 (40 bytes) ---- Console.WriteLine(); Console.WriteLine(" -- cached D3D light struct (0x008682b0..0x008682d8) --"); float ambR = ReadSingle(handle, Reloc(DAT_LightAmbient + 0)); float ambG = ReadSingle(handle, Reloc(DAT_LightAmbient + 4)); float ambB = ReadSingle(handle, Reloc(DAT_LightAmbient + 8)); float dcX = ReadSingle(handle, Reloc(DAT_LightDirCopy + 0)); float dcY = ReadSingle(handle, Reloc(DAT_LightDirCopy + 4)); float dcZ = ReadSingle(handle, Reloc(DAT_LightDirCopy + 8)); float dmX = ReadSingle(handle, Reloc(DAT_LightDirMain + 0)); float dmY = ReadSingle(handle, Reloc(DAT_LightDirMain + 4)); float dmZ = ReadSingle(handle, Reloc(DAT_LightDirMain + 8)); uint reservedVal = ReadUInt32(handle, Reloc(DAT_LightReserved)); uint lightType = ReadUInt32(handle, Reloc(DAT_LightType)); Console.WriteLine($" [retail sky] cache.amb = ({ambR,7:F4},{ambG,7:F4},{ambB,7:F4}) (fogTint * AmbBright, effective light.Ambient)"); Console.WriteLine($" [retail sky] cache.dirCpy = ({dcX,7:F4},{dcY,7:F4},{dcZ,7:F4}) (008682bc/c0/c4, sunDir duplicate)"); Console.WriteLine($" [retail sky] cache.dirMain= ({dmX,7:F4},{dmY,7:F4},{dmZ,7:F4}) (008682c8/cc/d0, sunDir primary)"); Console.WriteLine($" [retail sky] cache.reserv = 0x{reservedVal:X8} (008682d4, written 0 by 00505f30:6065)"); Console.WriteLine($" [retail sky] cache.type = 0x{lightType:X8} (008682d8, 3 = directional)"); Console.WriteLine("================================================================="); } /// /// Format a packed ARGB u32 as "#AARRGGBB (r=.. g=.. b=..)". Retail uses the /// standard Windows D3DCOLOR layout verified against FUN_00451a60 (chunk /// _00450000.c:615-622): float R = (u >> 16) & 0xff, G = (u >> 8) & 0xff, /// B = u & 0xff, each divided by 255. /// private static string FormatArgb(uint argb) { byte a = (byte)((argb >> 24) & 0xff); byte r = (byte)((argb >> 16) & 0xff); byte g = (byte)((argb >> 8) & 0xff); byte b = (byte)( argb & 0xff); return $"#{a:X2}{r:X2}{g:X2}{b:X2} (r={r / 255.0f:F3} g={g / 255.0f:F3} b={b / 255.0f:F3})"; } private static byte[] ReadBytes(IntPtr handle, IntPtr address, int count) { byte[] buf = new byte[count]; if (!ReadProcessMemory(handle, address, buf, buf.Length, out _)) throw new InvalidOperationException( $"ReadProcessMemory(0x{address.ToInt64():X8}, {count}) failed " + $"(Win32 error {Marshal.GetLastPInvokeError()})"); return buf; } private static uint ReadUInt32(IntPtr handle, IntPtr address) { byte[] buf = new byte[4]; if (!ReadProcessMemory(handle, address, buf, buf.Length, out _)) throw new InvalidOperationException( $"ReadProcessMemory(0x{address.ToInt64():X8}, 4) failed " + $"(Win32 error {Marshal.GetLastPInvokeError()})"); return BitConverter.ToUInt32(buf, 0); } private static float ReadSingle(IntPtr handle, IntPtr address) { byte[] buf = new byte[4]; if (!ReadProcessMemory(handle, address, buf, buf.Length, out _)) throw new InvalidOperationException( $"ReadProcessMemory(0x{address.ToInt64():X8}, 4) failed " + $"(Win32 error {Marshal.GetLastPInvokeError()})"); return BitConverter.ToSingle(buf, 0); } private static double ReadDouble(IntPtr handle, IntPtr address) { byte[] buf = new byte[8]; if (!ReadProcessMemory(handle, address, buf, buf.Length, out _)) throw new InvalidOperationException( $"ReadProcessMemory(0x{address.ToInt64():X8}, 8) failed " + $"(Win32 error {Marshal.GetLastPInvokeError()})"); return BitConverter.ToDouble(buf, 0); } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, uint processId); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ReadProcessMemory( IntPtr processHandle, IntPtr baseAddress, byte[] buffer, int size, out int bytesRead); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr handle); }