"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>
245 lines
8.1 KiB
Rust
245 lines
8.1 KiB
Rust
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 planet_class: 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,
|
|
planet_class, 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)?,
|
|
planet_class: 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)
|
|
}
|
|
}
|