1use 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#[cfg_attr(feature = "typescript", derive(Tsify))]
21#[derive(Serialize, Debug, Clone, PartialEq)]
22pub struct Violation {
23 pub target: Target,
25 pub rule: Rule,
27}
28
29#[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 Ship,
36 Item {
38 index: usize,
40 },
41 Charge {
43 index: usize,
45 },
46}
47
48#[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 Resource {
59 resource: Resource,
61 used: f64,
63 available: f64,
65 },
66 Slots {
68 slot: SlotKind,
70 used: u32,
72 available: u32,
74 },
75 WrongSlot {
77 expected: SlotKind,
79 },
80 SlotTaken,
82 WrongSlotIndex {
85 expected: u16,
87 },
88 SubsystemTaken,
90 Skill {
92 type_id: i32,
94 required: u8,
96 level: u8,
98 },
99 RigSize {
101 ship: u8,
103 item: u8,
105 },
106 ShipRestricted,
108 CapitalItem,
110 StructureItem,
112 ShipItem,
114 MaxGroup {
116 group_id: i32,
118 limit: GroupLimit,
120 used: u32,
122 allowed: u32,
124 },
125 MaxType {
127 type_id: i32,
129 used: u32,
131 allowed: u32,
133 },
134 ChargeGroup,
136 ChargeSize {
138 module: u8,
140 charge: u8,
142 },
143}
144
145#[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,
152 Powergrid,
154 Calibration,
156 DroneBay,
158 DroneBandwidth,
160 LaunchedDrones,
162 FighterBay,
164 FighterTubes,
166 LightFighterTubes,
168 SupportFighterTubes,
170 HeavyFighterTubes,
172 CargoBay,
174 ChargeCapacity,
176}
177
178#[cfg_attr(feature = "typescript", derive(Tsify))]
180#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
181#[serde(rename_all = "snake_case")]
182pub enum SlotKind {
183 High,
185 Medium,
187 Low,
189 Rig,
191 Subsystem,
193 Service,
195 Turret,
197 Launcher,
199}
200
201#[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,
208 Online,
210 Active,
212}
213
214pub(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
234struct Context<'a, I> {
237 info: &'a I,
238 fit: &'a Fit,
239 calculation: &'a Calculation,
240 items: Vec<Item<'a>>,
241}
242
243struct 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 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 fn amount(&self, result: &ItemResult, attribute_id: i32) -> f64 {
303 self.value(result, attribute_id).unwrap_or(0.0)
304 }
305
306 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
333fn 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 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 fn is_fitted(&self) -> bool {
376 self.rack().is_some()
377 }
378
379 fn is_on_hull(&self) -> bool {
382 self.is_fitted() || matches!(self.fit.slot, Slot::FighterTube(_) | Slot::FighterBay)
383 }
384
385 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}