// RetailTimeProbe — read the live retail acclient.exe process memory and // dump its TimeOfDay struct so we can compare against acdream's computed // calendar 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 // // 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 // 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). // Allow override from the command line just in case. string processName = args.Length > 0 ? args[0] : "acclient"; 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 — probing the first)"); Process 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}"); return 0; } finally { CloseHandle(handle); } } 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); }