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
|
|
@ -7,6 +7,9 @@
|
|||
<Project Path="src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj" />
|
||||
<Project Path="src/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tools/">
|
||||
<Project Path="tools/RetailTimeProbe/RetailTimeProbe.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/AcDream.Core.Tests.Fixtures.HelloPlugin/AcDream.Core.Tests.Fixtures.HelloPlugin.csproj" />
|
||||
<Project Path="tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj" />
|
||||
|
|
|
|||
117
docs/research/2026-04-23-retail-memory-probe.md
Normal file
117
docs/research/2026-04-23-retail-memory-probe.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# 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 `FUN_00501990` (`SkyDesc::PickCurrentDayGroup`) LCG seed.
|
||||
|
||||
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
|
||||
// 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.
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
211
tools/RetailTimeProbe/Program.cs
Normal file
211
tools/RetailTimeProbe/Program.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// 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 ?? "<null>"}");
|
||||
|
||||
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 <PlatformTarget>x86</PlatformTarget>.");
|
||||
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);
|
||||
}
|
||||
18
tools/RetailTimeProbe/RetailTimeProbe.csproj
Normal file
18
tools/RetailTimeProbe/RetailTimeProbe.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>RetailTimeProbe</RootNamespace>
|
||||
<!--
|
||||
Retail acclient.exe is 32-bit. We could run as x64 (ReadProcessMemory
|
||||
handles cross-bitness fine for 32-bit target processes) but force
|
||||
x86 to match the target's native pointer width, so hardcoded
|
||||
DAT_xxxxxxxx addresses stay sane and we don't need WOW64 quirks.
|
||||
-->
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Add table
Add a link
Reference in a new issue