pub mod finance; use std::collections::BTreeMap; use std::fmt; use std::fs::File; use std::path::Path; use std::str::FromStr; use serde::Deserialize; use serde::de::{self, Deserializer}; pub const CANONICAL_EXE_PATH: &str = "rt3_wineprefix/drive_c/rt3/RT3.exe"; pub const CONTROL_LOOP_ATLAS_PATH: &str = "docs/control-loop-atlas.md"; pub const FUNCTION_MAP_PATH: &str = "artifacts/exports/rt3-1.06/function-map.csv"; pub const BINARY_SUMMARY_PATH: &str = "artifacts/exports/rt3-1.06/binary-summary.json"; pub const REQUIRED_EXPORTS: &[&str] = &[ BINARY_SUMMARY_PATH, "artifacts/exports/rt3-1.06/sections.csv", "artifacts/exports/rt3-1.06/imported-dlls.txt", "artifacts/exports/rt3-1.06/imported-functions.csv", "artifacts/exports/rt3-1.06/interesting-strings.txt", "artifacts/exports/rt3-1.06/subsystem-inventory.md", FUNCTION_MAP_PATH, "artifacts/exports/rt3-1.06/ghidra-startup-functions.csv", "artifacts/exports/rt3-1.06/startup-call-chain.md", "artifacts/exports/rt3-1.06/analysis-context-functions.csv", "artifacts/exports/rt3-1.06/analysis-context-strings.csv", "artifacts/exports/rt3-1.06/analysis-context.md", "artifacts/exports/rt3-1.06/pending-template-store-functions.csv", "artifacts/exports/rt3-1.06/pending-template-store-record-kinds.csv", "artifacts/exports/rt3-1.06/pending-template-store-management.md", "artifacts/exports/rt3-1.06/event-effects-table.json", "artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json", ]; pub const REQUIRED_ATLAS_HEADINGS: &[&str] = &[ "## CRT and Process Startup", "## Bootstrap and Shell Service Bring-Up", "## Shell UI Command and Deferred Work Flow", "## Presentation, Overlay, and Frame Timing", "## Map and Scenario Content Load", "## Multiplayer Session and Transport Flow", "## Input, Save/Load, and Simulation", "## Next Mapping Passes", ]; pub const FUNCTION_MAP_HEADER: &[&str] = &[ "address", "size", "name", "subsystem", "calling_convention", "prototype_status", "source_tool", "confidence", "notes", "verified_against", ]; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Address(pub u32); impl Address { pub fn parse_hex(value: &str) -> Result { let trimmed = value.trim(); let digits = trimmed .strip_prefix("0x") .or_else(|| trimmed.strip_prefix("0X")) .unwrap_or(trimmed); u32::from_str_radix(digits, 16) .map(Self) .map_err(|err| format!("invalid hex address `{trimmed}`: {err}")) } } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "0x{:08x}", self.0) } } impl FromStr for Address { type Err = String; fn from_str(value: &str) -> Result { Self::parse_hex(value) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SubsystemId { Startup, Bootstrap, Shell, Support, Ui, Render, Audio, Input, Network, Filesystem, Resource, Map, Scenario, Save, Simulation, Unknown, } impl fmt::Display for SubsystemId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { Self::Startup => "startup", Self::Bootstrap => "bootstrap", Self::Shell => "shell", Self::Support => "support", Self::Ui => "ui", Self::Render => "render", Self::Audio => "audio", Self::Input => "input", Self::Network => "network", Self::Filesystem => "filesystem", Self::Resource => "resource", Self::Map => "map", Self::Scenario => "scenario", Self::Save => "save", Self::Simulation => "simulation", Self::Unknown => "unknown", }; f.write_str(label) } } impl FromStr for SubsystemId { type Err = String; fn from_str(value: &str) -> Result { match value { "startup" => Ok(Self::Startup), "bootstrap" => Ok(Self::Bootstrap), "shell" => Ok(Self::Shell), "support" => Ok(Self::Support), "ui" => Ok(Self::Ui), "render" => Ok(Self::Render), "audio" => Ok(Self::Audio), "input" => Ok(Self::Input), "network" => Ok(Self::Network), "filesystem" => Ok(Self::Filesystem), "resource" => Ok(Self::Resource), "map" => Ok(Self::Map), "scenario" => Ok(Self::Scenario), "save" => Ok(Self::Save), "simulation" => Ok(Self::Simulation), "unknown" => Ok(Self::Unknown), other => Err(format!("unknown subsystem `{other}`")), } } } impl<'de> Deserialize<'de> for SubsystemId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let raw = String::deserialize(deserializer)?; raw.parse().map_err(de::Error::custom) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ControlLoopId { CrtStartup, Bootstrap, ShellUi, PresentationFrame, MapScenarioLoad, MultiplayerTransport, InputSaveSimulation, } impl fmt::Display for ControlLoopId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { Self::CrtStartup => "crt-startup", Self::Bootstrap => "bootstrap", Self::ShellUi => "shell-ui", Self::PresentationFrame => "presentation-frame", Self::MapScenarioLoad => "map-scenario-load", Self::MultiplayerTransport => "multiplayer-transport", Self::InputSaveSimulation => "input-save-simulation", }; f.write_str(label) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum LoopRole { Root, Dispatcher, Cadence, StateAnchor, Gateway, } impl fmt::Display for LoopRole { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { Self::Root => "root", Self::Dispatcher => "dispatcher", Self::Cadence => "cadence", Self::StateAnchor => "state-anchor", Self::Gateway => "gateway", }; f.write_str(label) } } #[derive(Debug, Deserialize)] pub struct FunctionRecord { #[serde(deserialize_with = "deserialize_address")] pub address: Address, #[serde(default, deserialize_with = "deserialize_optional_u32")] pub size: Option, pub name: String, pub subsystem: SubsystemId, pub calling_convention: String, pub prototype_status: String, pub source_tool: String, pub confidence: u8, pub notes: String, pub verified_against: String, } #[derive(Debug, Deserialize)] pub struct BinarySummary { pub path: String, pub sha256: String, pub size_bytes: u64, #[serde(default)] pub summary: BTreeMap, } pub fn load_function_map(path: &Path) -> Result, Box> { let mut reader = csv::ReaderBuilder::new() .trim(csv::Trim::All) .from_path(path)?; let headers = reader.headers()?.clone(); let header_values: Vec<&str> = headers.iter().collect(); if header_values != FUNCTION_MAP_HEADER { return Err(format!("unexpected function-map header: {header_values:?}").into()); } let mut rows = Vec::new(); for record in reader.deserialize() { rows.push(record?); } Ok(rows) } pub fn load_binary_summary(path: &Path) -> Result> { let file = File::open(path)?; Ok(serde_json::from_reader(file)?) } fn deserialize_address<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let raw = String::deserialize(deserializer)?; raw.parse().map_err(de::Error::custom) } fn deserialize_optional_u32<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let raw = String::deserialize(deserializer)?; let trimmed = raw.trim(); if trimmed.is_empty() { return Ok(None); } let parsed = trimmed .parse::() .or_else(|_| u32::from_str_radix(trimmed.trim_start_matches("0x"), 16)) .map_err(de::Error::custom)?; Ok(Some(parsed)) } #[cfg(test)] mod tests { use super::*; #[test] fn parses_hex_addresses() { let address = Address::parse_hex("0x005a313b").expect("address should parse"); assert_eq!(address.0, 0x005a313b); assert_eq!(address.to_string(), "0x005a313b"); } #[test] fn parses_subsystems() { let subsystem: SubsystemId = "shell".parse().expect("subsystem should parse"); assert_eq!(subsystem, SubsystemId::Shell); assert_eq!(subsystem.to_string(), "shell"); } }