arma_rs/value/loadout/
mod.rs

1//! For working with Arma's unit loadout array
2
3use crate::{FromArma, FromArmaError, IntoArma, Value};
4
5mod assigned;
6mod container;
7mod extended;
8mod inventory_item;
9mod magazine;
10mod weapon;
11
12pub use assigned::AssignedItems;
13pub use container::Container;
14pub use extended::CBAExtended;
15pub use inventory_item::InventoryItem;
16pub use magazine::Magazine;
17pub use weapon::Weapon;
18
19#[derive(Debug, Default, Clone, PartialEq)]
20/// Arma Unit Loadout Array
21pub struct Loadout(
22    Weapon,
23    Weapon,
24    Weapon,
25    Container,
26    Container,
27    Container,
28    String,
29    String,
30    Weapon,
31    AssignedItems,
32    CBAExtended,
33);
34
35impl Loadout {
36    /// Get the primary weapon
37    #[must_use]
38    pub const fn primary(&self) -> &Weapon {
39        &self.0
40    }
41
42    /// Get the primary weapon mutably
43    pub fn primary_mut(&mut self) -> &mut Weapon {
44        &mut self.0
45    }
46
47    /// Set the primary weapon
48    pub fn set_primary(&mut self, primary: Weapon) {
49        self.0 = primary;
50    }
51
52    /// Get the secondary weapon (launcher)
53    #[must_use]
54    pub const fn secondary(&self) -> &Weapon {
55        &self.1
56    }
57
58    /// Get the secondary weapon (launcher) mutably
59    pub fn secondary_mut(&mut self) -> &mut Weapon {
60        &mut self.1
61    }
62
63    /// Set the secondary weapon (launcher)
64    pub fn set_secondary(&mut self, secondary: Weapon) {
65        self.1 = secondary;
66    }
67
68    /// Get the handgun weapon
69    #[must_use]
70    pub const fn handgun(&self) -> &Weapon {
71        &self.2
72    }
73
74    /// Get the handgun weapon mutably
75    pub fn handgun_mut(&mut self) -> &mut Weapon {
76        &mut self.2
77    }
78
79    /// Set the handgun weapon
80    pub fn set_handgun(&mut self, handgun: Weapon) {
81        self.2 = handgun;
82    }
83
84    /// Get the uniform
85    #[must_use]
86    pub const fn uniform(&self) -> &Container {
87        &self.3
88    }
89
90    /// Get the uniform mutably
91    pub fn uniform_mut(&mut self) -> &mut Container {
92        &mut self.3
93    }
94
95    /// Set the uniform
96    pub fn set_uniform(&mut self, uniform: Container) {
97        self.3 = uniform;
98    }
99
100    /// Get the vest
101    #[must_use]
102    pub const fn vest(&self) -> &Container {
103        &self.4
104    }
105
106    /// Get the vest mutably
107    pub fn vest_mut(&mut self) -> &mut Container {
108        &mut self.4
109    }
110
111    /// Set the vest
112    pub fn set_vest(&mut self, vest: Container) {
113        self.4 = vest;
114    }
115
116    /// Get the backpack
117    #[must_use]
118    pub const fn backpack(&self) -> &Container {
119        &self.5
120    }
121
122    /// Get the backpack mutably
123    pub fn backpack_mut(&mut self) -> &mut Container {
124        &mut self.5
125    }
126
127    /// Set the backpack
128    pub fn set_backpack(&mut self, backpack: Container) {
129        self.5 = backpack;
130    }
131
132    /// The class name of the current headgear
133    #[must_use]
134    pub fn headgear(&self) -> &str {
135        &self.6
136    }
137
138    /// Set the class name of the current headgear
139    pub fn set_headgear(&mut self, headgear: String) {
140        self.6 = headgear;
141    }
142
143    /// The class name of the current goggles / facewear
144    #[must_use]
145    pub fn goggles(&self) -> &str {
146        &self.7
147    }
148
149    /// Set the class name of the current goggles / facewear
150    pub fn set_goggles(&mut self, goggles: String) {
151        self.7 = goggles;
152    }
153
154    /// Get the binocular
155    #[must_use]
156    pub const fn binoculars(&self) -> &Weapon {
157        &self.8
158    }
159
160    /// Get the binocular mutably
161    pub fn binoculars_mut(&mut self) -> &mut Weapon {
162        &mut self.8
163    }
164
165    /// Set the binocular
166    pub fn set_binoculars(&mut self, binoculars: Weapon) {
167        self.8 = binoculars;
168    }
169
170    /// Get the assigned items
171    #[must_use]
172    pub const fn assigned_items(&self) -> &AssignedItems {
173        &self.9
174    }
175
176    /// Get the assigned items mutably
177    pub fn assigned_items_mut(&mut self) -> &mut AssignedItems {
178        &mut self.9
179    }
180
181    /// Set the assigned items
182    pub fn set_assigned_items(&mut self, assigned_items: AssignedItems) {
183        self.9 = assigned_items;
184    }
185
186    /// Get the CBA Extended Loadout Array
187    pub fn cba_extended(&self) -> &CBAExtended {
188        &self.10
189    }
190
191    /// Get a map of all items in the loadout and their quantities
192    pub fn classes(&self) -> std::collections::HashMap<String, u32> {
193        let mut items = std::collections::HashMap::new();
194        self.0.classes().iter().for_each(|c| {
195            *items.entry(c.to_string()).or_insert(0) += 1;
196        });
197        self.1.classes().iter().for_each(|c| {
198            *items.entry(c.to_string()).or_insert(0) += 1;
199        });
200        self.2.classes().iter().for_each(|c| {
201            *items.entry(c.to_string()).or_insert(0) += 1;
202        });
203        self.3.classes().iter().for_each(|(c, q)| {
204            *items.entry(c.clone()).or_insert(0) += q;
205        });
206        self.4.classes().iter().for_each(|(c, q)| {
207            *items.entry(c.clone()).or_insert(0) += q;
208        });
209        self.5.classes().iter().for_each(|(c, q)| {
210            *items.entry(c.clone()).or_insert(0) += q;
211        });
212        *items.entry(self.6.clone()).or_insert(0) += 1;
213        *items.entry(self.7.clone()).or_insert(0) += 1;
214        self.8.classes().iter().for_each(|c| {
215            *items.entry(c.to_string()).or_insert(0) += 1;
216        });
217        self.9.classes().iter().for_each(|c| {
218            *items.entry(c.to_string()).or_insert(0) += 1;
219        });
220        items.remove("");
221        items
222    }
223}
224
225impl FromArma for Loadout {
226    fn from_arma(s: String) -> Result<Self, FromArmaError> {
227        let vanilla = <(
228            Weapon,
229            Weapon,
230            Weapon,
231            Container,
232            Container,
233            Container,
234            String,
235            String,
236            Weapon,
237            AssignedItems,
238        )>::from_arma(s.clone())
239        .map(
240            |(
241                primary,
242                secondary,
243                handgun,
244                uniform,
245                vest,
246                backpack,
247                headgear,
248                goggles,
249                binoculars,
250                linked_items,
251            )| {
252                Self(
253                    primary,
254                    secondary,
255                    handgun,
256                    uniform,
257                    vest,
258                    backpack,
259                    headgear,
260                    goggles,
261                    binoculars,
262                    linked_items,
263                    CBAExtended::default(),
264                )
265            },
266        );
267        if vanilla.is_err() {
268            return <(
269                (
270                    Weapon,
271                    Weapon,
272                    Weapon,
273                    Container,
274                    Container,
275                    Container,
276                    String,
277                    String,
278                    Weapon,
279                    AssignedItems,
280                ),
281                CBAExtended,
282            )>::from_arma(s)
283            .map(
284                |(
285                    (
286                        primary,
287                        secondary,
288                        handgun,
289                        uniform,
290                        vest,
291                        backpack,
292                        headgear,
293                        goggles,
294                        binoculars,
295                        linked_items,
296                    ),
297                    extended,
298                )| {
299                    Self(
300                        primary,
301                        secondary,
302                        handgun,
303                        uniform,
304                        vest,
305                        backpack,
306                        headgear,
307                        goggles,
308                        binoculars,
309                        linked_items,
310                        extended,
311                    )
312                },
313            );
314        }
315        vanilla
316    }
317}
318impl IntoArma for Loadout {
319    fn to_arma(&self) -> Value {
320        Value::Array(vec![
321            self.primary().to_arma(),
322            self.secondary().to_arma(),
323            self.handgun().to_arma(),
324            self.uniform().to_arma(),
325            self.vest().to_arma(),
326            self.backpack().to_arma(),
327            Value::String(self.headgear().to_owned()),
328            Value::String(self.goggles().to_owned()),
329            self.binoculars().to_arma(),
330            self.assigned_items().to_arma(),
331        ])
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn b_soldier_at_f() {
341        let loadout = r#"[["arifle_MXC_Holo_pointer_F", "", "acc_pointer_IR", "optic_Holosight", ["30Rnd_65x39_caseless_mag", 30], [], ""],
342        ["launch_B_Titan_short_F", "", "", "", ["Titan_AT", 1], [], ""],
343        ["hgun_P07_F", "", "", "", ["16Rnd_9x21_Mag", 16], [], ""],
344        ["U_B_CombatUniform_mcam", [["FirstAidKit", 1], ["30Rnd_65x39_caseless_mag", 2, 30], ["Chemlight_green", 1, 1]]],
345        ["V_PlateCarrier1_rgr", [["30Rnd_65x39_caseless_mag", 3, 30], ["16Rnd_9x21_Mag", 2, 16], ["SmokeShell", 1 ,1], ["SmokeShellGreen", 1, 1], ["Chemlight_green", 1, 1]]],
346        ["B_AssaultPack_mcamo_AT",[["Titan_AT", 2, 1]]],
347        "H_HelmetB_light_desert", "G_Bandanna_tan",[],
348        ["ItemMap", "", "ItemRadio", "ItemCompass", "ItemWatch", "NVGoggles"]]"#;
349        Loadout::from_arma(loadout.to_string()).unwrap();
350        let loadout = r#"[["arifle_SPAR_02_blk_F","","","optic_Holosight_blk_F",["30Rnd_556x45_Stanag",30],[],""],[],["hgun_ACPC2_F","","","",["9Rnd_45ACP_Mag",8],[],""],["tacs_Uniform_Polo_TP_LS_TP_TB_NoLogo",[]],["V_PlateCarrier1_rgr_noflag_F",[]],[],"H_Cap_headphones","G_Shades_Black",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]"#;
351        let mut loadout = Loadout::from_arma(loadout.to_string()).unwrap();
352        loadout.set_secondary({
353            let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
354            weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
355            weapon
356        });
357        Loadout::from_arma(loadout.to_arma().to_string()).unwrap();
358    }
359
360    #[test]
361    fn marshal() {
362        let loadout = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
363        let mut loadout = Loadout::from_arma(loadout.to_string()).unwrap();
364        loadout.set_secondary({
365            let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
366            weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
367            weapon
368        });
369        loadout.set_primary({
370            let mut weapon = Weapon::new("arifle_MXC_F".to_string());
371            weapon.set_optic("optic_Holosight".to_string());
372            weapon
373        });
374        let uniform = loadout.uniform_mut();
375        uniform.set_class("U_B_CombatUniform_mcam".to_string());
376        let uniform_items = uniform.items_mut().unwrap();
377        uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 3));
378        uniform_items.push(InventoryItem::new_magazine(
379            "30Rnd_65x39_caseless_mag".to_string(),
380            5,
381            30,
382        ));
383    }
384
385    #[test]
386    fn extended_empty() {
387        let loadout = r#"[[["arifle_XMS_Shot_lxWS","","tacgt_ANPEQ_15_Low_Light_Black","CUP_optic_Elcan_SpecterDR_black_PIP",["tacgt_30Rnd_556x45_Ball_Tracer_PMAG",30],["6Rnd_12Gauge_Pellets",6],""],["CUP_launch_M136_Loaded","","","",[],[],""],[],["tacs_Uniform_Floral_JP_RS_LP_BB",[["kat_guedel",1],["ACE_EntrenchingTool",1],["ACE_EarPlugs",1],["ACE_CableTie",2],["ACE_quikclot",1],["ACE_packingBandage",2],["ACE_elasticBandage",1],["SmokeShell",2,1],["Chemlight_yellow",1,1],["Chemlight_red",3,1],["ACE_Chemlight_IR",2,1],["ACE_Chemlight_HiBlue",1,1]]],["milgp_v_mmac_marksman_belt_CB",[["ACE_tourniquet",4],["ACE_splint",1],["tacgt_30Rnd_556x45_EPR_PMAG",15,30],["tacgt_30Rnd_556x45_AP_PMAG",2,30],["6rnd_Smoke_Mag_lxWS",1,6]]],["milgp_bp_Pointman_cb",[["ACE_SpraypaintGreen",1],["ACE_splint",1],["synixe_painkillers",2],["ACE_microDAGR",1],["ACE_MapTools",1],["ACE_bodyBag",1],["ACE_packingBandage",10],["ACE_elasticBandage",9],["ACE_quikclot",5],["ACE_EarPlugs",2],["ACRE_PRC152",1],["ACRE_PRC152",1],["6Rnd_12Gauge_Pellets",2,6],["6Rnd_12Gauge_Slug",1,6],["tacgt_30Rnd_556x45_Ball_Tracer_PMAG",1,30]]],"synixe_contractors_Hat_Beret_Black","",["ACE_VectorDay","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[]]"#;
388        let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
389        assert!(loadout.cba_extended().is_empty());
390    }
391
392    #[test]
393    fn extended_crash_1_9_0() {
394        let loadout = r#"[[[],[],[],["synixe_contractors_Uniform_Contractor_Shirt",[]],[],[],"","",[],["","","","","",""]],[]]"#;
395        let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
396        assert!(loadout.cba_extended().is_empty());
397    }
398
399    #[test]
400    fn extended_items() {
401        let loadout = r#"[[["CUP_arifle_M4A1_SOMMOD_Grip_tan","","","CUP_optic_Eotech553_Black",["tacgt_30Rnd_556x45_EPR_PMAG_Tan",30],[],""],[],["ACE_VMM3","","","",[],[],""],["casual_plaid_gray_khaki_uniform",[["ACE_packingBandage",10],["ACE_elasticBandage",10],["ACE_CableTie",2],["kat_guedel",1],["ACE_tourniquet",2],["ACE_splint",1],["synixe_painkillers",2]]],["milgp_v_mmac_assaulter_belt_AOR2",[["ACRE_PRC152",1],["SmokeShell",2,1],["HandGrenade",2,1],["tacgt_30Rnd_556x45_EPR_PMAG_Tan",10,30]]],["B_MU_TacticalPack_cbr",[["ACE_bodyBag",1],["ToolKit",1],["ACE_SpraypaintGreen",1],["synixe_axe",1],["ACE_wirecutter",1],["ACE_EntrenchingTool",1],["ACE_rope3",2],["DemoCharge_Remote_Mag",2,1]]],"synixe_contractors_Cap_Headphones_GreenLogo","CUP_G_Tan_Scarf_Shades",["Binocular","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[["grad_slingHelmet","CUP_H_OpsCore_Grey"]]]"#;
402        let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
403        assert!(!loadout.cba_extended().is_empty());
404        assert_eq!(
405            loadout.primary().class(),
406            Some("CUP_arifle_M4A1_SOMMOD_Grip_tan")
407        );
408        assert_eq!(
409            loadout.cba_extended().get("grad_slingHelmet"),
410            Some(&Value::String("CUP_H_OpsCore_Grey".to_string()))
411        );
412    }
413
414    #[test]
415    fn classes() {
416        let loadout = r#"[[["CUP_arifle_M4A1_SOMMOD_Grip_tan","","","CUP_optic_Eotech553_Black",["tacgt_30Rnd_556x45_EPR_PMAG_Tan",30],[],""],[],["ACE_VMM3","","","",[],[],""],["casual_plaid_gray_khaki_uniform",[["ACE_packingBandage",10],["ACE_elasticBandage",10],["ACE_CableTie",2],["kat_guedel",1],["ACE_tourniquet",2],["ACE_splint",1],["synixe_painkillers",2]]],["milgp_v_mmac_assaulter_belt_AOR2",[["ACRE_PRC152",1],["SmokeShell",2,1],["HandGrenade",2,1],["tacgt_30Rnd_556x45_EPR_PMAG_Tan",10,30]]],["B_MU_TacticalPack_cbr",[["ACE_bodyBag",1],["ToolKit",1],["ACE_SpraypaintGreen",1],["synixe_axe",1],["ACE_wirecutter",1],["ACE_EntrenchingTool",1],["ACE_rope3",2],["DemoCharge_Remote_Mag",2,1]]],"synixe_contractors_Cap_Headphones_GreenLogo","CUP_G_Tan_Scarf_Shades",["Binocular","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[["grad_slingHelmet","CUP_H_OpsCore_Grey"]]]"#;
417        let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
418        assert_eq!(loadout.classes(), {
419            let mut classes = std::collections::HashMap::new();
420            classes.insert("CUP_arifle_M4A1_SOMMOD_Grip_tan".to_string(), 1);
421            classes.insert("CUP_optic_Eotech553_Black".to_string(), 1);
422            classes.insert("tacgt_30Rnd_556x45_EPR_PMAG_Tan".to_string(), 11);
423            classes.insert("ACE_VMM3".to_string(), 1);
424            classes.insert("casual_plaid_gray_khaki_uniform".to_string(), 1);
425            classes.insert("ACE_packingBandage".to_string(), 10);
426            classes.insert("ACE_elasticBandage".to_string(), 10);
427            classes.insert("ACE_CableTie".to_string(), 2);
428            classes.insert("kat_guedel".to_string(), 1);
429            classes.insert("ACE_tourniquet".to_string(), 2);
430            classes.insert("ACE_splint".to_string(), 1);
431            classes.insert("synixe_painkillers".to_string(), 2);
432            classes.insert("milgp_v_mmac_assaulter_belt_AOR2".to_string(), 1);
433            classes.insert("ACRE_PRC152".to_string(), 1);
434            classes.insert("SmokeShell".to_string(), 2);
435            classes.insert("HandGrenade".to_string(), 2);
436            classes.insert("B_MU_TacticalPack_cbr".to_string(), 1);
437            classes.insert("ACE_bodyBag".to_string(), 1);
438            classes.insert("ToolKit".to_string(), 1);
439            classes.insert("ACE_SpraypaintGreen".to_string(), 1);
440            classes.insert("synixe_axe".to_string(), 1);
441            classes.insert("ACE_wirecutter".to_string(), 1);
442            classes.insert("ACE_EntrenchingTool".to_string(), 1);
443            classes.insert("ACE_rope3".to_string(), 2);
444            classes.insert("DemoCharge_Remote_Mag".to_string(), 2);
445            classes.insert("synixe_contractors_Cap_Headphones_GreenLogo".to_string(), 1);
446            classes.insert("CUP_G_Tan_Scarf_Shades".to_string(), 1);
447            classes.insert("Binocular".to_string(), 1);
448            classes.insert("ItemMap".to_string(), 1);
449            classes.insert("ItemGPS".to_string(), 1);
450            classes.insert("ItemCompass".to_string(), 1);
451            classes.insert("ItemWatch".to_string(), 1);
452            classes
453        });
454    }
455}