refactor(simulation): split atlas.rs subcommands into modules
Extract 2119-line monolithic atlas.rs into 8 focused modules under src/bin/atlas/: main.rs (thin dispatch), common.rs (shared types and DB helpers), show.rs, mutate.rs, stats.rs, systems.rs, author.rs, and sync_wiki.rs. Commands enum stays in main.rs; each match arm delegates to module::cmd_fn(&conn, args). No behavior change. Closes #776. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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<String>,
|
||||
pub body_type: String,
|
||||
pub orbit_index: Option<i32>,
|
||||
pub proper_name: Option<String>,
|
||||
pub mass_class: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub surface_gravity: Option<f64>,
|
||||
pub biome_summary: Option<String>,
|
||||
pub hydrosphere: Option<String>,
|
||||
pub inhabited: bool,
|
||||
pub population: i64,
|
||||
pub economic_role: Option<String>,
|
||||
pub cultural_corridor: Option<String>,
|
||||
pub industrial_corridor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StationRow {
|
||||
pub station_id: String,
|
||||
pub system_id: String,
|
||||
pub orbits_body_id: Option<String>,
|
||||
pub station_type: String,
|
||||
pub proper_name: Option<String>,
|
||||
pub population: i64,
|
||||
pub economic_role: Option<String>,
|
||||
pub governance_type: Option<String>,
|
||||
pub docking_class: Option<String>,
|
||||
pub has_gate_infrastructure: bool,
|
||||
pub district_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SystemSummary {
|
||||
pub system_id: String,
|
||||
pub proper_name: Option<String>,
|
||||
pub star_type: Option<String>,
|
||||
pub geographic_sector: Option<String>,
|
||||
pub habitable_planet_count: Option<i32>,
|
||||
pub inhabited_planet_count: Option<i32>,
|
||||
pub bodies: Vec<BodyRow>,
|
||||
pub stations: Vec<StationRow>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Database helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn resolve_db_path(explicit: Option<PathBuf>) -> 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<BodyRow> {
|
||||
let mut sql = String::from(
|
||||
"SELECT body_id, system_id, parent_body_id, body_type, orbit_index,
|
||||
proper_name, mass_class, atmosphere, surface_gravity,
|
||||
biome_summary, hydrosphere, inhabited, population,
|
||||
economic_role, cultural_corridor, industrial_corridor
|
||||
FROM bodies WHERE 1=1",
|
||||
);
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = 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)?,
|
||||
biome_summary: row.get(9)?,
|
||||
hydrosphere: row.get(10)?,
|
||||
inhabited: row.get::<_, i32>(11)? != 0,
|
||||
population: row.get::<_, Option<i64>>(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<StationRow> {
|
||||
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<Box<dyn rusqlite::types::ToSql>> = 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<i64>>(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<i32>>(9)?.unwrap_or(0) != 0,
|
||||
district_count: row.get::<_, Option<i32>>(10)?.unwrap_or(1),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn query_stations_for_body(conn: &Connection, body_id: &str) -> Vec<StationRow> {
|
||||
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<i64>>(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<i32>>(9)?.unwrap_or(0) != 0,
|
||||
district_count: row.get::<_, Option<i32>>(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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user