use anyhow::{bail, Context, Result}; use galactica_util::to_radians; use nalgebra::{Point2, Point3}; use std::collections::{HashMap, HashSet}; use crate::{ handle::SpriteHandle, util::Polar, Content, ContentBuildContext, SystemHandle, SystemObjectHandle, }; pub(crate) mod syntax { use serde::Deserialize; use std::collections::HashMap; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] pub struct System { pub object: HashMap, } #[derive(Debug, Deserialize)] pub struct Object { pub sprite: String, pub position: Position, pub size: f32, pub radius: Option, pub angle: Option, pub landable: Option, pub name: Option, pub desc: Option, } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Position { Polar(PolarCoords), Cartesian(CoordinatesThree), } #[derive(Debug, Deserialize)] pub struct PolarCoords { pub center: CoordinatesTwo, pub radius: f32, pub angle: f32, pub z: f32, } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CoordinatesTwo { Label(String), Coords([f32; 2]), } impl ToString for CoordinatesTwo { fn to_string(&self) -> String { match self { Self::Label(s) => s.to_owned(), Self::Coords(v) => format!("{:?}", v), } } } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CoordinatesThree { Label(String), Coords([f32; 3]), } impl ToString for CoordinatesThree { fn to_string(&self) -> String { match self { Self::Label(s) => s.to_owned(), Self::Coords(v) => format!("{:?}", v), } } } } // Processed data structs. // These are exported. /// Represents a star system #[derive(Debug, Clone)] pub struct System { /// This star system's name pub name: String, /// This star system's handle pub handle: SystemHandle, /// Objects in this system pub objects: Vec, } /// Represents an orbiting body in a star system /// (A star, planet, moon, satellite, etc) /// These may be landable and may be decorative. /// System objects to not interact with the physics engine. #[derive(Debug, Clone)] pub struct SystemObject { /// This object's sprite pub sprite: SpriteHandle, /// This object's handle pub handle: SystemObjectHandle, /// This object's size. /// Measured as height in game units. /// This value is scaled for distance /// (i.e, the z-value of position) pub size: f32, /// This object's position, in game coordinates, /// relative to the system's center (0, 0). pub pos: Point3, /// This object's sprite's angle, in radians pub angle: f32, /// If true, ships may land on this object pub landable: bool, /// The display name of this object pub name: String, /// The description of this object pub desc: String, } /// Helper function for resolve_position, never called on its own. fn resolve_coordinates( objects: &HashMap, cor: &syntax::CoordinatesThree, mut cycle_detector: HashSet, ) -> Result> { match cor { syntax::CoordinatesThree::Coords(c) => Ok((*c).into()), syntax::CoordinatesThree::Label(l) => { if cycle_detector.contains(l) { bail!( "Found coordinate cycle: `{}`", cycle_detector.iter().fold(String::new(), |sum, a| { if sum.is_empty() { a.to_string() } else { sum + " -> " + a } }) ); } cycle_detector.insert(l.to_owned()); let p = match objects.get(l) { Some(p) => p, None => bail!("Could not resolve coordinate label `{l}`"), }; Ok(resolve_position(&objects, &p, cycle_detector) .with_context(|| format!("in object {:#?}", l))?) } } } /// Given an object, resolve its position as a Point3. fn resolve_position( objects: &HashMap, obj: &syntax::Object, cycle_detector: HashSet, ) -> Result> { match &obj.position { syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?), syntax::Position::Polar(p) => { let three = match &p.center { syntax::CoordinatesTwo::Label(s) => syntax::CoordinatesThree::Label(s.clone()), syntax::CoordinatesTwo::Coords(v) => { syntax::CoordinatesThree::Coords([v[0], v[1], f32::NAN]) } }; let r = resolve_coordinates(&objects, &three, cycle_detector)?; let plane = Polar { center: Point2::new(r.x, r.y), radius: p.radius, angle: to_radians(p.angle), } .to_cartesian(); Ok(Point3::new(plane.x, plane.y, p.z)) } } } impl crate::Build for System { type InputSyntaxType = HashMap; fn build( system: Self::InputSyntaxType, _build_context: &mut ContentBuildContext, content: &mut Content, ) -> Result<()> { for (system_name, system) in system { let mut objects = Vec::new(); let system_handle = SystemHandle { index: content.systems.len(), }; for (label, obj) in &system.object { let mut cycle_detector = HashSet::new(); cycle_detector.insert(label.clone()); let sprite_handle = match content.sprite_index.get(&obj.sprite) { None => bail!( "In system `{}`: sprite `{}` doesn't exist", system_name, obj.sprite ), Some(t) => *t, }; objects.push(SystemObject { sprite: sprite_handle, pos: resolve_position(&system.object, &obj, cycle_detector) .with_context(|| format!("in object {:#?}", label))?, size: obj.size, angle: to_radians(obj.angle.unwrap_or(0.0)), handle: SystemObjectHandle { system_handle, body_index: 0, }, landable: obj.landable.unwrap_or(false), name: obj .name .as_ref() .map(|x| x.clone()) .unwrap_or("".to_string()), // TODO: better linebreaks, handle double spaces // Tabs desc: obj .desc .as_ref() .map(|x| x.replace("\n", " ").replace("
", "\n")) .unwrap_or("".to_string()), }); } // Sort by z-distance. This is important, since these are // rendered in this order. We need far objects to be behind // near objects! objects.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z)); // Update object handles let mut i = 0; for o in &mut objects { o.handle.body_index = i; i += 1; } content.systems.push(Self { handle: system_handle, name: system_name, objects, }); } return Ok(()); } }