Iteration on the sky rendering pipeline to restore stars/moon visibility
at night and fix washed-out grey daytime clouds. Key fixes:
* sky.frag: disable fog-mix on sky meshes. Retail's keyframe FogEnd
(0..400m at midnight, up to 2400m during day) is calibrated for
terrain; sky meshes are authored at radii 1050-14271m which sits
past FogEnd universally, causing every sky pixel to saturate to
fogColor (dark navy). Stars, moon, dome texture all got
obliterated. The horizon-glow trade-off is noted in the shader
comment; research item to find retail's sky-specific fog range
later.
* SkyRenderer + sky.frag: promote rep.Luminosity into uEmissive so the
vertex lighting saturates properly for bright keyframes. Retail's
FUN_0059da60 non-luminous path writes rep.Luminosity into
material.Emissive via the cache +0x3c slot; we were instead using
it as a post-fragment multiply which could only dim, never brighten.
Net effect: daytime clouds now render saturated white, dome dims
correctly at night (rep.Luminosity=0.11 → Emissive=0.11), stars
and moon unchanged.
* terrain.vert: MIN_FACTOR 0.08 -> 0.0 per retail FUN_00532440 decompile
(DAT_00796344 ambient-floor = 0.0). Back-lit terrain now falls to
pure ambient rather than getting an 8% sun floor.
New research / tooling (no runtime impact):
* docs/research/2026-04-24-lambert-brightness-split.md — retail's
ambient-brightness formula pinned from PE .rdata read + live
RetailTimeProbe capture: effAmbBright = AmbBright + |sunDir| * 0.2
where scale constant 0x0079a1e8 = 0.2f exactly.
* docs/research/2026-04-23-lightning-real.md — research note on the
dat-baked PhysicsScript-driven lightning path (Rainy DayGroup has
explicit PES-triggered flash SkyObjects with 5ms time windows).
* Corrections stapled to sky-decompile-hunt-{B,C}.md: DAT_00842778 is
DirColor, DAT_0084277c is AmbColor (the hunt docs had the swap
backwards).
* tools/RetailTimeProbe/Program.cs: extended with pid=NNNN selector,
sky global probe (DirColor/AmbColor/AmbBright/sunDir/cache.amb),
and the 0x0079a1e8 scale-factor readout.
* tools/SkyObjectInspect/: throwaway dat-inspector built by the Opus
deep-dive agent. Identified GfxObj 0x010015EF as the stars layer
(A8R8G8B8 128x128 texture, 4% bright-pixel ratio).
* src/AcDream.App/Rendering/TextureCache.cs: per-texture alpha
histogram dump under ACDREAM_DUMP_SKY=1 for diagnosing "are the
clouds decoded with proper alpha" type questions.
README: rewrite to reflect current state (playable pre-alpha rendering
Dereth with animated characters, day-night cycle, weather, etc.)
instead of the stale "Phase 0 dat inventory only" description.
All 742 tests green.
385 lines
20 KiB
C#
385 lines
20 KiB
C#
// 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 ?? "<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}");
|
||
|
||
// ---------------- 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("=================================================================");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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);
|
||
}
|