use std::path::PathBuf; use std::process; use rusqlite::{params, Connection}; use serde::Serialize; // --------------------------------------------------------------------------- // Shared output types // --------------------------------------------------------------------------- #[derive(Serialize)] pub struct BodyRow { pub body_id: String, pub system_id: String, pub parent_body_id: Option, pub body_type: String, pub orbit_index: Option, pub proper_name: Option, pub mass_class: Option, pub atmosphere: Option, pub surface_gravity: Option, pub planet_class: Option, pub hydrosphere: Option, pub inhabited: bool, pub population: i64, pub economic_role: Option, pub cultural_corridor: Option, pub industrial_corridor: Option, } #[derive(Serialize)] pub struct StationRow { pub station_id: String, pub system_id: String, pub orbits_body_id: Option, pub station_type: String, pub proper_name: Option, pub population: i64, pub economic_role: Option, pub governance_type: Option, pub docking_class: Option, pub has_gate_infrastructure: bool, pub district_count: i32, } #[derive(Serialize)] pub struct SystemSummary { pub system_id: String, pub proper_name: Option, pub star_type: Option, pub geographic_sector: Option, pub habitable_planet_count: Option, pub inhabited_planet_count: Option, pub bodies: Vec, pub stations: Vec, } // --------------------------------------------------------------------------- // Database helpers // --------------------------------------------------------------------------- pub fn resolve_db_path(explicit: Option) -> PathBuf { if let Some(p) = explicit { return p; } // Walk up from CWD looking for server/data/systems.db 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;") .unwrap(); conn } // --------------------------------------------------------------------------- // Shared query helpers // --------------------------------------------------------------------------- pub fn query_bodies( conn: &Connection, system: Option<&str>, body_type: Option<&str>, inhabited_only: bool, unnamed_only: bool, ) -> Vec { let mut sql = String::from( "SELECT body_id, system_id, parent_body_id, body_type, orbit_index, proper_name, mass_class, atmosphere, surface_gravity, planet_class, hydrosphere, inhabited, population, economic_role, cultural_corridor, industrial_corridor FROM bodies WHERE 1=1", ); let mut param_values: Vec> = Vec::new(); if let Some(s) = system { sql.push_str(" AND system_id = ?"); param_values.push(Box::new(s.to_string())); } if let Some(t) = body_type { sql.push_str(" AND body_type = ?"); param_values.push(Box::new(t.to_string())); } if inhabited_only { sql.push_str(" AND inhabited = 1"); } if unnamed_only { sql.push_str(" AND proper_name IS NULL"); } sql.push_str(" ORDER BY system_id, orbit_index"); let params_ref: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|p| p.as_ref()).collect(); let mut stmt = conn.prepare(&sql).unwrap(); stmt.query_map(params_ref.as_slice(), |row| { Ok(BodyRow { body_id: row.get(0)?, system_id: row.get(1)?, parent_body_id: row.get(2)?, body_type: row.get(3)?, orbit_index: row.get(4)?, proper_name: row.get(5)?, mass_class: row.get(6)?, atmosphere: row.get(7)?, surface_gravity: row.get(8)?, planet_class: row.get(9)?, hydrosphere: row.get(10)?, inhabited: row.get::<_, i32>(11)? != 0, population: row.get::<_, Option>(12)?.unwrap_or(0), economic_role: row.get(13)?, cultural_corridor: row.get(14)?, industrial_corridor: row.get(15)?, }) }) .unwrap() .filter_map(|r| r.ok()) .collect() } pub fn query_stations( conn: &Connection, system: Option<&str>, station_type: Option<&str>, ) -> Vec { let mut sql = String::from( "SELECT station_id, system_id, orbits_body_id, station_type, proper_name, population, economic_role, governance_type, docking_class, has_gate_infrastructure, district_count FROM stations WHERE 1=1", ); let mut param_values: Vec> = Vec::new(); if let Some(s) = system { sql.push_str(" AND system_id = ?"); param_values.push(Box::new(s.to_string())); } if let Some(t) = station_type { sql.push_str(" AND station_type = ?"); param_values.push(Box::new(t.to_string())); } sql.push_str(" ORDER BY system_id, station_id"); let params_ref: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|p| p.as_ref()).collect(); let mut stmt = conn.prepare(&sql).unwrap(); stmt.query_map(params_ref.as_slice(), |row| { Ok(StationRow { station_id: row.get(0)?, system_id: row.get(1)?, orbits_body_id: row.get(2)?, station_type: row.get(3)?, proper_name: row.get(4)?, population: row.get::<_, Option>(5)?.unwrap_or(0), economic_role: row.get(6)?, governance_type: row.get(7)?, docking_class: row.get(8)?, has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, district_count: row.get::<_, Option>(10)?.unwrap_or(1), }) }) .unwrap() .filter_map(|r| r.ok()) .collect() } pub fn query_stations_for_body(conn: &Connection, body_id: &str) -> Vec { let mut stmt = conn .prepare( "SELECT station_id, system_id, orbits_body_id, station_type, proper_name, population, economic_role, governance_type, docking_class, has_gate_infrastructure, district_count FROM stations WHERE orbits_body_id = ?1 ORDER BY station_id", ) .unwrap(); stmt.query_map(params![body_id], |row| { Ok(StationRow { station_id: row.get(0)?, system_id: row.get(1)?, orbits_body_id: row.get(2)?, station_type: row.get(3)?, proper_name: row.get(4)?, population: row.get::<_, Option>(5)?.unwrap_or(0), economic_role: row.get(6)?, governance_type: row.get(7)?, docking_class: row.get(8)?, has_gate_infrastructure: row.get::<_, Option>(9)?.unwrap_or(0) != 0, district_count: row.get::<_, Option>(10)?.unwrap_or(1), }) }) .unwrap() .filter_map(|r| r.ok()) .collect() } // --------------------------------------------------------------------------- // Shared formatting // --------------------------------------------------------------------------- pub fn format_population(pop: i64) -> String { if pop >= 1_000_000_000 { format!("{:.1}B", pop as f64 / 1_000_000_000.0) } else if pop >= 1_000_000 { format!("{}M", pop / 1_000_000) } else if pop >= 1_000 { format!("{}K", pop / 1_000) } else { format!("{}", pop) } }