1use std::collections::{BTreeMap, BTreeSet};
35
36use hpr_design::Rocket;
37use serde::{Deserialize, Serialize};
38
39use super::component::subcomponents;
40use super::document::Element;
41use super::motors::stage_of;
42use super::value::{Dimension, Values};
43use super::warning::{Warning, WarningKind};
44
45pub(super) const DEPLOYMENT: &str = "/deployment";
49pub(super) const DRAG: &str = "/drag";
51pub(super) const SEPARATION: &str = "/separation";
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
56#[serde(rename_all = "snake_case")]
57#[non_exhaustive]
58pub enum DeployEvent {
59 Launch,
61 Ejection,
63 Apogee,
65 Altitude,
67 LowerStageSeparation,
69 Never,
71 Other(String),
73}
74
75impl DeployEvent {
76 pub fn parse(text: &str) -> Self {
78 match text.trim() {
79 "launch" => Self::Launch,
80 "ejection" => Self::Ejection,
81 "apogee" => Self::Apogee,
82 "altitude" => Self::Altitude,
83 "lowerstageseparation" => Self::LowerStageSeparation,
84 "never" => Self::Never,
85 other => Self::Other(other.to_owned()),
86 }
87 }
88
89 pub fn as_str(&self) -> &str {
91 match self {
92 Self::Launch => "launch",
93 Self::Ejection => "ejection",
94 Self::Apogee => "apogee",
95 Self::Altitude => "altitude",
96 Self::LowerStageSeparation => "lowerstageseparation",
97 Self::Never => "never",
98 Self::Other(text) => text,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
106#[serde(rename_all = "snake_case")]
107#[non_exhaustive]
108pub enum SeparationEvent {
109 Launch,
111 Ignition,
113 Burnout,
115 Ejection,
117 UpperIgnition,
119 AltitudeAscending,
121 Apogee,
123 AltitudeDescending,
125 Never,
127 Other(String),
129}
130
131impl SeparationEvent {
132 pub fn parse(text: &str) -> Self {
134 match text.trim() {
135 "launch" => Self::Launch,
136 "ignition" => Self::Ignition,
137 "burnout" => Self::Burnout,
138 "ejection" => Self::Ejection,
139 "upperignition" => Self::UpperIgnition,
140 "altitudeascending" => Self::AltitudeAscending,
141 "apogee" => Self::Apogee,
142 "altitudedescending" => Self::AltitudeDescending,
143 "never" => Self::Never,
144 other => Self::Other(other.to_owned()),
145 }
146 }
147
148 pub fn as_str(&self) -> &str {
150 match self {
151 Self::Launch => "launch",
152 Self::Ignition => "ignition",
153 Self::Burnout => "burnout",
154 Self::Ejection => "ejection",
155 Self::UpperIgnition => "upperignition",
156 Self::AltitudeAscending => "altitudeascending",
157 Self::Apogee => "apogee",
158 Self::AltitudeDescending => "altitudedescending",
159 Self::Never => "never",
160 Self::Other(text) => text,
161 }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
168#[non_exhaustive]
169#[serde(deny_unknown_fields)]
170#[schemars(rename = "EventSetting_for_{E}")]
172pub struct EventSetting<E> {
173 pub event: Option<E>,
175 pub altitude_m: Option<f64>,
177 pub delay_s: Option<f64>,
179}
180
181impl<E: Clone> EventSetting<E> {
182 fn overridden_by(&self, over: &Self) -> Self {
184 Self {
185 event: over.event.clone().or_else(|| self.event.clone()),
186 altitude_m: over.altitude_m.or(self.altitude_m),
187 delay_s: over.delay_s.or(self.delay_s),
188 }
189 }
190}
191
192pub type Deployment = EventSetting<DeployEvent>;
194
195pub type Separation = EventSetting<SeparationEvent>;
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
200#[serde(rename_all = "snake_case")]
201#[non_exhaustive]
202pub enum DeviceKind {
203 Parachute,
205 Streamer,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
211#[non_exhaustive]
212#[serde(deny_unknown_fields)]
213pub struct RecoveryDevice {
214 pub id: String,
216 pub stage: usize,
218 pub kind: DeviceKind,
220 pub cd: Option<Dimension>,
223 pub deployment: Deployment,
225 pub configurations: BTreeMap<String, Deployment>,
228}
229
230impl RecoveryDevice {
231 pub fn deployment_in(&self, id: &str) -> &Deployment {
233 self.configurations.get(id).unwrap_or(&self.deployment)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
239#[non_exhaustive]
240#[serde(deny_unknown_fields)]
241pub struct StageSeparation {
242 pub id: String,
244 pub stage: usize,
246 pub separation: Separation,
248 pub configurations: BTreeMap<String, Separation>,
251}
252
253impl StageSeparation {
254 pub fn separation_in(&self, id: &str) -> &Separation {
256 self.configurations.get(id).unwrap_or(&self.separation)
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
262#[non_exhaustive]
263#[serde(deny_unknown_fields)]
264pub struct UnreadDevice {
265 pub at: String,
267 pub tag: String,
269 pub inside: String,
272}
273
274#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
276#[non_exhaustive]
277#[serde(deny_unknown_fields)]
278pub struct Recovery {
279 pub devices: Vec<RecoveryDevice>,
281 pub separations: Vec<StageSeparation>,
283 pub unread: Vec<UnreadDevice>,
285 pub unread_separations: Vec<UnreadDevice>,
287}
288
289#[derive(Debug, Clone, PartialEq)]
291pub(super) struct DeviceRead {
292 pub at: String,
294 kind: DeviceKind,
295 cd: Option<Dimension>,
296 deployment: Deployment,
297 configurations: BTreeMap<String, Deployment>,
298}
299
300#[derive(Debug, Clone, PartialEq)]
302pub(super) struct SeparationRead {
303 at: String,
305 separation: Separation,
306 configurations: BTreeMap<String, Separation>,
307}
308
309pub(super) fn device(element: &Element, at: &str, warnings: &mut Vec<Warning>) -> DeviceRead {
311 let kind = if element.name == "streamer" {
312 DeviceKind::Streamer
313 } else {
314 DeviceKind::Parachute
315 };
316 let cd = Values::new(element, &format!("{at}{DRAG}"), warnings).dimension(&["cd"]);
319 let (deployment, configurations) = trigger(
320 element,
321 &format!("{at}{DEPLOYMENT}"),
322 "deploy",
323 "deploymentconfiguration",
324 DeployEvent::parse,
325 |event| !matches!(event, DeployEvent::Other(_)),
326 warnings,
327 );
328 DeviceRead {
329 at: at.to_owned(),
330 kind,
331 cd,
332 deployment,
333 configurations,
334 }
335}
336
337pub(super) fn separation(
339 element: &Element,
340 at: &str,
341 warnings: &mut Vec<Warning>,
342) -> Option<SeparationRead> {
343 let (separation, configurations) = trigger(
344 element,
345 &format!("{at}{SEPARATION}"),
346 "separation",
347 "separationconfiguration",
348 SeparationEvent::parse,
349 |event| !matches!(event, SeparationEvent::Other(_)),
350 warnings,
351 );
352 let empty = separation.event.is_none()
353 && separation.altitude_m.is_none()
354 && separation.delay_s.is_none()
355 && configurations.is_empty();
356 (!empty).then_some(SeparationRead {
357 at: at.to_owned(),
358 separation,
359 configurations,
360 })
361}
362
363fn trigger<E: Clone>(
366 element: &Element,
367 at: &str,
368 prefix: &str,
369 per: &str,
370 parse: fn(&str) -> E,
371 known: fn(&E) -> bool,
372 warnings: &mut Vec<Warning>,
373) -> (EventSetting<E>, BTreeMap<String, EventSetting<E>>) {
374 let tag = format!("{prefix}event");
375 let own = |element: &Element, at: &str, warnings: &mut Vec<Warning>| {
376 let mut values = Values::new(element, at, warnings);
377 let event = match values.word(&[&tag]) {
378 None => None,
379 Some(text) if text.is_empty() => {
380 values.warn_at(
381 WarningKind::Dropped,
382 format!("`{tag}` is empty; it was read as not stated"),
383 );
384 values.forget(&[&tag]);
385 None
386 }
387 Some(text) => {
388 let event = parse(&text);
389 if !known(&event) {
390 values.warn_at(
391 WarningKind::Unusual,
392 format!(
393 "`{tag}` says `{text}`, a word OpenRocket 24.12 does not write; it \
394 was kept as written"
395 ),
396 );
397 }
398 Some(event)
399 }
400 };
401 EventSetting {
402 event,
403 altitude_m: values.number(&[&format!("{prefix}altitude")]),
404 delay_s: values.number(&[&format!("{prefix}delay")]),
405 }
406 };
407 let default = own(element, at, warnings);
408 let mut configurations = BTreeMap::new();
409 super::reads::note(element, per);
410 for child in element.children_named(per) {
411 let here = format!("{at}/{per}");
412 let Some(id) = child
413 .attribute("configid")
414 .map(str::trim)
415 .filter(|id| !id.is_empty())
416 else {
417 warnings.push(Warning::new(
418 here,
419 WarningKind::Dropped,
420 format!("a `{per}` with no `configid` belongs to no configuration; it was ignored"),
421 ));
422 continue;
423 };
424 let over = own(child, &here, warnings);
425 if configurations
426 .insert(id.to_owned(), default.overridden_by(&over))
427 .is_some()
428 {
429 warnings.push(Warning::new(
430 here,
431 WarningKind::Dropped,
432 format!("configuration `{id}` is set twice here; the last was kept"),
433 ));
434 }
435 }
436 (default, configurations)
437}
438
439pub(super) fn read(
442 rocket_element: &Element,
443 rocket: &Rocket,
444 devices: Vec<(String, DeviceRead)>,
445 separations: Vec<(String, SeparationRead)>,
446) -> Recovery {
447 let mut placed = BTreeSet::new();
448 let mut read_devices = Vec::new();
449 for (id, device) in devices {
450 let Some(stage) = stage_of(rocket, &id) else {
451 continue;
452 };
453 placed.insert(device.at.clone());
454 read_devices.push(RecoveryDevice {
455 id,
456 stage,
457 kind: device.kind,
458 cd: device.cd,
459 deployment: device.deployment,
460 configurations: device.configurations,
461 });
462 }
463 placed.extend(separations.iter().map(|(_, read)| read.at.clone()));
465 let read_separations = separations
466 .into_iter()
467 .filter_map(|(id, read)| {
468 Some(StageSeparation {
469 stage: rocket.stages.iter().position(|stage| stage.id == id)?,
470 id,
471 separation: read.separation,
472 configurations: read.configurations,
473 })
474 })
475 .collect();
476 let mut unread = Vec::new();
477 let mut unread_separations = Vec::new();
478 for (index, child) in subcomponents(rocket_element).enumerate() {
479 let path = format!("openrocket/rocket/{}[{index}]", child.name);
480 unread_devices(
481 child,
482 &path,
483 None,
484 &placed,
485 &mut unread,
486 &mut unread_separations,
487 );
488 }
489 Recovery {
490 devices: read_devices,
491 separations: read_separations,
492 unread,
493 unread_separations,
494 }
495}
496
497fn unread_devices(
502 element: &Element,
503 at: &str,
504 inside: Option<&str>,
505 read: &BTreeSet<String>,
506 found: &mut Vec<UnreadDevice>,
507 separations: &mut Vec<UnreadDevice>,
508) {
509 let inside = inside.or(match element.name.as_str() {
510 tag @ ("podset" | "parallelstage") if !read.contains(at) => Some(tag),
511 _ => None,
512 });
513 let states_a_separation = ["separationevent", "separationconfiguration"]
514 .iter()
515 .any(|tag| element.child(tag).is_some());
516 if element.name == "parallelstage" && states_a_separation && !read.contains(at) {
517 separations.push(UnreadDevice {
518 at: at.to_owned(),
519 tag: element.name.clone(),
520 inside: inside.unwrap_or(element.name.as_str()).to_owned(),
521 });
522 }
523 if matches!(element.name.as_str(), "parachute" | "streamer") && !read.contains(at) {
524 found.push(UnreadDevice {
525 at: at.to_owned(),
526 tag: element.name.clone(),
527 inside: inside.unwrap_or(element.name.as_str()).to_owned(),
528 });
529 }
530 for (index, child) in subcomponents(element).enumerate() {
531 let path = format!("{at}/{}[{index}]", child.name);
532 unread_devices(child, &path, inside, read, found, separations);
533 }
534}