Add hook debug tooling and refine RT3 atlas

This commit is contained in:
Jan Petykiewicz 2026-04-08 16:31:33 -07:00
commit 57bf0666e0
38 changed files with 14437 additions and 873 deletions

View file

@ -7,3 +7,8 @@ license.workspace = true
[lib]
name = "dinput8"
crate-type = ["cdylib", "rlib"]
[dependencies]
rrt-model = { path = "../rrt-model" }
serde.workspace = true
serde_json.workspace = true

View file

@ -1,10 +1,144 @@
#![cfg_attr(not(windows), allow(dead_code))]
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use rrt_model::finance::{
AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceOutcome, FinanceSnapshot,
};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FinanceLogPaths {
pub snapshot_path: PathBuf,
pub outcome_path: PathBuf,
}
pub fn sample_finance_snapshot() -> FinanceSnapshot {
FinanceSnapshot {
policy: AnnualFinancePolicy {
dividends_allowed: false,
..AnnualFinancePolicy::default()
},
company: CompanyFinanceState {
current_cash: 100_000,
support_adjusted_share_price: 27.5,
book_value_per_share: 20.0,
outstanding_share_count: 60_000,
recent_net_profits: [40_000, 30_000, 20_000],
recent_revenue_totals: [250_000, 240_000, 230_000],
bonds: vec![
BondPosition {
principal: 150_000,
coupon_rate: 0.12,
years_remaining: 12,
},
BondPosition {
principal: 10_000,
coupon_rate: 0.10,
years_remaining: 10,
},
],
..CompanyFinanceState::default()
},
}
}
pub fn write_finance_snapshot_bundle(
base_dir: &Path,
stem: &str,
snapshot: &FinanceSnapshot,
) -> io::Result<FinanceLogPaths> {
fs::create_dir_all(base_dir)?;
let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json"));
let outcome_path = base_dir.join(format!("rrt_finance_{stem}_outcome.json"));
let outcome: FinanceOutcome = snapshot.evaluate();
let snapshot_json = serde_json::to_vec_pretty(snapshot)
.map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?;
let outcome_json = serde_json::to_vec_pretty(&outcome)
.map_err(|err| io::Error::other(format!("serialize outcome: {err}")))?;
fs::write(&snapshot_path, snapshot_json)?;
fs::write(&outcome_path, outcome_json)?;
Ok(FinanceLogPaths {
snapshot_path,
outcome_path,
})
}
pub fn write_finance_snapshot_only(
base_dir: &Path,
stem: &str,
snapshot: &FinanceSnapshot,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let snapshot_path = base_dir.join(format!("rrt_finance_{stem}_snapshot.json"));
let snapshot_json = serde_json::to_vec_pretty(snapshot)
.map_err(|err| io::Error::other(format!("serialize snapshot: {err}")))?;
fs::write(&snapshot_path, snapshot_json)?;
Ok(snapshot_path)
}
#[derive(Debug, Clone, Serialize)]
pub struct IndexedCollectionProbeRow {
pub entry_id: usize,
pub live: bool,
pub resolved_ptr: usize,
pub active_flag: Option<u8>,
}
#[derive(Debug, Clone, Serialize)]
pub struct IndexedCollectionProbe {
pub collection_addr: usize,
pub flat_payload: bool,
pub stride: u32,
pub id_bound: i32,
pub payload_ptr: usize,
pub tombstone_ptr: usize,
pub first_rows: Vec<IndexedCollectionProbeRow>,
}
pub fn write_indexed_collection_probe(
base_dir: &Path,
stem: &str,
probe: &IndexedCollectionProbe,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let path = base_dir.join(format!("rrt_finance_{stem}_collection_probe.json"));
let json = serde_json::to_vec_pretty(probe)
.map_err(|err| io::Error::other(format!("serialize collection probe: {err}")))?;
fs::write(&path, json)?;
Ok(path)
}
#[cfg(windows)]
mod windows_hook {
use super::{
IndexedCollectionProbe, IndexedCollectionProbeRow, sample_finance_snapshot,
write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe,
};
use core::ffi::{c_char, c_void};
use core::mem;
use core::ptr;
use std::env;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
use rrt_model::finance::{
AnnualFinancePolicy, BondPosition, CompanyFinanceState, FinanceSnapshot, GrowthSetting,
};
const DLL_PROCESS_ATTACH: u32 = 1;
const E_FAIL: i32 = 0x8000_4005_u32 as i32;
@ -17,10 +151,92 @@ mod windows_hook {
static LOG_PATH: &[u8] = b"rrt_hook_attach.log\0";
static ATTACH_MESSAGE: &[u8] = b"rrt-hook: process attach\n";
static FINANCE_CAPTURE_STARTED_MESSAGE: &[u8] = b"rrt-hook: finance capture thread started\n";
static FINANCE_CAPTURE_SCAN_MESSAGE: &[u8] =
b"rrt-hook: finance capture raw collection scan\n";
static FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE: &[u8] =
b"rrt-hook: finance collection probe written\n";
static FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE: &[u8] =
b"rrt-hook: finance capture company resolved\n";
static FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE: &[u8] =
b"rrt-hook: finance probe snapshot written\n";
static FINANCE_CAPTURE_TIMEOUT_MESSAGE: &[u8] =
b"rrt-hook: finance capture timed out\n";
static AUTO_LOAD_STARTED_MESSAGE: &[u8] =
b"rrt-hook: auto load hook armed\n";
static AUTO_LOAD_HOOK_INSTALLED_MESSAGE: &[u8] =
b"rrt-hook: auto load shell-pump hook installed\n";
static AUTO_LOAD_READY_MESSAGE: &[u8] =
b"rrt-hook: auto load ready gate passed\n";
static AUTO_LOAD_DEFERRED_MESSAGE: &[u8] =
b"rrt-hook: auto load restore deferred to later shell-pump turn\n";
static AUTO_LOAD_CALLING_MESSAGE: &[u8] =
b"rrt-hook: auto load restore calling\n";
static AUTO_LOAD_OWNER_ENTRY_MESSAGE: &[u8] =
b"rrt-hook: auto load larger owner entering\n";
static AUTO_LOAD_OWNER_RETURNED_MESSAGE: &[u8] =
b"rrt-hook: auto load larger owner returned\n";
static AUTO_LOAD_TRIGGERED_MESSAGE: &[u8] =
b"rrt-hook: auto load restore invoked\n";
static AUTO_LOAD_SUCCESS_MESSAGE: &[u8] =
b"rrt-hook: auto load request reported success\n";
static AUTO_LOAD_FAILURE_MESSAGE: &[u8] =
b"rrt-hook: auto load request reported failure\n";
static DEBUG_MESSAGE: &[u8] = b"rrt-hook: DllMain process attach\0";
static DIRECT_INPUT8_CREATE_NAME: &[u8] = b"DirectInput8Create\0";
static mut REAL_DINPUT8_CREATE: Option<DirectInput8CreateFn> = None;
static FINANCE_TEMPLATE_EMITTED: AtomicBool = AtomicBool::new(false);
static FINANCE_CAPTURE_STARTED: AtomicBool = AtomicBool::new(false);
static FINANCE_COLLECTION_PROBE_WRITTEN: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_THREAD_STARTED: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_ATTEMPTED: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_DEFERRED: AtomicBool = AtomicBool::new(false);
static AUTO_LOAD_LAST_GATE_MASK: AtomicU32 = AtomicU32::new(u32::MAX);
static AUTO_LOAD_READY_COUNT: AtomicU32 = AtomicU32::new(0);
static AUTO_LOAD_SAVE_STEM: OnceLock<String> = OnceLock::new();
static mut SHELL_PUMP_TRAMPOLINE: usize = 0;
const COMPANY_COLLECTION_ADDR: usize = 0x0062be10;
const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024;
const SHELL_STATE_PTR_ADDR: usize = 0x006cec74;
const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78;
const SHELL_PUMP_ADDR: usize = 0x00483f70;
const SHELL_STATE_ACTIVE_MODE_OFFSET: usize = 0x08;
const SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET: usize = 0x0c;
const RUNTIME_PROFILE_PTR_ADDR: usize = 0x006cec7c;
const RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET: usize = 0x11;
const RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET: usize = 0x97;
const INDEXED_COLLECTION_FLAT_FLAG_OFFSET: usize = 0x04;
const INDEXED_COLLECTION_STRIDE_OFFSET: usize = 0x08;
const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14;
const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30;
const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34;
const COMPANY_ACTIVE_OFFSET: usize = 0x3f;
const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47;
const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57;
const COMPANY_BOND_COUNT_OFFSET: usize = 0x5b;
const COMPANY_BOND_TABLE_OFFSET: usize = 0x5f;
const COMPANY_FOUNDING_YEAR_OFFSET: usize = 0x157;
const COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET: usize = 0x163;
const COMPANY_CITY_CONNECTION_LATCH_OFFSET: usize = 0x0d18;
const COMPANY_LINKED_TRANSIT_LATCH_OFFSET: usize = 0x0d56;
const SCENARIO_CURRENT_YEAR_OFFSET: usize = 0x0d;
const SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET: usize = 0x4c7c;
const SCENARIO_BANKRUPTCY_TOGGLE_OFFSET: usize = 0x4a8f;
const SCENARIO_BOND_TOGGLE_OFFSET: usize = 0x4a8b;
const SCENARIO_STOCK_TOGGLE_OFFSET: usize = 0x4a87;
const SCENARIO_DIVIDEND_TOGGLE_OFFSET: usize = 0x4a93;
const MAX_CAPTURE_POLL_ATTEMPTS: usize = 120;
const CAPTURE_POLL_INTERVAL: Duration = Duration::from_secs(1);
const AUTO_LOAD_READY_POLLS: u32 = 30;
const AUTO_LOAD_DEFER_POLLS: u32 = 5;
const MEM_COMMIT: u32 = 0x1000;
const MEM_RESERVE: u32 = 0x2000;
const PAGE_EXECUTE_READWRITE: u32 = 0x40;
unsafe extern "system" {
fn CreateFileA(
lp_file_name: *const c_char,
@ -46,10 +262,28 @@ mod windows_hook {
) -> i32;
fn CloseHandle(handle: isize) -> i32;
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
fn FlushInstructionCache(
process: *mut c_void,
base_address: *const c_void,
size: usize,
) -> i32;
fn GetCurrentProcess() -> *mut c_void;
fn GetSystemDirectoryA(buffer: *mut u8, size: u32) -> u32;
fn GetProcAddress(module: isize, name: *const c_char) -> *mut c_void;
fn LoadLibraryA(name: *const c_char) -> isize;
fn OutputDebugStringA(output: *const c_char);
fn VirtualAlloc(
address: *mut c_void,
size: usize,
allocation_type: u32,
protect: u32,
) -> *mut c_void;
fn VirtualProtect(
address: *mut c_void,
size: usize,
new_protect: u32,
old_protect: *mut u32,
) -> i32;
}
#[repr(C)]
@ -67,7 +301,8 @@ mod windows_hook {
out: *mut *mut c_void,
outer: *mut c_void,
) -> i32;
type ShellPumpFn = unsafe extern "thiscall" fn(*mut u8) -> i32;
type LargerManualLoadOwnerFn = unsafe extern "thiscall" fn(*mut u8, u32, u32);
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(
module: *mut c_void,
@ -92,6 +327,10 @@ mod windows_hook {
out: *mut *mut c_void,
outer: *mut c_void,
) -> i32 {
maybe_emit_finance_template_bundle();
maybe_start_finance_capture_thread();
maybe_install_auto_load_hook();
let direct_input8_create = unsafe { load_direct_input8_create() };
match direct_input8_create {
Some(callback) => unsafe { callback(instance, version, riid, out, outer) },
@ -100,6 +339,10 @@ mod windows_hook {
}
unsafe fn append_attach_log() {
append_log_message(ATTACH_MESSAGE);
}
fn append_log_message(message: &[u8]) {
let handle = unsafe {
CreateFileA(
LOG_PATH.as_ptr().cast(),
@ -120,8 +363,8 @@ mod windows_hook {
let _ = unsafe {
WriteFile(
handle,
ATTACH_MESSAGE.as_ptr().cast(),
ATTACH_MESSAGE.len() as u32,
message.as_ptr().cast(),
message.len() as u32,
&mut bytes_written,
ptr::null_mut(),
)
@ -129,6 +372,613 @@ mod windows_hook {
let _ = unsafe { CloseHandle(handle) };
}
fn append_log_line(line: &str) {
append_log_message(line.as_bytes());
}
fn maybe_emit_finance_template_bundle() {
if env::var_os("RRT_WRITE_FINANCE_TEMPLATE").is_none() {
return;
}
if FINANCE_TEMPLATE_EMITTED.swap(true, Ordering::AcqRel) {
return;
}
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = write_finance_snapshot_bundle(
&base_dir,
"attach_template",
&sample_finance_snapshot(),
);
}
fn maybe_start_finance_capture_thread() {
if env::var_os("RRT_WRITE_FINANCE_CAPTURE").is_none() {
return;
}
if FINANCE_CAPTURE_STARTED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(FINANCE_CAPTURE_STARTED_MESSAGE);
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = thread::Builder::new()
.name("rrt-finance-capture".to_string())
.spawn(move || {
for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS {
if !FINANCE_COLLECTION_PROBE_WRITTEN.load(Ordering::Acquire) {
if let Some(probe) = unsafe { capture_company_collection_probe() } {
if write_indexed_collection_probe(&base_dir, "attach_probe", &probe)
.is_ok()
{
FINANCE_COLLECTION_PROBE_WRITTEN.store(true, Ordering::Release);
append_log_message(FINANCE_CAPTURE_PROBE_DUMP_WRITTEN_MESSAGE);
}
}
}
if let Some(snapshot) = unsafe { try_capture_probe_snapshot() } {
append_log_message(FINANCE_CAPTURE_COMPANY_RESOLVED_MESSAGE);
if write_finance_snapshot_only(&base_dir, "attach_probe", &snapshot)
.is_ok()
{
append_log_message(FINANCE_CAPTURE_PROBE_WRITTEN_MESSAGE);
return;
}
}
thread::sleep(CAPTURE_POLL_INTERVAL);
}
append_log_message(FINANCE_CAPTURE_TIMEOUT_MESSAGE);
});
}
fn maybe_install_auto_load_hook() {
let save_stem = match env::var("RRT_AUTO_LOAD_SAVE") {
Ok(value) if !value.trim().is_empty() => value,
_ => return,
};
let _ = AUTO_LOAD_SAVE_STEM.set(save_stem);
if AUTO_LOAD_HOOK_INSTALLED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(AUTO_LOAD_STARTED_MESSAGE);
AUTO_LOAD_THREAD_STARTED.store(true, Ordering::Release);
if unsafe { install_shell_pump_hook() } {
append_log_message(AUTO_LOAD_HOOK_INSTALLED_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
}
fn run_auto_load_worker(save_stem: &str) {
append_log_message(AUTO_LOAD_CALLING_MESSAGE);
let staged = unsafe { invoke_manual_load_branch(save_stem) };
if staged {
append_log_message(AUTO_LOAD_TRIGGERED_MESSAGE);
append_log_message(AUTO_LOAD_SUCCESS_MESSAGE);
} else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
}
AUTO_LOAD_IN_PROGRESS.store(false, Ordering::Release);
}
unsafe fn invoke_manual_load_branch(save_stem: &str) -> bool {
if save_stem.is_empty() || save_stem.as_bytes().contains(&0) {
return false;
}
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
let runtime_profile = unsafe { read_ptr(RUNTIME_PROFILE_PTR_ADDR as *const u8) };
let active_mode = unsafe { resolve_active_mode_ptr() };
if shell_state.is_null() || runtime_profile.is_null() || active_mode.is_null() {
return false;
}
let path_seed = unsafe { runtime_profile.add(RUNTIME_PROFILE_MANUAL_LOAD_PATH_OFFSET) };
if unsafe { write_c_string(path_seed, 260, save_stem.as_bytes()) }.is_none() {
return false;
}
unsafe {
ptr::write_unaligned(
runtime_profile
.add(RUNTIME_PROFILE_PENDING_LOAD_BYTE_OFFSET)
.cast::<u8>(),
0,
)
};
let larger_owner: LargerManualLoadOwnerFn =
unsafe { mem::transmute(0x00438890usize) };
let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) };
if global_active_mode.is_null() {
unsafe {
ptr::write_unaligned(
(ACTIVE_MODE_PTR_ADDR as *mut u8).cast::<usize>(),
active_mode as usize,
)
};
}
append_log_message(AUTO_LOAD_OWNER_ENTRY_MESSAGE);
unsafe { larger_owner(active_mode, 1, 0) };
append_log_message(AUTO_LOAD_OWNER_RETURNED_MESSAGE);
if global_active_mode.is_null() {
unsafe {
ptr::write_unaligned((ACTIVE_MODE_PTR_ADDR as *mut u8).cast::<usize>(), 0)
};
}
true
}
unsafe fn write_c_string(
destination: *mut u8,
capacity: usize,
bytes: &[u8],
) -> Option<()> {
if bytes.len() + 1 > capacity {
return None;
}
unsafe { ptr::write_bytes(destination, 0, capacity) };
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), destination, bytes.len()) };
Some(())
}
unsafe fn try_capture_probe_snapshot() -> Option<FinanceSnapshot> {
append_log_message(FINANCE_CAPTURE_SCAN_MESSAGE);
let company = unsafe { resolve_first_active_company()? };
Some(unsafe { capture_probe_snapshot_from_company(company) })
}
unsafe fn runtime_saved_world_restore_gate_mask() -> u32 {
let mut mask = 0_u32;
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if !shell_state.is_null() {
mask |= 0x1;
}
let shell_controller = unsafe { read_ptr(SHELL_CONTROLLER_PTR_ADDR as *const u8) };
if !shell_controller.is_null() {
mask |= 0x2;
}
let active_mode = unsafe { resolve_active_mode_ptr() };
if !active_mode.is_null() {
mask |= 0x4;
}
mask
}
unsafe fn current_mode_id() -> u32 {
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if shell_state.is_null() {
return 0;
}
unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) }
}
fn auto_load_ready_polls() -> u32 {
env::var("RRT_AUTO_LOAD_READY_POLLS")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.filter(|value| *value > 0)
.unwrap_or(AUTO_LOAD_READY_POLLS)
}
fn auto_load_defer_polls() -> u32 {
env::var("RRT_AUTO_LOAD_DEFER_POLLS")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or(AUTO_LOAD_DEFER_POLLS)
}
unsafe extern "fastcall" fn shell_pump_detour(this: *mut u8, _edx: usize) -> i32 {
let trampoline: ShellPumpFn = unsafe { mem::transmute(SHELL_PUMP_TRAMPOLINE) };
let result = unsafe { trampoline(this) };
maybe_service_auto_load_on_main_thread();
result
}
fn maybe_service_auto_load_on_main_thread() {
if !AUTO_LOAD_HOOK_INSTALLED.load(Ordering::Acquire)
|| AUTO_LOAD_ATTEMPTED.load(Ordering::Acquire)
|| AUTO_LOAD_IN_PROGRESS.load(Ordering::Acquire)
{
return;
}
let gate_mask = unsafe { runtime_saved_world_restore_gate_mask() };
let last_gate_mask = AUTO_LOAD_LAST_GATE_MASK.swap(gate_mask, Ordering::AcqRel);
if gate_mask != last_gate_mask {
log_auto_load_gate_mask(gate_mask);
}
let mode_id = unsafe { current_mode_id() };
let ready = gate_mask == 0x7 && mode_id == 2;
let ready_count = if ready {
AUTO_LOAD_READY_COUNT.fetch_add(1, Ordering::AcqRel) + 1
} else {
AUTO_LOAD_READY_COUNT.store(0, Ordering::Release);
AUTO_LOAD_DEFERRED.store(false, Ordering::Release);
0
};
let ready_polls = auto_load_ready_polls();
if ready_count < ready_polls {
return;
}
if !AUTO_LOAD_DEFERRED.load(Ordering::Acquire) {
AUTO_LOAD_DEFERRED.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_READY_MESSAGE);
append_log_message(AUTO_LOAD_DEFERRED_MESSAGE);
return;
}
if ready_count < ready_polls.saturating_add(auto_load_defer_polls()) {
return;
}
if AUTO_LOAD_ATTEMPTED.swap(true, Ordering::AcqRel) {
return;
}
let Some(save_stem) = AUTO_LOAD_SAVE_STEM.get() else {
append_log_message(AUTO_LOAD_FAILURE_MESSAGE);
return;
};
AUTO_LOAD_IN_PROGRESS.store(true, Ordering::Release);
append_log_message(AUTO_LOAD_READY_MESSAGE);
run_auto_load_worker(save_stem);
}
fn log_auto_load_gate_mask(mask: u32) {
let mut line = String::from("rrt-hook: auto load gate mask ");
let global_active_mode = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as usize;
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
let mode_id = if shell_state.is_null() {
0
} else {
unsafe { read_u32(shell_state.add(SHELL_STATE_ACTIVE_MODE_OFFSET)) as usize }
};
let field_active_mode_object = if shell_state.is_null() {
0
} else {
unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) as usize }
};
let _ = write!(
&mut line,
"0x{mask:01x} shell_state={} shell_controller={} active_mode={} global_active_mode=0x{global_active_mode:08x} mode_id=0x{mode_id:08x} field_active_mode_object=0x{field_active_mode_object:08x}\n",
(mask & 0x1) != 0,
(mask & 0x2) != 0,
(mask & 0x4) != 0,
);
append_log_line(&line);
}
unsafe fn resolve_active_mode_ptr() -> *mut u8 {
let global_active_mode = unsafe { resolve_global_active_mode_ptr() };
if !global_active_mode.is_null() {
return global_active_mode;
}
let shell_state = unsafe { read_ptr(SHELL_STATE_PTR_ADDR as *const u8) };
if shell_state.is_null() {
return ptr::null_mut();
}
unsafe { read_ptr(shell_state.add(SHELL_STATE_ACTIVE_MODE_OBJECT_OFFSET)) }
}
unsafe fn resolve_global_active_mode_ptr() -> *mut u8 {
unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) }
}
unsafe fn install_shell_pump_hook() -> bool {
const STOLEN_LEN: usize = 8;
let target = SHELL_PUMP_ADDR as *mut u8;
let trampoline_size = STOLEN_LEN + 5;
let trampoline = unsafe {
VirtualAlloc(
ptr::null_mut(),
trampoline_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE,
)
} as *mut u8;
if trampoline.is_null() {
return false;
}
unsafe { ptr::copy_nonoverlapping(target, trampoline, STOLEN_LEN) };
unsafe {
write_rel32_jump(
trampoline.add(STOLEN_LEN),
target.add(STOLEN_LEN) as usize,
)
};
let mut old_protect = 0_u32;
if unsafe {
VirtualProtect(
target.cast(),
STOLEN_LEN,
PAGE_EXECUTE_READWRITE,
&mut old_protect,
)
} == 0
{
return false;
}
unsafe { write_rel32_jump(target, shell_pump_detour as *const () as usize) };
unsafe { ptr::write(target.add(5), 0x90) };
unsafe { ptr::write(target.add(6), 0x90) };
unsafe { ptr::write(target.add(7), 0x90) };
let mut restore_protect = 0_u32;
let _ = unsafe {
VirtualProtect(
target.cast(),
STOLEN_LEN,
old_protect,
&mut restore_protect,
)
};
let _ = unsafe {
FlushInstructionCache(
GetCurrentProcess(),
target.cast(),
STOLEN_LEN,
)
};
unsafe {
SHELL_PUMP_TRAMPOLINE = trampoline as usize;
}
true
}
unsafe fn write_rel32_jump(location: *mut u8, destination: usize) {
unsafe { ptr::write(location, 0xE9) };
let next_ip = unsafe { location.add(5) } as usize;
let relative = (destination as isize - next_ip as isize) as i32;
unsafe { ptr::write_unaligned(location.add(1).cast::<i32>(), relative) };
}
unsafe fn resolve_first_active_company() -> Option<*mut u8> {
let collection = COMPANY_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return None;
}
for entry_id in 1..=id_bound as usize {
if unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } {
let company = unsafe { indexed_collection_resolve_live_entry_by_id(collection, entry_id) };
if !company.is_null() && unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 } {
return Some(company);
}
}
}
None
}
unsafe fn capture_company_collection_probe() -> Option<IndexedCollectionProbe> {
let collection = COMPANY_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return Some(IndexedCollectionProbe {
collection_addr: COMPANY_COLLECTION_ADDR,
flat_payload: unsafe {
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
},
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize
},
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
first_rows: Vec::new(),
});
}
let mut first_rows = Vec::new();
let sample_bound = (id_bound as usize).min(8);
for entry_id in 1..=sample_bound {
let live = unsafe { indexed_collection_entry_id_is_live(collection, entry_id) };
let resolved_ptr = unsafe {
indexed_collection_resolve_live_entry_by_id(collection, entry_id) as usize
};
let active_flag = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(COMPANY_ACTIVE_OFFSET)) })
};
first_rows.push(IndexedCollectionProbeRow {
entry_id,
live,
resolved_ptr,
active_flag,
});
}
Some(IndexedCollectionProbe {
collection_addr: COMPANY_COLLECTION_ADDR,
flat_payload: unsafe {
read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0
},
stride: unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) },
id_bound,
payload_ptr: unsafe { read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) as usize },
tombstone_ptr: unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET)) as usize
},
first_rows,
})
}
unsafe fn capture_probe_snapshot_from_company(company: *mut u8) -> FinanceSnapshot {
let scenario = unsafe { read_ptr(ACTIVE_MODE_PTR_ADDR as *const u8) } as *const u8;
let current_year = unsafe { read_u16(scenario.add(SCENARIO_CURRENT_YEAR_OFFSET)) };
let founding_year = unsafe { read_u16(company.add(COMPANY_FOUNDING_YEAR_OFFSET)) };
let last_bankruptcy_year =
unsafe { read_u16(company.add(COMPANY_LAST_BANKRUPTCY_YEAR_OFFSET)) };
let outstanding_share_count =
unsafe { read_u32(company.add(COMPANY_OUTSTANDING_SHARES_OFFSET)) };
let bonds = unsafe { capture_bonds(company, current_year) };
let company_value = unsafe { read_u32(company.add(COMPANY_COMPANY_VALUE_OFFSET)) as i64 };
let growth_setting = unsafe {
growth_setting_from_raw(read_u8(
scenario.add(SCENARIO_BUILDING_DENSITY_GROWTH_OFFSET),
))
};
FinanceSnapshot {
policy: AnnualFinancePolicy {
annual_mode: 0x0c,
bankruptcy_allowed: unsafe {
read_u8(scenario.add(SCENARIO_BANKRUPTCY_TOGGLE_OFFSET)) == 0
},
bond_issuance_allowed: unsafe {
read_u8(scenario.add(SCENARIO_BOND_TOGGLE_OFFSET)) == 0
},
stock_actions_allowed: unsafe {
read_u8(scenario.add(SCENARIO_STOCK_TOGGLE_OFFSET)) == 0
},
dividends_allowed: unsafe {
read_u8(scenario.add(SCENARIO_DIVIDEND_TOGGLE_OFFSET)) == 0
},
growth_setting,
..AnnualFinancePolicy::default()
},
company: CompanyFinanceState {
active: unsafe { read_u8(company.add(COMPANY_ACTIVE_OFFSET)) != 0 },
years_since_founding: year_delta(current_year, founding_year),
years_since_last_bankruptcy: year_delta(current_year, last_bankruptcy_year),
current_company_value: company_value,
outstanding_share_count,
city_connection_bonus_latch: unsafe {
read_u8(company.add(COMPANY_CITY_CONNECTION_LATCH_OFFSET)) != 0
},
linked_transit_service_latch: unsafe {
read_u8(company.add(COMPANY_LINKED_TRANSIT_LATCH_OFFSET)) != 0
},
chairman_buyback_factor: None,
bonds,
..CompanyFinanceState::default()
},
}
}
unsafe fn capture_bonds(company: *mut u8, current_year: u16) -> Vec<BondPosition> {
let bond_count = unsafe { read_u8(company.add(COMPANY_BOND_COUNT_OFFSET)) as usize };
let table = unsafe { company.add(COMPANY_BOND_TABLE_OFFSET) };
let mut bonds = Vec::with_capacity(bond_count);
for index in 0..bond_count {
let slot = unsafe { table.add(index * 12) };
let principal = unsafe { read_i32(slot) } as i64;
let maturity_year = unsafe { read_u32(slot.add(4)) };
let coupon_rate = unsafe { read_f32(slot.add(8)) } as f64;
bonds.push(BondPosition {
principal,
coupon_rate,
years_remaining: maturity_year
.saturating_sub(current_year as u32)
.min(u8::MAX as u32) as u8,
});
}
bonds
}
fn growth_setting_from_raw(raw: u8) -> GrowthSetting {
match raw {
1 => GrowthSetting::ExpansionBias,
2 => GrowthSetting::DividendSuppressed,
_ => GrowthSetting::Neutral,
}
}
fn year_delta(current_year: u16, past_year: u16) -> u8 {
current_year
.saturating_sub(past_year)
.min(u8::MAX as u16) as u8
}
unsafe fn indexed_collection_entry_id_is_live(collection: *const u8, entry_id: usize) -> bool {
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if entry_id == 0 || entry_id > id_bound.max(0) as usize {
return false;
}
let tombstone_bits = unsafe {
read_ptr(collection.add(INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET))
};
if tombstone_bits.is_null() {
return true;
}
let bit_index = entry_id as u32;
let word = unsafe {
ptr::read_unaligned(tombstone_bits.add((bit_index / 32) as usize).cast::<u32>())
};
(word & (1_u32 << (bit_index % 32))) == 0
}
unsafe fn indexed_collection_resolve_live_entry_by_id(
collection: *const u8,
entry_id: usize,
) -> *mut u8 {
if !unsafe { indexed_collection_entry_id_is_live(collection, entry_id) } {
return ptr::null_mut();
}
let payload = unsafe { read_ptr(collection.add(INDEXED_COLLECTION_PAYLOAD_OFFSET)) };
if payload.is_null() {
return ptr::null_mut();
}
let stride = unsafe { read_u32(collection.add(INDEXED_COLLECTION_STRIDE_OFFSET)) as usize };
let flat = unsafe { read_u32(collection.add(INDEXED_COLLECTION_FLAT_FLAG_OFFSET)) != 0 };
if flat {
unsafe { payload.add(stride * entry_id) }
} else {
unsafe { ptr::read_unaligned(payload.add(stride * entry_id).cast::<*mut u8>()) }
}
}
unsafe fn read_u8(address: *const u8) -> u8 {
unsafe { ptr::read_unaligned(address) }
}
unsafe fn read_u16(address: *const u8) -> u16 {
unsafe { ptr::read_unaligned(address.cast::<u16>()) }
}
unsafe fn read_u32(address: *const u8) -> u32 {
unsafe { ptr::read_unaligned(address.cast::<u32>()) }
}
unsafe fn read_i32(address: *const u8) -> i32 {
unsafe { ptr::read_unaligned(address.cast::<i32>()) }
}
unsafe fn read_f32(address: *const u8) -> f32 {
unsafe { ptr::read_unaligned(address.cast::<f32>()) }
}
unsafe fn read_ptr(address: *const u8) -> *mut u8 {
unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) }
}
unsafe fn load_direct_input8_create() -> Option<DirectInput8CreateFn> {
if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } {
return Some(callback);
@ -168,3 +1018,30 @@ mod windows_hook {
pub fn host_build_marker() -> &'static str {
"rrt-hook host build"
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn writes_snapshot_bundle_to_disk() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!("rrt-hook-finance-{nonce}"));
let paths = write_finance_snapshot_bundle(&dir, "testcase", &sample_finance_snapshot())
.expect("bundle should be written");
let snapshot_json = fs::read_to_string(&paths.snapshot_path).expect("snapshot should exist");
let outcome_json = fs::read_to_string(&paths.outcome_path).expect("outcome should exist");
assert!(snapshot_json.contains("\"policy\""));
assert!(outcome_json.contains("\"evaluation\""));
let _ = fs::remove_file(&paths.snapshot_path);
let _ = fs::remove_file(&paths.outcome_path);
let _ = fs::remove_dir(&dir);
}
}