//! Database loading — reads economy data from systems.db. use std::collections::HashMap; use std::path::PathBuf; use std::process; use rusqlite::Connection; // --------------------------------------------------------------------------- // Data types // --------------------------------------------------------------------------- #[derive(Debug, Clone)] pub struct Commodity { pub id: String, // Display name — used in reporting (#807+): #[allow(dead_code)] pub name: String, pub tier: String, pub base_price: f64, // Used by Layer 2+ pricing (#807, #808): #[allow(dead_code)] pub elasticity: String, #[allow(dead_code)] pub production_ubiquity: Option, #[allow(dead_code)] pub demand_model: String, } #[derive(Debug, Clone)] pub struct ChainInput { pub commodity_id: String, pub quantity: f64, } #[derive(Debug, Clone)] pub struct ProductionChain { pub chain_id: String, pub output_commodity_id: String, pub output_quantity: f64, // Used by Layer 2+ for location-constrained production (#807): #[allow(dead_code)] pub location_bound: bool, pub inputs: Vec, } #[derive(Debug, Clone)] pub struct CorpPresence { pub corp_id: String, pub system_id: String, pub primary_operation: Option, } #[derive(Debug, Clone)] pub struct SystemInfo { pub system_id: String, // Used for display/reporting in #807+: #[allow(dead_code)] pub proper_name: Option, pub population: i64, pub cultural_corridor: Option, pub gate_energy_connected: bool, /// Currency zone: TRACTUS_PRIMARY | MARK_PRIMARY | MIXED (D-171, D-172) pub currency_zone: String, /// Hop count from the nearest gateway — used for shadow economy seeding (D-174) pub hop_distance: i64, /// Gate topology type — used for shadow economy seeding (D-174) pub gate_topology: Option, /// Political zone — used for shadow economy seeding (D-174) pub political_zone: Option, } /// A directed gate link between two systems. #[derive(Debug, Clone)] pub struct GateLink { pub from_system_id: String, pub to_system_id: String, } /// The complete economics dataset loaded from systems.db. pub struct Economy { pub commodities: Vec, pub commodity_map: HashMap, pub chains: Vec, /// Map: output_commodity_id → list of chains that produce it pub chains_by_output: HashMap>, /// Map: system_id → SystemInfo pub systems: HashMap, pub corp_presences: Vec, /// Map: system_id → list of corp presences pub presences_by_system: HashMap>, /// Bidirectional gate links (transport graph) pub gate_links: Vec, /// Raw corp data for archetype inference: (corp_id, behavioral_archetype?, specialization?) pub corp_archetype_data: Vec<(String, Option, Option)>, } // --------------------------------------------------------------------------- // DB helpers // --------------------------------------------------------------------------- pub fn resolve_db_path(explicit: Option) -> PathBuf { if let Some(p) = explicit { return p; } let mut dir = std::env::current_dir().expect("Cannot determine CWD"); loop { let candidate = dir.join("server").join("data").join("systems.db"); if candidate.exists() { return candidate; } if !dir.pop() { break; } } eprintln!("error: cannot find server/data/systems.db — pass --db explicitly"); process::exit(1); } pub fn open_db(path: &PathBuf) -> Connection { let conn = Connection::open(path).unwrap_or_else(|e| { eprintln!("error: cannot open {}: {}", path.display(), e); process::exit(1); }); conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;") .expect("PRAGMA setup failed"); conn } // --------------------------------------------------------------------------- // Loaders // --------------------------------------------------------------------------- fn load_commodities(conn: &Connection) -> Vec { let mut stmt = conn .prepare( "SELECT commodity_id, name, tier, base_price, elasticity, production_ubiquity, demand_model FROM commodities ORDER BY commodity_id", ) .expect("prepare commodities"); stmt.query_map([], |row| { Ok(Commodity { id: row.get(0)?, name: row.get(1)?, tier: row.get(2)?, base_price: row.get(3)?, elasticity: row.get(4)?, production_ubiquity: row.get(5)?, demand_model: row.get::<_, Option>(6)?.unwrap_or_default(), }) }) .expect("query commodities") .filter_map(|r| r.ok()) .collect() } fn load_chains(conn: &Connection) -> Vec { let mut chain_stmt = conn .prepare( "SELECT chain_id, output_commodity_id, output_quantity, location_bound FROM production_chains ORDER BY chain_id", ) .expect("prepare chains"); let mut chains: Vec = chain_stmt .query_map([], |row| { Ok(ProductionChain { chain_id: row.get(0)?, output_commodity_id: row.get(1)?, output_quantity: row.get(2)?, location_bound: row.get::<_, i32>(3)? != 0, inputs: Vec::new(), }) }) .expect("query chains") .filter_map(|r| r.ok()) .collect(); // Load inputs for each chain let mut input_stmt = conn .prepare( "SELECT chain_id, input_commodity_id, quantity FROM chain_inputs ORDER BY chain_id, input_commodity_id", ) .expect("prepare chain_inputs"); let all_inputs: Vec<(String, String, f64)> = input_stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) .expect("query chain_inputs") .filter_map(|r| r.ok()) .collect(); // Build index of chain_id → inputs let mut input_map: HashMap> = HashMap::new(); for (chain_id, commodity_id, quantity) in all_inputs { input_map .entry(chain_id) .or_default() .push(ChainInput { commodity_id, quantity }); } for chain in &mut chains { if let Some(inputs) = input_map.remove(&chain.chain_id) { chain.inputs = inputs; } } chains } fn load_systems(conn: &Connection) -> HashMap { let mut stmt = conn .prepare( "SELECT ss.system_id, ss.proper_name, ss.cultural_corridor, ss.gate_energy_connected, COALESCE(se.population, 0) as population, COALESCE(ss.currency_zone, 'TRACTUS_PRIMARY') as currency_zone, COALESCE(sg.hop_distance_from_gateway, 5) as hop_distance, sg.gate_topology, ss.political_zone FROM star_systems ss LEFT JOIN system_economy se ON ss.system_id = se.system_id LEFT JOIN system_gates sg ON ss.system_id = sg.system_id ORDER BY ss.system_id", ) .expect("prepare systems"); stmt.query_map([], |row| { Ok(SystemInfo { system_id: row.get(0)?, proper_name: row.get(1)?, cultural_corridor: row.get(2)?, gate_energy_connected: row.get::<_, Option>(3)?.unwrap_or(1) != 0, population: row.get(4)?, currency_zone: row.get::<_, Option>(5)?.unwrap_or_else(|| "TRACTUS_PRIMARY".to_string()), hop_distance: row.get::<_, Option>(6)?.unwrap_or(5), gate_topology: row.get(7)?, political_zone: row.get(8)?, }) }) .expect("query systems") .filter_map(|r| r.ok()) .map(|s| (s.system_id.clone(), s)) .collect() } fn load_corp_archetype_data( conn: &Connection, ) -> Vec<(String, Option, Option)> { let mut stmt = conn .prepare( "SELECT corp_id, behavioral_archetype, specialization FROM corporations ORDER BY corp_id", ) .expect("prepare corp archetype data"); stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) .expect("query corp archetype data") .filter_map(|r| r.ok()) .collect() } fn load_gate_links(conn: &Connection) -> Vec { let mut stmt = conn .prepare( "SELECT from_system_id, to_system_id FROM gate_links ORDER BY from_system_id, to_system_id", ) .expect("prepare gate_links"); stmt.query_map([], |row| { Ok(GateLink { from_system_id: row.get(0)?, to_system_id: row.get(1)?, }) }) .expect("query gate_links") .filter_map(|r| r.ok()) .collect() } fn load_corp_presences(conn: &Connection) -> Vec { // Resolve body/station location_id back to system_id via LEFT JOINs. // corp_presence.location_type is 'body' | 'station' per schema. let mut stmt = conn .prepare( "SELECT cp.corp_id, COALESCE(b.system_id, s.system_id) AS system_id, cp.primary_operation FROM corp_presence cp LEFT JOIN bodies b ON cp.location_type = 'body' AND cp.location_id = b.body_id LEFT JOIN stations s ON cp.location_type = 'station' AND cp.location_id = s.station_id WHERE COALESCE(b.system_id, s.system_id) IS NOT NULL ORDER BY system_id, cp.corp_id", ) .expect("prepare corp_presence"); stmt.query_map([], |row| { Ok(CorpPresence { corp_id: row.get(0)?, system_id: row.get(1)?, primary_operation: row.get(2)?, }) }) .expect("query corp_presence") .filter_map(|r| r.ok()) .collect() } // --------------------------------------------------------------------------- // Main loader // --------------------------------------------------------------------------- pub fn load_economy(conn: &Connection) -> Economy { let commodities = load_commodities(conn); let commodity_map: HashMap = commodities.iter().map(|c| (c.id.clone(), c.clone())).collect(); let chains = load_chains(conn); let mut chains_by_output: HashMap> = HashMap::new(); for chain in &chains { chains_by_output .entry(chain.output_commodity_id.clone()) .or_default() .push(chain.clone()); } let systems = load_systems(conn); let corp_presences = load_corp_presences(conn); let mut presences_by_system: HashMap> = HashMap::new(); for cp in &corp_presences { presences_by_system .entry(cp.system_id.clone()) .or_default() .push(cp.clone()); } let gate_links = load_gate_links(conn); let corp_archetype_data = load_corp_archetype_data(conn); Economy { commodities, commodity_map, chains, chains_by_output, systems, corp_presences, presences_by_system, gate_links, corp_archetype_data, } }