"Biome" describes per-zone vegetation classification (Whittaker table). "Planet class" describes overall planetary character. The conflation caused the planet generator to misclassify ~270 bodies as barren. Scope: systems.db column, schema SQL, Rust atlas code, wiki table headers (Biome → Class), atlas proposal JSONs, all docs/decisions, tooling scripts. Also normalizes atmosphere vocabulary (breathable → standard) and expands planet class mapping to all 26 wiki values. Unknown classes default to temperate for modder safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
320 lines
12 KiB
Rust
320 lines
12 KiB
Rust
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,
|
|
planet_class, 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()
|
|
);
|
|
}
|