Ground named cargo production event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-16 23:44:55 -07:00
commit 6a5d028d19
14 changed files with 987 additions and 217 deletions

View file

@ -119,10 +119,49 @@ pub fn write_indexed_collection_probe(
Ok(path)
}
#[derive(Debug, Clone, Serialize)]
pub struct CargoCollectionProbeRow {
pub entry_id: usize,
pub live: bool,
pub resolved_ptr: usize,
pub stem: Option<String>,
pub route_style_byte: Option<u8>,
pub subtype_byte: Option<u8>,
pub class_marker: Option<u32>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CargoCollectionProbe {
pub collection_addr: usize,
pub flat_payload: bool,
pub stride: u32,
pub id_bound: i32,
pub payload_ptr: usize,
pub tombstone_ptr: usize,
pub live_entry_count: usize,
pub rows: Vec<CargoCollectionProbeRow>,
}
pub fn write_cargo_collection_probe(
base_dir: &Path,
stem: &str,
probe: &CargoCollectionProbe,
) -> io::Result<PathBuf> {
fs::create_dir_all(base_dir)?;
let path = base_dir.join(format!("rrt_cargo_{stem}_collection_probe.json"));
let json = serde_json::to_vec_pretty(probe)
.map_err(|err| io::Error::other(format!("serialize cargo collection probe: {err}")))?;
fs::write(&path, json)?;
Ok(path)
}
#[cfg(windows)]
mod windows_hook {
use super::{
IndexedCollectionProbe, IndexedCollectionProbeRow, sample_finance_snapshot,
CargoCollectionProbe, CargoCollectionProbeRow, IndexedCollectionProbe,
IndexedCollectionProbeRow, sample_finance_snapshot, write_cargo_collection_probe,
write_finance_snapshot_bundle, write_finance_snapshot_only, write_indexed_collection_probe,
};
use core::ffi::{c_char, c_void};
@ -160,6 +199,9 @@ mod windows_hook {
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 CARGO_CAPTURE_STARTED_MESSAGE: &[u8] = b"rrt-hook: cargo capture thread started\n";
static CARGO_CAPTURE_WRITTEN_MESSAGE: &[u8] = b"rrt-hook: cargo collection probe written\n";
static CARGO_CAPTURE_TIMEOUT_MESSAGE: &[u8] = b"rrt-hook: cargo 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-state hook installed\n";
@ -284,6 +326,7 @@ mod windows_hook {
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 CARGO_CAPTURE_STARTED: 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);
@ -324,6 +367,7 @@ mod windows_hook {
static mut MODE2_TEARDOWN_TRAMPOLINE: usize = 0;
const COMPANY_COLLECTION_ADDR: usize = 0x0062be10;
const CARGO_COLLECTION_ADDR: usize = 0x0062ba8c;
const SHELL_CONTROLLER_PTR_ADDR: usize = 0x006d4024;
const SHELL_STATE_PTR_ADDR: usize = 0x006cec74;
const ACTIVE_MODE_PTR_ADDR: usize = 0x006cec78;
@ -360,6 +404,10 @@ mod windows_hook {
const INDEXED_COLLECTION_ID_BOUND_OFFSET: usize = 0x14;
const INDEXED_COLLECTION_PAYLOAD_OFFSET: usize = 0x30;
const INDEXED_COLLECTION_TOMBSTONE_BITSET_OFFSET: usize = 0x34;
const CARGO_STEM_OFFSET: usize = 0x04;
const CARGO_SUBTYPE_OFFSET: usize = 0x32;
const CARGO_ROUTE_STYLE_OFFSET: usize = 0x46;
const CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET: usize = 0x9a;
const COMPANY_ACTIVE_OFFSET: usize = 0x3f;
const COMPANY_OUTSTANDING_SHARES_OFFSET: usize = 0x47;
const COMPANY_COMPANY_VALUE_OFFSET: usize = 0x57;
@ -493,6 +541,7 @@ mod windows_hook {
) -> i32 {
maybe_emit_finance_template_bundle();
maybe_start_finance_capture_thread();
maybe_start_cargo_capture_thread();
maybe_install_auto_load_hook();
let direct_input8_create = unsafe { load_direct_input8_create() };
@ -592,6 +641,41 @@ mod windows_hook {
});
}
fn maybe_start_cargo_capture_thread() {
if env::var_os("RRT_WRITE_CARGO_CAPTURE").is_none() {
return;
}
if CARGO_CAPTURE_STARTED.swap(true, Ordering::AcqRel) {
return;
}
append_log_message(CARGO_CAPTURE_STARTED_MESSAGE);
let base_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let _ = thread::Builder::new()
.name("rrt-cargo-capture".to_string())
.spawn(move || {
let mut last_probe: Option<CargoCollectionProbe> = None;
for _ in 0..MAX_CAPTURE_POLL_ATTEMPTS {
if let Some(probe) = unsafe { capture_cargo_collection_probe() } {
last_probe = Some(probe.clone());
if probe.live_entry_count > 0
&& write_cargo_collection_probe(&base_dir, "attach_probe", &probe)
.is_ok()
{
append_log_message(CARGO_CAPTURE_WRITTEN_MESSAGE);
return;
}
}
thread::sleep(CAPTURE_POLL_INTERVAL);
}
if let Some(probe) = last_probe {
let _ = write_cargo_collection_probe(&base_dir, "attach_probe_timeout", &probe);
}
append_log_message(CARGO_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,
@ -2751,6 +2835,93 @@ mod windows_hook {
})
}
unsafe fn capture_cargo_collection_probe() -> Option<CargoCollectionProbe> {
let collection = CARGO_COLLECTION_ADDR as *const u8;
let id_bound = unsafe { read_i32(collection.add(INDEXED_COLLECTION_ID_BOUND_OFFSET)) };
if id_bound <= 0 {
return Some(CargoCollectionProbe {
collection_addr: CARGO_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
},
live_entry_count: 0,
rows: Vec::new(),
});
}
let mut live_entry_count = 0_usize;
let mut rows = Vec::with_capacity(id_bound as usize);
for entry_id in 1..=id_bound as usize {
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
};
if live && resolved_ptr != 0 {
live_entry_count += 1;
}
let stem = if resolved_ptr == 0 {
None
} else {
Some(unsafe {
read_c_string((resolved_ptr as *const u8).add(CARGO_STEM_OFFSET), 0x1e)
})
};
let route_style_byte = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_ROUTE_STYLE_OFFSET)) })
};
let subtype_byte = if resolved_ptr == 0 {
None
} else {
Some(unsafe { read_u8((resolved_ptr as *const u8).add(CARGO_SUBTYPE_OFFSET)) })
};
let class_marker = if live {
Some(unsafe {
read_u32(
collection.add(CARGO_COLLECTION_CLASS_MARKER_BASE_OFFSET + entry_id * 4),
)
})
} else {
None
};
rows.push(CargoCollectionProbeRow {
entry_id,
live,
resolved_ptr,
stem,
route_style_byte,
subtype_byte,
class_marker,
});
}
Some(CargoCollectionProbe {
collection_addr: CARGO_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
},
live_entry_count,
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)) };
@ -2905,6 +3076,18 @@ mod windows_hook {
unsafe { ptr::read_unaligned(address.cast::<*mut u8>()) }
}
unsafe fn read_c_string(address: *const u8, max_len: usize) -> String {
let mut len = 0;
while len < max_len {
let byte = unsafe { read_u8(address.add(len)) };
if byte == 0 {
break;
}
len += 1;
}
String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(address, len) }).into_owned()
}
unsafe fn load_direct_input8_create() -> Option<DirectInput8CreateFn> {
if let Some(callback) = unsafe { REAL_DINPUT8_CREATE } {
return Some(callback);