use serde::Serialize;
#[cfg(feature = "typescript")]
use tsify::Tsify;
use esf_data::{Info, eve};
use crate::calculate::{Calculation, ItemResult};
use crate::fit::{Fit, FitItem, Slot};
mod charge;
mod item;
mod resource;
mod skill;
mod slot;
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct Violation {
pub target: Target,
pub rule: Rule,
}
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Target {
Ship,
Item {
index: usize,
},
Charge {
index: usize,
},
}
#[non_exhaustive]
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Rule {
Resource {
resource: Resource,
used: f64,
available: f64,
},
Slots {
slot: SlotKind,
used: u32,
available: u32,
},
WrongSlot {
expected: SlotKind,
},
SlotTaken,
WrongSlotIndex {
expected: u16,
},
SubsystemTaken,
Skill {
type_id: i32,
required: u8,
level: u8,
},
RigSize {
ship: u8,
item: u8,
},
ShipRestricted,
CapitalItem,
StructureItem,
ShipItem,
MaxGroup {
group_id: i32,
limit: GroupLimit,
used: u32,
allowed: u32,
},
MaxType {
type_id: i32,
used: u32,
allowed: u32,
},
ChargeGroup,
ChargeSize {
module: u8,
charge: u8,
},
}
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Resource {
Cpu,
Powergrid,
Calibration,
DroneBay,
DroneBandwidth,
LaunchedDrones,
FighterBay,
FighterTubes,
LightFighterTubes,
SupportFighterTubes,
HeavyFighterTubes,
CargoBay,
ChargeCapacity,
}
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SlotKind {
High,
Medium,
Low,
Rig,
Subsystem,
Service,
Turret,
Launcher,
}
#[cfg_attr(feature = "typescript", derive(Tsify))]
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GroupLimit {
Fitted,
Online,
Active,
}
pub(crate) fn validate(info: &impl Info, fit: &Fit, calculation: &Calculation) -> Vec<Violation> {
let context = Context::new(info, fit, calculation);
let mut found = Vec::new();
resource::validate(&context, &mut found);
slot::validate(&context, &mut found);
item::validate(&context, &mut found);
charge::validate(&context, &mut found);
skill::validate(&context, &mut found);
found
}
struct Context<'a, I> {
info: &'a I,
fit: &'a Fit,
calculation: &'a Calculation,
items: Vec<Item<'a>>,
}
struct Item<'a> {
index: usize,
fit: &'a FitItem,
result: &'a ItemResult,
group_id: i32,
category_id: i32,
rack: Option<SlotKind>,
hardpoint: Option<SlotKind>,
}
impl<'a, I: Info> Context<'a, I> {
fn new(info: &'a I, fit: &'a Fit, calculation: &'a Calculation) -> Context<'a, I> {
let items = fit
.items
.iter()
.zip(&calculation.items)
.enumerate()
.map(|(index, (fit_item, result))| {
let (rack, hardpoint) = slots_of(info, fit_item.type_id);
Item {
index,
fit: fit_item,
result,
group_id: info
.get_type(fit_item.type_id)
.map_or(0, |r#type| r#type.group_id()),
category_id: info
.get_type(fit_item.type_id)
.map_or(0, |r#type| r#type.category_id()),
rack,
hardpoint,
}
})
.collect();
Context {
info,
fit,
calculation,
items,
}
}
fn attribute_id(&self, name: &str) -> Option<i32> {
self.info.attribute_name_to_id(name)
}
fn value(&self, result: &ItemResult, attribute_id: i32) -> Option<f64> {
result
.attributes
.get(&attribute_id)
.map(|attribute| attribute.value)
}
fn amount(&self, result: &ItemResult, attribute_id: i32) -> f64 {
self.value(result, attribute_id).unwrap_or(0.0)
}
fn base_value(&self, type_id: i32, attribute_id: i32) -> Option<f64> {
self.info
.get_dogma_attributes(type_id)
.into_iter()
.flatten()
.find(|attribute| attribute.attribute_id() == attribute_id)
.map(|attribute| f64::from(attribute.value()))
}
fn ship(&self) -> &ItemResult {
&self.calculation.ship
}
fn ship_group_id(&self) -> i32 {
self.ship_type().map_or(0, |r#type| r#type.group_id())
}
fn ship_category_id(&self) -> i32 {
self.ship_type().map_or(0, |r#type| r#type.category_id())
}
fn ship_type(&self) -> Option<eve::Type<'_>> {
self.info.get_type(self.fit.ship.type_id)
}
}
fn slots_of(info: &impl Info, type_id: i32) -> (Option<SlotKind>, Option<SlotKind>) {
let mut rack = None;
let mut hardpoint = None;
for type_effect in info.get_dogma_effects(type_id).into_iter().flatten() {
let Some(effect) = info.get_dogma_effect(type_effect.effect_id()) else {
continue;
};
match effect.name() {
"hiPower" => rack = Some(SlotKind::High),
"medPower" => rack = Some(SlotKind::Medium),
"loPower" => rack = Some(SlotKind::Low),
"rigSlot" => rack = Some(SlotKind::Rig),
"subSystem" => rack = Some(SlotKind::Subsystem),
"serviceSlot" => rack = Some(SlotKind::Service),
"turretFitted" => hardpoint = Some(SlotKind::Turret),
"launcherFitted" => hardpoint = Some(SlotKind::Launcher),
_ => {}
}
}
(rack, hardpoint)
}
impl Item<'_> {
fn rack(&self) -> Option<SlotKind> {
match self.fit.slot {
Slot::High(_) => Some(SlotKind::High),
Slot::Medium(_) => Some(SlotKind::Medium),
Slot::Low(_) => Some(SlotKind::Low),
Slot::Rig(_) => Some(SlotKind::Rig),
Slot::Subsystem(_) => Some(SlotKind::Subsystem),
Slot::Service(_) => Some(SlotKind::Service),
_ => None,
}
}
fn is_fitted(&self) -> bool {
self.rack().is_some()
}
fn is_on_hull(&self) -> bool {
self.is_fitted() || matches!(self.fit.slot, Slot::FighterTube(_) | Slot::FighterBay)
}
fn is_used(&self) -> bool {
self.fit.slot != Slot::Cargo
}
fn violation(&self, rule: Rule) -> Violation {
Violation {
target: Target::Item { index: self.index },
rule,
}
}
}