2026-04-08 16:31:33 -07:00
|
|
|
pub mod finance;
|
|
|
|
|
|
2026-04-02 23:11:15 -07:00
|
|
|
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",
|
2026-04-16 19:03:07 -07:00
|
|
|
"artifacts/exports/rt3-1.06/event-effects-table.json",
|
2026-04-16 20:20:41 -07:00
|
|
|
"artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json",
|
2026-04-02 23:11:15 -07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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<Self, String> {
|
|
|
|
|
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, Self::Err> {
|
|
|
|
|
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<Self, Self::Err> {
|
|
|
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
|
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<u32>,
|
|
|
|
|
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<String, String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn load_function_map(path: &Path) -> Result<Vec<FunctionRecord>, Box<dyn std::error::Error>> {
|
|
|
|
|
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<BinarySummary, Box<dyn std::error::Error>> {
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
|
Ok(serde_json::from_reader(file)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn deserialize_address<'de, D>(deserializer: D) -> Result<Address, D::Error>
|
|
|
|
|
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<Option<u32>, 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::<u32>()
|
|
|
|
|
.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");
|
|
|
|
|
}
|
|
|
|
|
}
|