ED_LRR/rust/src/ship.rs

254 lines
7.8 KiB
Rust

//! 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<Self, String> {
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::<Event>(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<HashMap<String, Ship>, String> {
let mut ret = HashMap::new();
let re = Regex::new(r"^Journal\.\d{12}\.\d{2}\.log$").unwrap();
let mut journals: Vec<PathBuf> = 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::<Event>(&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<f32> {
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<PyObject> {
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<PyObject> {
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))
}
}