rrt/crates/rrt-hook/src/windows/capture.rs

333 lines
13 KiB
Rust

use super::*;
pub(super) 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());
}
pub(super) 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);
});
}
pub(super) 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);
});
}
pub(super) 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) })
}
pub(super) 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
}
pub(super) 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,
})
}
pub(super) 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,
})
}
pub(super) 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()
},
}
}
pub(super) 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
}
pub(super) fn growth_setting_from_raw(raw: u8) -> GrowthSetting {
match raw {
1 => GrowthSetting::ExpansionBias,
2 => GrowthSetting::DividendSuppressed,
_ => GrowthSetting::Neutral,
}
}
pub(super) fn year_delta(current_year: u16, past_year: u16) -> u8 {
current_year.saturating_sub(past_year).min(u8::MAX as u16) as u8
}