Files
settled-reach/server/src/bin/atlas/common.rs
T
jpmschweitzerandClaude Opus 4.6 db241b88bd refactor(schema): rename biome_summary to planet_class (D-188)
"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>
2026-04-06 17:10:35 +02:00

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)
}
}