Skip to main content

esf_dogma_engine/validate/
mod.rs

1//! Checks a calculated fit against EVE's fitting rules.
2
3use serde::Serialize;
4
5#[cfg(feature = "typescript")]
6use tsify::Tsify;
7
8use esf_data::{Info, eve};
9
10use crate::calculate::{Calculation, ItemResult};
11use crate::fit::{Fit, FitItem, Slot};
12
13mod charge;
14mod item;
15mod resource;
16mod skill;
17mod slot;
18
19/// One rule the fit breaks, and what breaks it.
20#[cfg_attr(feature = "typescript", derive(Tsify))]
21#[derive(Serialize, Debug, Clone, PartialEq)]
22pub struct Violation {
23    /// What the rule is about.
24    pub target: Target,
25    /// The rule, and the values that failed it.
26    pub rule: Rule,
27}
28
29/// What a [`Violation`] is about. `Item` and `Charge` index into `Fit::items`.
30#[cfg_attr(feature = "typescript", derive(Tsify))]
31#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
32#[serde(tag = "type", rename_all = "snake_case")]
33pub enum Target {
34    /// The ship, for what it carries as a whole.
35    Ship,
36    /// An item of the fit.
37    Item {
38        /// The position in `Fit::items`.
39        index: usize,
40    },
41    /// The charge in an item of the fit.
42    Charge {
43        /// The position in `Fit::items` of the item holding the charge.
44        index: usize,
45    },
46}
47
48/// A rule of EVE's, and the values that failed it.
49///
50/// What an item would accept instead is not repeated here; it is on the item
51/// itself, as `chargeGroup1`, `canFitShipType1` and the like.
52#[non_exhaustive]
53#[cfg_attr(feature = "typescript", derive(Tsify))]
54#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
55#[serde(tag = "type", rename_all = "snake_case")]
56pub enum Rule {
57    /// More of a fitting resource used than the ship has.
58    Resource {
59        /// Which resource ran out.
60        resource: Resource,
61        /// How much is used.
62        used: f64,
63        /// How much there is.
64        available: f64,
65    },
66    /// More items in a rack than the ship has slots.
67    Slots {
68        /// Which rack is full.
69        slot: SlotKind,
70        /// How many items are in it.
71        used: u32,
72        /// How many fit.
73        available: u32,
74    },
75    /// The item belongs in another kind of slot.
76    WrongSlot {
77        /// The rack the item asks for.
78        expected: SlotKind,
79    },
80    /// Another item of the fit is in this slot too.
81    SlotTaken,
82    /// An implant or booster in a slot other than the one it occupies. The
83    /// number is `implantness` or `boosterness`.
84    WrongSlotIndex {
85        /// The slot the item belongs in.
86        expected: u16,
87    },
88    /// Another subsystem covers the same part of the ship.
89    SubsystemTaken,
90    /// A skill the character is missing, or has not trained far enough.
91    Skill {
92        /// The type id of the skill.
93        type_id: i32,
94        /// The level the item asks for.
95        required: u8,
96        /// The level the character has; 0 when untrained.
97        level: u8,
98    },
99    /// A rig of another size than the ship takes.
100    RigSize {
101        /// The size the ship takes.
102        ship: u8,
103        /// The size of the rig.
104        item: u8,
105    },
106    /// The item cannot go on this ship at all.
107    ShipRestricted,
108    /// A capital item on a ship that is not a capital.
109    CapitalItem,
110    /// A structure module on something that is not a structure.
111    StructureItem,
112    /// A module of a ship on a structure.
113    ShipItem,
114    /// More of a group than the ship may hold in that state.
115    MaxGroup {
116        /// The group the limit is on.
117        group_id: i32,
118        /// The state the limit counts.
119        limit: GroupLimit,
120        /// How many are in that state.
121        used: u32,
122        /// How many may be.
123        allowed: u32,
124    },
125    /// More of one type fitted than allowed.
126    MaxType {
127        /// The type the limit is on.
128        type_id: i32,
129        /// How many are fitted.
130        used: u32,
131        /// How many may be.
132        allowed: u32,
133    },
134    /// A charge of a group the module does not take.
135    ChargeGroup,
136    /// A charge of another size than the module takes.
137    ChargeSize {
138        /// The size the module takes.
139        module: u8,
140        /// The size of the charge.
141        charge: u8,
142    },
143}
144
145/// A resource the ship only has so much of.
146#[cfg_attr(feature = "typescript", derive(Tsify))]
147#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
148#[serde(rename_all = "snake_case")]
149pub enum Resource {
150    /// CPU.
151    Cpu,
152    /// Powergrid.
153    Powergrid,
154    /// Calibration, which the rigs take up.
155    Calibration,
156    /// Room in the drone bay.
157    DroneBay,
158    /// Bandwidth for the drones in space.
159    DroneBandwidth,
160    /// Drones in space at once.
161    LaunchedDrones,
162    /// Room in the fighter bay.
163    FighterBay,
164    /// Fighter tubes.
165    FighterTubes,
166    /// Tubes that take a light squadron.
167    LightFighterTubes,
168    /// Tubes that take a support squadron.
169    SupportFighterTubes,
170    /// Tubes that take a heavy squadron.
171    HeavyFighterTubes,
172    /// Room in the cargo hold.
173    CargoBay,
174    /// Room in the module for the charge it holds.
175    ChargeCapacity,
176}
177
178/// A rack of slots, or a hardpoint a weapon needs.
179#[cfg_attr(feature = "typescript", derive(Tsify))]
180#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
181#[serde(rename_all = "snake_case")]
182pub enum SlotKind {
183    /// A high slot.
184    High,
185    /// A medium slot.
186    Medium,
187    /// A low slot.
188    Low,
189    /// A rig slot.
190    Rig,
191    /// A subsystem slot.
192    Subsystem,
193    /// A service slot, on a structure.
194    Service,
195    /// A turret hardpoint.
196    Turret,
197    /// A launcher hardpoint.
198    Launcher,
199}
200
201/// Which state a group limit counts.
202#[cfg_attr(feature = "typescript", derive(Tsify))]
203#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
204#[serde(rename_all = "snake_case")]
205pub enum GroupLimit {
206    /// Fitted at all.
207    Fitted,
208    /// Online or higher.
209    Online,
210    /// Active or higher.
211    Active,
212}
213
214/// Report every rule the fit breaks.
215///
216/// The calculation has to be the one [`calculate()`](crate::calculate) made
217/// for this fit: a rule reads the values after skills and modules changed
218/// them, so a gun's powergrid is the one its owner really pays.
219///
220/// Violations come out grouped by the kind of rule, and within a kind in the
221/// order of `Fit::items`. An empty result means the fit breaks nothing.
222pub(crate) fn validate(info: &impl Info, fit: &Fit, calculation: &Calculation) -> Vec<Violation> {
223    let context = Context::new(info, fit, calculation);
224
225    let mut found = Vec::new();
226    resource::validate(&context, &mut found);
227    slot::validate(&context, &mut found);
228    item::validate(&context, &mut found);
229    charge::validate(&context, &mut found);
230    skill::validate(&context, &mut found);
231    found
232}
233
234/// The fit and its calculation side by side, with what the SDE says about
235/// each item gathered once rather than per rule.
236struct Context<'a, I> {
237    info: &'a I,
238    fit: &'a Fit,
239    calculation: &'a Calculation,
240    items: Vec<Item<'a>>,
241}
242
243/// One item of the fit: what was asked for, what it calculated to, and what
244/// only the SDE knows about it.
245struct Item<'a> {
246    index: usize,
247    fit: &'a FitItem,
248    result: &'a ItemResult,
249    group_id: i32,
250    category_id: i32,
251    rack: Option<SlotKind>,
252    hardpoint: Option<SlotKind>,
253}
254
255impl<'a, I: Info> Context<'a, I> {
256    fn new(info: &'a I, fit: &'a Fit, calculation: &'a Calculation) -> Context<'a, I> {
257        let items = fit
258            .items
259            .iter()
260            .zip(&calculation.items)
261            .enumerate()
262            .map(|(index, (fit_item, result))| {
263                let (rack, hardpoint) = slots_of(info, fit_item.type_id);
264                Item {
265                    index,
266                    fit: fit_item,
267                    result,
268                    group_id: info
269                        .get_type(fit_item.type_id)
270                        .map_or(0, |r#type| r#type.group_id()),
271                    category_id: info
272                        .get_type(fit_item.type_id)
273                        .map_or(0, |r#type| r#type.category_id()),
274                    rack,
275                    hardpoint,
276                }
277            })
278            .collect();
279
280        Context {
281            info,
282            fit,
283            calculation,
284            items,
285        }
286    }
287
288    fn attribute_id(&self, name: &str) -> Option<i32> {
289        self.info.attribute_name_to_id(name)
290    }
291
292    /// What the attribute reached, or `None` when the item does not carry it.
293    fn value(&self, result: &ItemResult, attribute_id: i32) -> Option<f64> {
294        result
295            .attributes
296            .get(&attribute_id)
297            .map(|attribute| attribute.value)
298    }
299
300    /// What the attribute reached, reading one the item does not carry as
301    /// zero. That is what a resource nothing uses adds up to.
302    fn amount(&self, result: &ItemResult, attribute_id: i32) -> f64 {
303        self.value(result, attribute_id).unwrap_or(0.0)
304    }
305
306    /// What an attribute starts at on a type the fit does not hold.
307    fn base_value(&self, type_id: i32, attribute_id: i32) -> Option<f64> {
308        self.info
309            .get_dogma_attributes(type_id)
310            .into_iter()
311            .flatten()
312            .find(|attribute| attribute.attribute_id() == attribute_id)
313            .map(|attribute| f64::from(attribute.value()))
314    }
315
316    fn ship(&self) -> &ItemResult {
317        &self.calculation.ship
318    }
319
320    fn ship_group_id(&self) -> i32 {
321        self.ship_type().map_or(0, |r#type| r#type.group_id())
322    }
323
324    fn ship_category_id(&self) -> i32 {
325        self.ship_type().map_or(0, |r#type| r#type.category_id())
326    }
327
328    fn ship_type(&self) -> Option<eve::Type<'_>> {
329        self.info.get_type(self.fit.ship.type_id)
330    }
331}
332
333/// The slot effects EVE marks an item with. An item carries at most one of
334/// each kind.
335fn slots_of(info: &impl Info, type_id: i32) -> (Option<SlotKind>, Option<SlotKind>) {
336    let mut rack = None;
337    let mut hardpoint = None;
338
339    for type_effect in info.get_dogma_effects(type_id).into_iter().flatten() {
340        let Some(effect) = info.get_dogma_effect(type_effect.effect_id()) else {
341            continue;
342        };
343        match effect.name() {
344            "hiPower" => rack = Some(SlotKind::High),
345            "medPower" => rack = Some(SlotKind::Medium),
346            "loPower" => rack = Some(SlotKind::Low),
347            "rigSlot" => rack = Some(SlotKind::Rig),
348            "subSystem" => rack = Some(SlotKind::Subsystem),
349            "serviceSlot" => rack = Some(SlotKind::Service),
350            "turretFitted" => hardpoint = Some(SlotKind::Turret),
351            "launcherFitted" => hardpoint = Some(SlotKind::Launcher),
352            _ => {}
353        }
354    }
355
356    (rack, hardpoint)
357}
358
359impl Item<'_> {
360    /// Which rack the fit put it in.
361    fn rack(&self) -> Option<SlotKind> {
362        match self.fit.slot {
363            Slot::High(_) => Some(SlotKind::High),
364            Slot::Medium(_) => Some(SlotKind::Medium),
365            Slot::Low(_) => Some(SlotKind::Low),
366            Slot::Rig(_) => Some(SlotKind::Rig),
367            Slot::Subsystem(_) => Some(SlotKind::Subsystem),
368            Slot::Service(_) => Some(SlotKind::Service),
369            _ => None,
370        }
371    }
372
373    /// Whether it is fitted to the ship, rather than carried by it or by the
374    /// character.
375    fn is_fitted(&self) -> bool {
376        self.rack().is_some()
377    }
378
379    /// Whether the hull has to accept it: what is fitted to it, and the
380    /// fighters it launches.
381    fn is_on_hull(&self) -> bool {
382        self.is_fitted() || matches!(self.fit.slot, Slot::FighterTube(_) | Slot::FighterBay)
383    }
384
385    /// Whether the character has to be able to use it. Cargo is only hauled.
386    fn is_used(&self) -> bool {
387        self.fit.slot != Slot::Cargo
388    }
389
390    fn violation(&self, rule: Rule) -> Violation {
391        Violation {
392            target: Target::Item { index: self.index },
393            rule,
394        }
395    }
396}