333 lines
13 KiB
Rust
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
|
|
}
|