1use crate::{
15 as_tbl, coalition::Side, controller::MissionPoint, country, is_hooks_env, net::SlotId,
16 string_enum, wrapped_prim, wrapped_table, Color, DcsTableExt, LuaEnv, LuaVec2, Path, Quad2,
17 Sequence, String,
18};
19use anyhow::{bail, Result};
20use fxhash::FxHashMap;
21use mlua::{prelude::*, Value};
22use serde_derive::{Deserialize, Serialize};
23use std::{cmp::max, collections::hash_map::Entry, ops::Deref};
24
25wrapped_table!(Weather, None);
26
27wrapped_prim!(UnitId, i64, Hash, Copy);
28
29impl Default for UnitId {
30 fn default() -> Self {
31 UnitId(0)
32 }
33}
34
35impl UnitId {
36 pub fn next(&mut self) {
37 self.0 += 1
38 }
39}
40
41wrapped_prim!(GroupId, i64, Hash, Copy);
42
43impl Default for GroupId {
44 fn default() -> Self {
45 GroupId(0)
46 }
47}
48
49impl GroupId {
50 pub fn next(&mut self) {
51 self.0 += 1
52 }
53}
54
55string_enum!(Skill, u8, [
56 Client => "Client",
57 Excellant => "Excellant",
58 Player => "Player",
59 Average => "Average",
60 Good => "Good",
61 High => "High"
62]);
63
64#[derive(Debug, Clone)]
65pub struct Property {
66 pub key: String,
67 pub value: String
68}
69
70impl<'lua> FromLua<'lua> for Property {
71 fn from_lua(value: Value<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
72 let tbl = LuaTable::from_lua(value, lua)?;
73 Ok(Self {
74 key: tbl.raw_get("key")?,
75 value: tbl.raw_get("value")?
76 })
77 }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
81pub enum TriggerZoneTyp {
82 Circle { radius: f64 },
83 Quad(Quad2),
84}
85
86wrapped_prim!(TriggerZoneId, i64, Copy, Hash);
87
88wrapped_table!(TriggerZone, None);
89
90impl<'lua> TriggerZone<'lua> {
91 pub fn name(&self) -> Result<String> {
92 Ok(self.raw_get("name")?)
93 }
94
95 pub fn pos(&self) -> Result<na::base::Vector2<f64>> {
96 Ok(na::base::Vector2::new(
97 self.raw_get("x")?,
98 self.raw_get("y")?,
99 ))
100 }
101
102 pub fn typ(&self) -> Result<TriggerZoneTyp> {
103 Ok(match self.raw_get("type")? {
104 0 => TriggerZoneTyp::Circle {
105 radius: self.raw_get("radius")?,
106 },
107 2 => TriggerZoneTyp::Quad(self.raw_get("verticies")?),
108 n => bail!("unknown trigger zone type {}", n),
109 })
110 }
111
112 pub fn color(&self) -> Result<Color> {
113 Ok(self.raw_get("color")?)
114 }
115
116 pub fn id(&self) -> Result<TriggerZoneId> {
117 Ok(self.raw_get("zoneId")?)
118 }
119
120 pub fn properties(&self) -> Result<Sequence<'lua, Property>> {
121 Ok(self.raw_get("properties")?)
122 }
123}
124
125wrapped_table!(NavPoint, None);
126
127wrapped_table!(Task, None);
128
129wrapped_table!(Route, None);
130
131impl<'lua> Route<'lua> {
132 pub fn points(&self) -> Result<Sequence<'lua, MissionPoint>> {
133 Ok(self.t.raw_get("points")?)
134 }
135
136 pub fn set_points(&self, points: Vec<MissionPoint>) -> Result<()> {
137 Ok(self.t.raw_set("points", points)?)
138 }
139}
140
141wrapped_table!(Unit, None);
142
143impl<'lua> Unit<'lua> {
144 pub fn name(&self) -> Result<String> {
145 Ok(self.raw_get("name")?)
146 }
147
148 pub fn id(&self) -> Result<UnitId> {
149 Ok(self.raw_get("unitId")?)
150 }
151
152 pub fn set_id(&self, id: UnitId) -> Result<()> {
153 Ok(self.raw_set("unitId", id)?)
154 }
155
156 pub fn slot(&self) -> Result<SlotId> {
157 Ok(SlotId::from(self.id()?))
158 }
159
160 pub fn set_name(&self, name: String) -> Result<()> {
161 Ok(self.raw_set("name", name)?)
162 }
163
164 pub fn pos(&self) -> Result<na::base::Vector2<f64>> {
165 Ok(na::base::Vector2::new(
166 self.raw_get("x")?,
167 self.raw_get("y")?,
168 ))
169 }
170
171 pub fn set_pos(&self, pos: na::base::Vector2<f64>) -> Result<()> {
172 self.raw_set("x", pos.x)?;
173 self.raw_set("y", pos.y)?;
174 Ok(())
175 }
176
177 pub fn heading(&self) -> Result<f64> {
178 Ok(self.raw_get("heading")?)
179 }
180
181 pub fn set_heading(&self, h: f64) -> Result<()> {
182 self.raw_set("psi", -h)?;
183 Ok(self.raw_set("heading", h)?)
184 }
185
186 pub fn alt(&self) -> Result<Option<f64>> {
187 Ok(self.raw_get("alt")?)
188 }
189
190 pub fn set_alt(&self, a: f64) -> Result<()> {
191 Ok(self.raw_set("alt", a)?)
192 }
193
194 pub fn typ(&self) -> Result<String> {
195 Ok(self.raw_get("type")?)
196 }
197
198 pub fn skill(&self) -> Result<Skill> {
199 Ok(self.raw_get("skill")?)
200 }
201}
202
203wrapped_table!(Group, None);
204
205impl<'lua> Group<'lua> {
206 pub fn name(&self) -> Result<String> {
207 Ok(self.raw_get("name")?)
208 }
209
210 pub fn set_name(&self, name: String) -> Result<()> {
211 Ok(self.raw_set("name", name)?)
212 }
213
214 pub fn pos(&self) -> Result<na::base::Vector2<f64>> {
215 Ok(na::base::Vector2::new(
216 self.t.raw_get("x")?,
217 self.t.raw_get("y")?,
218 ))
219 }
220
221 pub fn set_pos(&self, pos: na::base::Vector2<f64>) -> Result<()> {
222 self.t.raw_set("x", pos.x)?;
223 self.t.raw_set("y", pos.y)?;
224 Ok(())
225 }
226
227 pub fn frequency(&self) -> Result<f64> {
228 Ok(self.raw_get("frequency")?)
229 }
230
231 pub fn modulation(&self) -> Result<i64> {
232 Ok(self.raw_get("modulation")?)
233 }
234
235 pub fn late_activation(&self) -> bool {
236 self.raw_get("lateActivation").unwrap_or(false)
237 }
238
239 pub fn id(&self) -> Result<GroupId> {
240 Ok(self.raw_get("groupId")?)
241 }
242
243 pub fn set_id(&self, id: GroupId) -> Result<()> {
244 Ok(self.raw_set("groupId", id)?)
245 }
246
247 pub fn tasks(&self) -> Result<Sequence<'lua, Task>> {
248 Ok(self.raw_get("tasks")?)
249 }
250
251 pub fn route(&self) -> Result<Route> {
252 Ok(self.raw_get("route")?)
253 }
254
255 pub fn set_route(&self, r: Route) -> Result<()> {
256 Ok(self.raw_set("route", r)?)
257 }
258
259 pub fn hidden(&self) -> bool {
260 self.raw_get("hidden").unwrap_or(false)
261 }
262
263 pub fn units(&self) -> Result<Sequence<'lua, Unit<'lua>>> {
264 Ok(self.raw_get("units")?)
265 }
266
267 pub fn uncontrolled(&self) -> bool {
268 self.raw_get("uncontrolled").unwrap_or(true)
269 }
270}
271
272wrapped_table!(Country, None);
273
274impl<'lua> Country<'lua> {
275 pub fn id(&self) -> Result<country::Country> {
276 Ok(self.raw_get("id")?)
277 }
278
279 pub fn name(&self) -> Result<String> {
280 Ok(self.raw_get("name")?)
281 }
282
283 pub fn planes(&self) -> Result<Sequence<'lua, Group<'lua>>> {
284 let g: Option<mlua::Table> = self.raw_get("plane")?;
285 g.map(|g| Ok(g.raw_get("group")?))
286 .unwrap_or_else(|| Sequence::empty(self.lua))
287 }
288
289 pub fn helicopters(&self) -> Result<Sequence<'lua, Group<'lua>>> {
290 let g: Option<mlua::Table> = self.raw_get("helicopter")?;
291 g.map(|g| Ok(g.raw_get("group")?))
292 .unwrap_or_else(|| Sequence::empty(self.lua))
293 }
294
295 pub fn ships(&self) -> Result<Sequence<'lua, Group<'lua>>> {
296 let g: Option<mlua::Table> = self.raw_get("ship")?;
297 g.map(|g| Ok(g.raw_get("group")?))
298 .unwrap_or_else(|| Sequence::empty(self.lua))
299 }
300
301 pub fn vehicles(&self) -> Result<Sequence<'lua, Group<'lua>>> {
302 let g: Option<mlua::Table> = self.raw_get("vehicle")?;
303 g.map(|g| Ok(g.raw_get("group")?))
304 .unwrap_or_else(|| Sequence::empty(self.lua))
305 }
306
307 pub fn statics(&self) -> Result<Sequence<'lua, Group<'lua>>> {
308 let g: Option<mlua::Table> = self.raw_get("static")?;
309 g.map(|g| Ok(g.raw_get("group")?))
310 .unwrap_or_else(|| Sequence::empty(self.lua))
311 }
312}
313
314wrapped_table!(Coalition, None);
315
316impl<'lua> Coalition<'lua> {
317 pub fn bullseye(&self) -> Result<LuaVec2> {
318 Ok(self.t.raw_get("bullseye")?)
319 }
320
321 pub fn nav_points(&self) -> Result<Sequence<'lua, NavPoint<'lua>>> {
322 Ok(self.t.raw_get("nav_points")?)
323 }
324
325 pub fn name(&self) -> Result<String> {
326 Ok(self.t.raw_get("name")?)
327 }
328
329 pub fn countries(&self) -> Result<Sequence<'lua, Country<'lua>>> {
330 Ok(self.t.raw_get("country")?)
331 }
332
333 pub fn country(&self, country: country::Country) -> Result<Option<Country<'lua>>> {
334 for c in self.countries()? {
335 let c = c?;
336 if c.id()? == country {
337 return Ok(Some(c));
338 }
339 }
340 return Ok(None);
341 }
342
343 fn index(&self, side: Side, base: Path) -> Result<CoalitionIndex> {
344 let base = base.append(["country"]);
345 let mut idx = CoalitionIndex::default();
346 for (i, country) in self.countries()?.into_iter().enumerate() {
347 let country = country?;
348 let cid = country.id()?;
349 let base = base.append([i + 1]);
350 macro_rules! index_group {
351 ($name:literal, $cat:expr, $tbl:ident) => {
352 for (i, group) in country.$tbl()?.into_iter().enumerate() {
353 let group = group?;
354 let name = group.name()?;
355 let gid = group.id()?;
356 idx.max_gid = GroupId(max(idx.max_gid.0, gid.0));
357 let base = base.append([$name, "group"]).append([i + 1]);
358 match idx.groups.entry(gid) {
359 Entry::Occupied(_) => bail!("duplicate group id {:?}", gid),
360 Entry::Vacant(e) => {
361 e.insert(IndexedGroup {
362 side,
363 country: cid,
364 category: $cat,
365 path: base.clone(),
366 });
367 }
368 }
369 match idx.groups_by_name.entry(name.clone()) {
370 Entry::Occupied(_) => bail!("duplicate group name {name}"),
371 Entry::Vacant(e) => e.insert(gid),
372 };
373 match idx.$tbl.entry(name.clone()) {
374 Entry::Occupied(_) => bail!("duplicate group name {name}"),
375 Entry::Vacant(e) => e.insert(gid),
376 };
377 for (i, unit) in group.units()?.into_iter().enumerate() {
378 let unit = unit?;
379 let base = base.append(["units"]).append([i + 1]);
380 let name = unit.name()?;
381 let uid = unit.id()?;
382 idx.max_uid = UnitId(max(idx.max_uid.0, uid.0));
383 match idx.units.entry(uid) {
384 Entry::Occupied(_) => bail!("duplicate unit id {:?}", uid),
385 Entry::Vacant(e) => e.insert(IndexedUnit {
386 side,
387 country: cid,
388 path: base.clone(),
389 }),
390 };
391 match idx.units_by_name.entry(name.clone()) {
392 Entry::Occupied(_) => bail!("duplicate unit name {name}"),
393 Entry::Vacant(e) => e.insert(uid),
394 };
395 match idx.groups_by_unit.entry(uid) {
396 Entry::Occupied(_) => bail!("guplicate unit id {:?}", uid),
397 Entry::Vacant(e) => e.insert(gid),
398 };
399 }
400 }
401 };
402 }
403 index_group!("plane", GroupKind::Plane, planes);
404 index_group!("helicopter", GroupKind::Helicopter, helicopters);
405 index_group!("ship", GroupKind::Ship, ships);
406 index_group!("vehicle", GroupKind::Vehicle, vehicles);
407 index_group!("static", GroupKind::Static, statics);
408 }
409 Ok(idx)
410 }
411}
412
413#[derive(Debug, Clone, Copy)]
414pub struct Role {
415 pub neutrals: u8,
416 pub red: u8,
417 pub blue: u8,
418}
419
420impl<'lua> FromLua<'lua> for Role {
421 fn from_lua(value: Value<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
422 let tbl: LuaTable = FromLua::from_lua(value, lua)?;
423 Ok(Self {
424 neutrals: tbl.raw_get("neutrals")?,
425 red: tbl.raw_get("red")?,
426 blue: tbl.raw_get("blue")?,
427 })
428 }
429}
430
431wrapped_table!(Roles, None);
432
433impl<'lua> Roles<'lua> {
434 pub fn artillery_commander(&self) -> Result<Role> {
435 Ok(self.raw_get("artillery_commander")?)
436 }
437
438 pub fn instructor(&self) -> Result<Role> {
439 Ok(self.raw_get("instructor")?)
440 }
441
442 pub fn observer(&self) -> Result<Role> {
443 Ok(self.raw_get("observer")?)
444 }
445
446 pub fn forward_observer(&self) -> Result<Role> {
447 Ok(self.raw_get("forward_observer")?)
448 }
449}
450
451wrapped_table!(GroundControl, None);
452
453impl<'lua> GroundControl<'lua> {
454 pub fn roles(&self) -> Result<Roles<'lua>> {
455 Ok(self.raw_get("roles")?)
456 }
457}
458
459#[derive(Debug, Clone, Serialize)]
460struct IndexedGroup {
461 side: Side,
462 country: country::Country,
463 category: GroupKind,
464 path: Path,
465}
466
467#[derive(Debug, Clone, Serialize)]
468struct IndexedUnit {
469 side: Side,
470 country: country::Country,
471 path: Path,
472}
473
474#[derive(Debug, Clone, Serialize, Default)]
475pub struct CoalitionIndex {
476 max_uid: UnitId,
477 max_gid: GroupId,
478 units: FxHashMap<UnitId, IndexedUnit>,
479 units_by_name: FxHashMap<String, UnitId>,
480 groups: FxHashMap<GroupId, IndexedGroup>,
481 groups_by_name: FxHashMap<String, GroupId>,
482 groups_by_unit: FxHashMap<UnitId, GroupId>,
483 planes: FxHashMap<String, GroupId>,
484 helicopters: FxHashMap<String, GroupId>,
485 ships: FxHashMap<String, GroupId>,
486 vehicles: FxHashMap<String, GroupId>,
487 statics: FxHashMap<String, GroupId>,
488}
489
490#[derive(Debug, Clone, Serialize, Default)]
491pub struct MizIndex {
492 by_side: FxHashMap<Side, CoalitionIndex>,
493 triggers: FxHashMap<String, Path>,
494}
495
496impl MizIndex {
497 pub fn max_uid(&self) -> UnitId {
498 self.by_side
499 .iter()
500 .fold(UnitId::default(), |muid, (_, cidx)| {
501 UnitId(max(muid.0, cidx.max_uid.0))
502 })
503 }
504
505 pub fn max_gid(&self) -> GroupId {
506 self.by_side
507 .iter()
508 .fold(GroupId::default(), |mgid, (_, cidx)| {
509 GroupId(max(mgid.0, cidx.max_gid.0))
510 })
511 }
512}
513
514#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
515pub enum GroupKind {
516 Any,
517 Plane,
518 Helicopter,
519 Ship,
520 Vehicle,
521 Static,
522}
523
524#[derive(Debug, Clone, Serialize)]
525pub struct GroupInfo<'lua> {
526 pub side: Side,
527 pub country: country::Country,
528 pub category: GroupKind,
529 pub group: Group<'lua>,
530}
531
532#[derive(Debug, Clone, Serialize)]
533pub struct UnitInfo<'lua> {
534 pub side: Side,
535 pub country: country::Country,
536 pub unit: Unit<'lua>,
537}
538
539wrapped_table!(Miz, None);
540
541impl<'lua> Miz<'lua> {
542 pub fn singleton<L: LuaEnv<'lua> + Copy>(lua: L) -> Result<Self> {
543 if is_hooks_env(lua.inner()) {
544 let current: mlua::Table = lua.inner().globals().get("_current_mission")?;
545 Ok(current.get("mission")?)
546 } else {
547 let env: mlua::Table = lua.inner().globals().get("env")?;
548 Ok(env.get("mission")?)
549 }
550 }
551
552 pub fn ground_control(&self) -> Result<GroundControl> {
553 Ok(self.raw_get("groundControl")?)
554 }
555
556 pub fn coalition(&self, side: Side) -> Result<Coalition<'lua>> {
557 let coa: mlua::Table = self.raw_get("coalition")?;
558 Ok(coa.raw_get(side.to_str())?)
559 }
560
561 pub fn triggers(&self) -> Result<Sequence<'lua, TriggerZone<'lua>>> {
562 let triggers: mlua::Table = self.t.raw_get("triggers")?;
563 Ok(triggers.raw_get("zones")?)
564 }
565
566 pub fn weather(&self) -> Result<Weather<'lua>> {
567 Ok(self.t.raw_get("weather")?)
568 }
569
570 pub fn get_group(&self, idx: &MizIndex, id: &GroupId) -> Result<Option<GroupInfo<'lua>>> {
571 idx.by_side
572 .iter()
573 .find_map(|(_, idx)| idx.groups.get(id))
574 .map(|ifo| {
575 self.raw_get_path(&ifo.path).map(|group| GroupInfo {
576 side: ifo.side,
577 country: ifo.country,
578 category: ifo.category,
579 group,
580 })
581 })
582 .transpose()
583 }
584
585 pub fn get_group_by_name(
586 &self,
587 idx: &MizIndex,
588 kind: GroupKind,
589 side: Side,
590 name: &str,
591 ) -> Result<Option<GroupInfo<'lua>>> {
592 idx.by_side
593 .get(&side)
594 .and_then(|cidx| match kind {
595 GroupKind::Any => cidx.groups_by_name.get(name),
596 GroupKind::Plane => cidx.planes.get(name),
597 GroupKind::Helicopter => cidx.helicopters.get(name),
598 GroupKind::Vehicle => cidx.vehicles.get(name),
599 GroupKind::Ship => cidx.ships.get(name),
600 GroupKind::Static => cidx.statics.get(name),
601 })
602 .and_then(|gid| self.get_group(idx, gid).transpose())
603 .transpose()
604 }
605
606 pub fn get_unit(&self, idx: &MizIndex, id: &UnitId) -> Result<Option<UnitInfo<'lua>>> {
607 idx.by_side
608 .iter()
609 .find_map(|(_, idx)| idx.units.get(id))
610 .map(|ifo| {
611 self.raw_get_path(&ifo.path).map(|unit| UnitInfo {
612 side: ifo.side,
613 country: ifo.country,
614 unit,
615 })
616 })
617 .transpose()
618 }
619
620 pub fn get_unit_by_name(&self, idx: &MizIndex, name: &str) -> Result<Option<UnitInfo<'lua>>> {
621 idx.by_side
622 .iter()
623 .find_map(|(_, idx)| idx.units_by_name.get(name).and_then(|id| idx.units.get(id)))
624 .map(|ifo| {
625 self.raw_get_path(&ifo.path).map(|unit| UnitInfo {
626 side: ifo.side,
627 country: ifo.country,
628 unit,
629 })
630 })
631 .transpose()
632 }
633
634 pub fn get_group_by_unit(
635 &self,
636 idx: &MizIndex,
637 id: &UnitId,
638 ) -> Result<Option<GroupInfo<'lua>>> {
639 idx.by_side
640 .iter()
641 .find_map(|(_, idx)| idx.groups_by_unit.get(id))
642 .and_then(|gid| self.get_group(idx, gid).transpose())
643 .transpose()
644 }
645
646 pub fn get_group_by_unit_name(
647 &self,
648 idx: &MizIndex,
649 name: &str,
650 ) -> Result<Option<GroupInfo<'lua>>> {
651 idx.by_side
652 .iter()
653 .find_map(|(_, idx)| {
654 idx.units_by_name
655 .get(name)
656 .and_then(|uid| idx.groups_by_unit.get(uid))
657 })
658 .and_then(|gid| self.get_group(idx, gid).transpose())
659 .transpose()
660 }
661
662 pub fn get_trigger_zone(
663 &self,
664 idx: &MizIndex,
665 name: &str,
666 ) -> Result<Option<TriggerZone<'lua>>> {
667 idx.triggers
668 .get(name)
669 .map(|path| self.raw_get_path(path))
670 .transpose()
671 }
672
673 pub fn sortie(&self) -> Result<String> {
674 Ok(self.raw_get("sortie")?)
675 }
676
677 pub fn index(&self) -> Result<MizIndex> {
678 let base = Path::default();
679 let mut idx = MizIndex::default();
680 {
681 let base = base.append(["triggers", "zones"]);
682 for (i, tz) in self.triggers()?.into_iter().enumerate() {
683 let tz = tz?;
684 let base = base.append([i + 1]);
685 let name = tz.name()?;
686 match idx.triggers.entry(name.clone()) {
687 Entry::Vacant(e) => e.insert(base),
688 Entry::Occupied(_) => bail!("duplicate trigger zone {name}"),
689 };
690 }
691 }
692 for side in Side::ALL {
693 let base = base.append(["coalition", side.to_str()]);
694 idx.by_side
695 .entry(side)
696 .or_insert(self.coalition(side)?.index(side, base)?);
697 }
698 Ok(idx)
699 }
700}