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,319 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::common::format_population;
|
||||
|
||||
pub fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) {
|
||||
// Get list of systems to sync
|
||||
let system_ids: Vec<String> = if let Some(sid) = system_id {
|
||||
vec![sid.to_string()]
|
||||
} else {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id")
|
||||
.unwrap();
|
||||
stmt.query_map([], |row| row.get::<_, String>(0))
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut synced = 0;
|
||||
|
||||
for sid in &system_ids {
|
||||
let sid_slug = sid.replace(' ', "-");
|
||||
let wiki_path = std::path::Path::new(wiki_dir)
|
||||
.join(&sid_slug)
|
||||
.join("index.md");
|
||||
if !wiki_path.exists() {
|
||||
eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Query bodies
|
||||
let mut body_stmt = conn
|
||||
.prepare(
|
||||
"SELECT body_id, proper_name, body_type, orbit_index, parent_body_id,
|
||||
inhabited, population, mass_class, atmosphere, surface_gravity,
|
||||
orbital_period_days, rotation_period_hours,
|
||||
biome_summary, hydrosphere, economic_role, settlement_pattern,
|
||||
industrial_corridor
|
||||
FROM bodies WHERE system_id = ?1
|
||||
ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
struct Body {
|
||||
id: String,
|
||||
name: Option<String>,
|
||||
btype: String,
|
||||
orbit: i32,
|
||||
parent: Option<String>,
|
||||
inhabited: bool,
|
||||
population: i64,
|
||||
mass_class: Option<String>,
|
||||
atmosphere: Option<String>,
|
||||
gravity: Option<f64>,
|
||||
orbital_days: Option<f64>,
|
||||
rotation_hours: Option<f64>,
|
||||
biome: Option<String>,
|
||||
hydro: Option<String>,
|
||||
econ: Option<String>,
|
||||
settlement: Option<String>,
|
||||
industrial: Option<String>,
|
||||
}
|
||||
|
||||
let bodies: Vec<Body> = body_stmt
|
||||
.query_map(params![sid], |row| {
|
||||
Ok(Body {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
btype: row.get(2)?,
|
||||
orbit: row.get::<_, Option<i32>>(3)?.unwrap_or(0),
|
||||
parent: row.get(4)?,
|
||||
inhabited: row.get::<_, i32>(5)? != 0,
|
||||
population: row.get::<_, Option<i64>>(6)?.unwrap_or(0),
|
||||
mass_class: row.get(7)?,
|
||||
atmosphere: row.get(8)?,
|
||||
gravity: row.get(9)?,
|
||||
orbital_days: row.get(10)?,
|
||||
rotation_hours: row.get(11)?,
|
||||
biome: row.get(12)?,
|
||||
hydro: row.get(13)?,
|
||||
econ: row.get(14)?,
|
||||
settlement: row.get(15)?,
|
||||
industrial: row.get(16)?,
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
// Query stations
|
||||
let mut station_stmt = conn
|
||||
.prepare(
|
||||
"SELECT station_id, proper_name, orbits_body_id, station_type,
|
||||
population, economic_role, governance_type, docking_class,
|
||||
has_gate_infrastructure, district_count
|
||||
FROM stations WHERE system_id = ?1
|
||||
ORDER BY station_id",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
struct Station {
|
||||
id: String,
|
||||
name: Option<String>,
|
||||
orbits: Option<String>,
|
||||
stype: String,
|
||||
population: i64,
|
||||
econ: Option<String>,
|
||||
governance: Option<String>,
|
||||
docking: Option<String>,
|
||||
gate: bool,
|
||||
districts: i32,
|
||||
}
|
||||
|
||||
let stations: Vec<Station> = station_stmt
|
||||
.query_map(params![sid], |row| {
|
||||
Ok(Station {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
orbits: row.get(2)?,
|
||||
stype: row.get(3)?,
|
||||
population: row.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||||
econ: row.get(5)?,
|
||||
governance: row.get(6)?,
|
||||
docking: row.get(7)?,
|
||||
gate: row.get::<_, Option<i32>>(8)?.unwrap_or(0) != 0,
|
||||
districts: row.get::<_, Option<i32>>(9)?.unwrap_or(1),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
if bodies.is_empty() && stations.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build the Celestial Bodies section
|
||||
let mut section = String::new();
|
||||
section.push_str("## Celestial Bodies\n");
|
||||
section
|
||||
.push_str("<!-- READ-ONLY — generated from systems.db bodies/stations tables -->\n\n");
|
||||
|
||||
// Bodies table
|
||||
if !bodies.is_empty() {
|
||||
section.push_str("| Orbit | ID | Name | Type | Inhabited | Pop | Mass | Gravity | Year (d) | Day (h) | Atmo | Biome | Hydro | Economy | Settlement | Industrial |\n");
|
||||
section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n");
|
||||
|
||||
// First pass: top-level bodies (no parent)
|
||||
for b in &bodies {
|
||||
if b.parent.is_some() {
|
||||
continue;
|
||||
}
|
||||
let name = b.name.as_deref().unwrap_or("—");
|
||||
let pop = if b.population > 0 {
|
||||
format_population(b.population)
|
||||
} else {
|
||||
"—".to_string()
|
||||
};
|
||||
let grav = b
|
||||
.gravity
|
||||
.map(|g| format!("{:.2}g", g))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let year = b
|
||||
.orbital_days
|
||||
.map(|d| format!("{:.0}", d))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let day = b
|
||||
.rotation_hours
|
||||
.map(|h| format!("{:.1}", h))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
section.push_str(&format!(
|
||||
"| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
|
||||
b.orbit,
|
||||
b.id,
|
||||
name,
|
||||
b.btype,
|
||||
if b.inhabited { "yes" } else { "no" },
|
||||
pop,
|
||||
b.mass_class.as_deref().unwrap_or("—"),
|
||||
grav,
|
||||
year,
|
||||
day,
|
||||
b.atmosphere.as_deref().unwrap_or("—"),
|
||||
b.biome.as_deref().unwrap_or("—"),
|
||||
b.hydro.as_deref().unwrap_or("—"),
|
||||
b.econ.as_deref().unwrap_or("—"),
|
||||
b.settlement.as_deref().unwrap_or("—"),
|
||||
b.industrial.as_deref().unwrap_or("—"),
|
||||
));
|
||||
|
||||
// Child bodies (moons)
|
||||
for m in &bodies {
|
||||
if m.parent.as_deref() == Some(&b.id) {
|
||||
let mname = m.name.as_deref().unwrap_or("—");
|
||||
let mpop = if m.population > 0 {
|
||||
format_population(m.population)
|
||||
} else {
|
||||
"—".to_string()
|
||||
};
|
||||
let mgrav = m
|
||||
.gravity
|
||||
.map(|g| format!("{:.2}g", g))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let myear = m
|
||||
.orbital_days
|
||||
.map(|d| format!("{:.0}", d))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let mday = m
|
||||
.rotation_hours
|
||||
.map(|h| format!("{:.1}", h))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
section.push_str(&format!(
|
||||
"| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
|
||||
b.orbit,
|
||||
m.orbit,
|
||||
m.id,
|
||||
mname,
|
||||
m.btype,
|
||||
if m.inhabited { "yes" } else { "no" },
|
||||
mpop,
|
||||
m.mass_class.as_deref().unwrap_or("—"),
|
||||
mgrav,
|
||||
myear,
|
||||
mday,
|
||||
m.atmosphere.as_deref().unwrap_or("—"),
|
||||
m.biome.as_deref().unwrap_or("—"),
|
||||
m.hydro.as_deref().unwrap_or("—"),
|
||||
m.econ.as_deref().unwrap_or("—"),
|
||||
m.settlement.as_deref().unwrap_or("—"),
|
||||
m.industrial.as_deref().unwrap_or("—"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
section.push('\n');
|
||||
}
|
||||
|
||||
// Stations table
|
||||
if !stations.is_empty() {
|
||||
section.push_str("### Stations & Facilities\n\n");
|
||||
section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n");
|
||||
section.push_str("|---|---|---|---|---|---|---|---|---|---|\n");
|
||||
for s in &stations {
|
||||
let name = s.name.as_deref().unwrap_or("—");
|
||||
let orbits = s.orbits.as_deref().unwrap_or("—");
|
||||
let pop = if s.population > 0 {
|
||||
format_population(s.population)
|
||||
} else {
|
||||
"—".to_string()
|
||||
};
|
||||
section.push_str(&format!(
|
||||
"| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n",
|
||||
s.id,
|
||||
name,
|
||||
s.stype,
|
||||
orbits,
|
||||
pop,
|
||||
s.econ.as_deref().unwrap_or("—"),
|
||||
s.governance.as_deref().unwrap_or("—"),
|
||||
s.docking.as_deref().unwrap_or("—"),
|
||||
if s.gate { "yes" } else { "no" },
|
||||
s.districts,
|
||||
));
|
||||
}
|
||||
section.push('\n');
|
||||
}
|
||||
|
||||
// Read current wiki content
|
||||
let content = std::fs::read_to_string(&wiki_path).unwrap();
|
||||
|
||||
// Replace or insert the Celestial Bodies section
|
||||
let marker_start = "## Celestial Bodies";
|
||||
let new_content = if let Some(start_pos) = content.find(marker_start) {
|
||||
// Find the next ## heading after the section (or end of file)
|
||||
let after = &content[start_pos + marker_start.len()..];
|
||||
let end_offset = after
|
||||
.find("\n## ")
|
||||
.map(|p| start_pos + marker_start.len() + p + 1)
|
||||
.unwrap_or(content.len());
|
||||
format!(
|
||||
"{}{}\n{}",
|
||||
&content[..start_pos],
|
||||
section.trim_end(),
|
||||
&content[end_offset..]
|
||||
)
|
||||
} else {
|
||||
// Insert before ## Topology if it exists, otherwise before end of file
|
||||
if let Some(topo_pos) = content.find("\n## Topology") {
|
||||
let insert_pos = topo_pos + 1; // after the newline
|
||||
format!(
|
||||
"{}{}\n\n{}",
|
||||
&content[..insert_pos],
|
||||
section.trim_end(),
|
||||
&content[insert_pos..]
|
||||
)
|
||||
} else {
|
||||
// No topology section — append at end
|
||||
format!("{}\n{}", content.trim_end(), section)
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::write(&wiki_path, new_content).unwrap();
|
||||
eprintln!("synced: {} → {}", sid, wiki_path.display());
|
||||
synced += 1;
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SyncResult {
|
||||
systems_synced: usize,
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&SyncResult {
|
||||
systems_synced: synced
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user