//! Ship fuel consumption and jump range calculations use crate::common::get_fsd_booster_info; use crate::journal::*; use eyre::Result; use pyo3::conversion::ToPyObject; use pyo3::prelude::*; use pyo3::types::PyDict; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; /// Frame Shift Drive information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FSD { /// Rating pub rating_val: f32, /// Class pub class_val: f32, /// Optimized Mass pub opt_mass: f32, /// Max fuel per jump pub max_fuel: f32, /// Boost factor pub boost: f32, /// Guardian booster bonus range pub guardian_booster: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Ship { pub base_mass: f32, pub fuel_mass: f32, pub fuel_capacity: f32, pub fsd: FSD, } impl Ship { pub fn new( base_mass: f32, fuel_mass: f32, fuel_capacity: f32, fsd_type: (char, u8), max_fuel: f32, opt_mass: f32, guardian_booster: usize, ) -> Result { let rating_val: f32 = match fsd_type.0 { 'A' => 12.0, 'B' => 10.0, 'C' => 8.0, 'D' => 10.0, 'E' => 11.0, err => { return Err(format!("Invalid rating: {}", err)); } }; if fsd_type.1 < 2 || fsd_type.1 > 8 { return Err(format!("Invalid class: {}", fsd_type.1)); }; if guardian_booster != 0 { return Err("Guardian booster not yet implemented!".to_owned()); } let ret = Self { fuel_capacity, fuel_mass, base_mass, fsd: FSD { rating_val, class_val: 2.0 + (0.15 * ((fsd_type.1 - 2) as f32)), opt_mass, max_fuel, boost: 1.0, guardian_booster: get_fsd_booster_info(guardian_booster)?, }, }; Ok(ret) } pub fn new_from_json(data: &str) -> Result<(String, Ship), String> { match serde_json::from_str::(data) { Ok(Event { event: EventData::Unknown, }) => { return Err(format!("Invalid Loadout event: {}", data)); } Ok(ev) => { if let Some(loadout) = ev.get_loadout() { return loadout.try_into_ship(); } else { return Err(format!("Invalid Loadout event: {}", data)); } } Err(msg) => { return Err(format!("{}", msg)); } }; } pub fn new_from_journal() -> Result, String> { let mut ret = HashMap::new(); let re = Regex::new(r"^Journal\.\d{12}\.\d{2}\.log$").unwrap(); let mut journals: Vec = Vec::new(); let mut userprofile = PathBuf::from(std::env::var("Userprofile").unwrap()); userprofile.push("Saved Games"); userprofile.push("Frontier Developments"); userprofile.push("Elite Dangerous"); if let Ok(iter) = userprofile.read_dir() { for entry in iter.flatten() { if re.is_match(&entry.file_name().to_string_lossy()) { journals.push(entry.path()); }; } } journals.sort(); for journal in &journals { let mut fh = BufReader::new(File::open(journal).unwrap()); let mut line = String::new(); while let Ok(n) = fh.read_line(&mut line) { if n == 0 { break; } match serde_json::from_str::(&line) { Ok(Event { event: EventData::Unknown, }) => {} Ok(ev) => { if let Some(loadout) = ev.get_loadout() { let (key, ship) = loadout.try_into_ship()?; ret.insert(key, ship); } } Err(_) => {} }; line.clear(); } } if ret.is_empty() { return Err("No ships loaded!".to_owned()); } return Ok(ret); } pub fn can_jump(&self, d: f32) -> bool { self.fuel_cost(d) <= self.fsd.max_fuel.min(self.fuel_mass) } pub fn boost(&mut self, boost: f32) { self.fsd.boost = boost; } pub fn refuel(&mut self) { self.fuel_mass = self.fuel_capacity; } pub fn make_jump(&mut self, d: f32) -> Option { let cost = self.fuel_cost(d); if cost > self.fsd.max_fuel.min(self.fuel_mass) { return None; } self.fuel_mass -= cost; self.fsd.boost = 1.0; Some(cost) } pub fn jump_range(&self, fuel: f32, booster: bool) -> f32 { let mass = self.base_mass + fuel; let mut fuel = self.fsd.max_fuel.min(fuel); if booster { fuel *= self.boost_fuel_mult(); } let opt_mass = self.fsd.opt_mass * self.fsd.boost; return opt_mass * ((1000.0 * fuel) / self.fsd.rating_val).powf(self.fsd.class_val.recip()) / mass; } pub fn max_range(&self) -> f32 { return self.jump_range(self.fsd.max_fuel, true); } pub fn range(&self) -> f32 { return self.jump_range(self.fuel_mass, true); } pub fn full_range(&self) -> f32 { return self.jump_range(self.fuel_capacity, true); } fn boost_fuel_mult(&self) -> f32 { if self.fsd.guardian_booster == 0.0 { return 1.0; } let base_range = self.jump_range(self.fuel_mass, false); // current range without booster return ((base_range + self.fsd.guardian_booster) / base_range).powf(self.fsd.class_val); } pub fn fuel_cost_for_jump(&self, fuel_mass: f32, dist: f32, boost: f32) -> Option<(f32, f32)> { if dist == 0.0 { return Some((0.0, 0.0)); } let mass = self.base_mass + fuel_mass; let opt_mass = self.fsd.opt_mass * boost; let base_cost = (dist * mass) / opt_mass; let fuel_cost = (self.fsd.rating_val * 0.001 * base_cost.powf(self.fsd.class_val)) / self.boost_fuel_mult(); if fuel_cost > self.fsd.max_fuel || fuel_cost > fuel_mass { return None; }; return Some((fuel_cost, fuel_mass - fuel_cost)); } pub fn fuel_cost(&self, d: f32) -> f32 { if d == 0.0 { return 0.0; } let mass = self.base_mass + self.fuel_mass; let opt_mass = self.fsd.opt_mass * self.fsd.boost; let base_cost = (d * mass) / opt_mass; return (self.fsd.rating_val * 0.001 * base_cost.powf(self.fsd.class_val)) / self.boost_fuel_mult(); } } impl FSD { pub fn to_object(&self, py: Python) -> PyResult { let elem = PyDict::new(py); elem.set_item("rating_val", self.rating_val)?; elem.set_item("class_val", self.class_val)?; elem.set_item("opt_mass", self.opt_mass)?; elem.set_item("max_fuel", self.max_fuel)?; elem.set_item("boost", self.boost)?; elem.set_item("guardian_booster", self.guardian_booster)?; Ok(elem.to_object(py)) } } impl Ship { pub fn to_object(&self, py: Python) -> PyResult { let elem = PyDict::new(py); elem.set_item("base_mass", self.base_mass)?; elem.set_item("fuel_mass", self.fuel_mass)?; elem.set_item("fuel_capacity", self.fuel_capacity)?; elem.set_item("fsd", self.fsd.to_object(py)?)?; Ok(elem.to_object(py)) } }