genie_rec/
unit_type.rs

1use crate::Result;
2use arrayvec::ArrayVec;
3use byteorder::{ReadBytesExt, WriteBytesExt, LE};
4pub use genie_dat::AttributeCost;
5pub use genie_support::{StringKey, UnitTypeID};
6use std::cmp;
7use std::convert::{TryFrom, TryInto};
8
9use std::io::{Read, Write};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum UnitBaseClass {
13    Static = 10,
14    Animated = 20,
15    Doppelganger = 25,
16    Moving = 30,
17    Action = 40,
18    BaseCombat = 50,
19    Missile = 60,
20    Combat = 70,
21    Building = 80,
22    Tree = 90,
23}
24
25impl cmp::PartialOrd for UnitBaseClass {
26    fn partial_cmp(&self, other: &UnitBaseClass) -> Option<cmp::Ordering> {
27        if self == other {
28            return Some(cmp::Ordering::Equal);
29        }
30
31        let self_n = *self as u8;
32        let other_n = *other as u8;
33
34        // handle weird leaves specially
35        match self {
36            Self::Doppelganger => {
37                if self_n > other_n {
38                    Some(cmp::Ordering::Greater)
39                } else {
40                    None
41                }
42            }
43            Self::Missile => {
44                if self_n > other_n {
45                    Some(cmp::Ordering::Greater)
46                } else {
47                    None
48                }
49            }
50            Self::Tree => match other {
51                Self::Static => Some(cmp::Ordering::Greater),
52                _ => None,
53            },
54            _ => match other {
55                Self::Doppelganger => {
56                    if self_n < other_n {
57                        Some(cmp::Ordering::Less)
58                    } else {
59                        None
60                    }
61                }
62                Self::Missile => {
63                    if self_n < other_n {
64                        Some(cmp::Ordering::Less)
65                    } else {
66                        None
67                    }
68                }
69                Self::Tree => match self {
70                    Self::Static => Some(cmp::Ordering::Less),
71                    _ => None,
72                },
73                _ => Some(self_n.cmp(&other_n)),
74            },
75        }
76    }
77}
78
79#[derive(Debug, Clone, Copy, thiserror::Error)]
80#[error("unknown unit base class: {}", .0)]
81pub struct ParseUnitBaseClassError(u8);
82
83impl TryFrom<u8> for UnitBaseClass {
84    type Error = ParseUnitBaseClassError;
85
86    fn try_from(n: u8) -> std::result::Result<Self, Self::Error> {
87        match n {
88            10 => Ok(UnitBaseClass::Static),
89            20 => Ok(UnitBaseClass::Animated),
90            25 => Ok(UnitBaseClass::Doppelganger),
91            30 => Ok(UnitBaseClass::Moving),
92            40 => Ok(UnitBaseClass::Action),
93            50 => Ok(UnitBaseClass::BaseCombat),
94            60 => Ok(UnitBaseClass::Missile),
95            70 => Ok(UnitBaseClass::Combat),
96            80 => Ok(UnitBaseClass::Building),
97            90 => Ok(UnitBaseClass::Tree),
98            n => Err(ParseUnitBaseClassError(n)),
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104pub struct CompactUnitType {
105    pub unit_base_class: UnitBaseClass,
106    pub static_: StaticUnitAttributes,
107    pub animated: Option<AnimatedUnitAttributes>,
108    pub moving: Option<MovingUnitAttributes>,
109    pub action: Option<ActionUnitAttributes>,
110    pub base_combat: Option<BaseCombatUnitAttributes>,
111    pub missile: Option<MissileUnitAttributes>,
112    pub combat: Option<CombatUnitAttributes>,
113    pub building: Option<BuildingUnitAttributes>,
114}
115
116impl CompactUnitType {
117    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
118        let unit_base_class = input.read_u8()?.try_into().unwrap();
119        let static_ = StaticUnitAttributes::read_from(&mut input)?;
120        let mut unit = Self {
121            unit_base_class,
122            static_,
123            animated: None,
124            moving: None,
125            action: None,
126            base_combat: None,
127            missile: None,
128            combat: None,
129            building: None,
130        };
131        if unit_base_class >= UnitBaseClass::Animated {
132            unit.animated = Some(AnimatedUnitAttributes::read_from(&mut input)?);
133        }
134        if unit_base_class >= UnitBaseClass::Moving {
135            unit.moving = Some(MovingUnitAttributes::read_from(&mut input)?);
136        }
137        if unit_base_class >= UnitBaseClass::Action {
138            unit.action = Some(ActionUnitAttributes::read_from(&mut input)?);
139        }
140        if unit_base_class >= UnitBaseClass::BaseCombat {
141            unit.base_combat = Some(BaseCombatUnitAttributes::read_from(&mut input, version)?);
142        }
143        if unit_base_class >= UnitBaseClass::Missile {
144            unit.missile = Some(MissileUnitAttributes::read_from(&mut input)?);
145        }
146        if unit_base_class >= UnitBaseClass::Combat {
147            unit.combat = Some(CombatUnitAttributes::read_from(&mut input)?);
148        }
149        if unit_base_class >= UnitBaseClass::Building {
150            unit.building = Some(BuildingUnitAttributes::read_from(&mut input)?);
151        }
152        Ok(unit)
153    }
154}
155
156#[derive(Debug, Default, Clone)]
157pub struct StaticUnitAttributes {
158    id: UnitTypeID,
159    copy_id: UnitTypeID,
160    base_id: UnitTypeID,
161    unit_class: u16,
162    hotkey_id: u32,
163    available: bool,
164    death_object_id: Option<UnitTypeID>,
165    string_id: Option<StringKey>,
166    description_id: Option<StringKey>,
167    flags: Option<u32>,
168    help_string_id: Option<StringKey>,
169    terrain_restriction: Option<u16>,
170    hidden_in_editor: bool,
171    is_queueable_tech: bool,
172    hit_points: u16,
173    line_of_sight: f32,
174    garrison_capacity: u8,
175    radius: (f32, f32),
176    attribute_max_amount: u16,
177    attribute_amount_held: f32,
178    disabled: bool,
179}
180
181impl StaticUnitAttributes {
182    pub fn read_from(mut input: impl Read) -> Result<Self> {
183        let mut attrs = Self::default();
184        attrs.id = input.read_u16::<LE>()?.into();
185        attrs.copy_id = input.read_u16::<LE>()?.into();
186        attrs.base_id = input.read_u16::<LE>()?.into();
187        attrs.unit_class = input.read_u16::<LE>()?;
188        attrs.hotkey_id = input.read_u32::<LE>()?;
189        attrs.available = input.read_u8()? != 0;
190        let hidden_in_editor = input.read_i8()?;
191        // UserPatch data
192        let hidden_flags = if hidden_in_editor == -16 {
193            attrs.death_object_id = match input.read_i16::<LE>()? {
194                -1 => None,
195                id => Some(id.try_into().unwrap()),
196            };
197            attrs.string_id = Some(input.read_u16::<LE>()?.into());
198            attrs.description_id = Some(input.read_u16::<LE>()?.into());
199            attrs.flags = Some(input.read_u32::<LE>()?);
200            attrs.help_string_id = Some(input.read_u32::<LE>()?.into());
201            attrs.terrain_restriction = Some(input.read_u16::<LE>()?);
202            input.read_i8()?
203        } else {
204            hidden_in_editor
205        };
206        attrs.hidden_in_editor = hidden_flags != 0;
207        // Community Patch
208        attrs.is_queueable_tech = (hidden_flags & 2) == 2;
209        attrs.hit_points = input.read_u16::<LE>()?;
210        attrs.line_of_sight = input.read_f32::<LE>()?;
211        attrs.garrison_capacity = input.read_u8()?;
212        attrs.radius = (input.read_f32::<LE>()?, input.read_f32::<LE>()?);
213        attrs.attribute_max_amount = input.read_u16::<LE>()?;
214        attrs.attribute_amount_held = input.read_f32::<LE>()?;
215        attrs.disabled = input.read_u8()? != 0;
216        Ok(attrs)
217    }
218}
219
220#[derive(Debug, Default, Clone)]
221pub struct AnimatedUnitAttributes {
222    pub speed: f32,
223}
224
225impl AnimatedUnitAttributes {
226    pub fn read_from(mut input: impl Read) -> Result<Self> {
227        let speed = input.read_f32::<LE>()?;
228        Ok(Self { speed })
229    }
230
231    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
232        output.write_f32::<LE>(self.speed)?;
233        Ok(())
234    }
235}
236
237#[derive(Debug, Default, Clone)]
238pub struct MovingUnitAttributes {
239    pub turn_speed: f32,
240}
241
242impl MovingUnitAttributes {
243    pub fn read_from(mut input: impl Read) -> Result<Self> {
244        let turn_speed = input.read_f32::<LE>()?;
245        Ok(Self { turn_speed })
246    }
247
248    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
249        output.write_f32::<LE>(self.turn_speed)?;
250        Ok(())
251    }
252}
253
254#[derive(Debug, Default, Clone)]
255pub struct ActionUnitAttributes {
256    pub search_radius: f32,
257    pub work_rate: f32,
258}
259
260impl ActionUnitAttributes {
261    pub fn read_from(mut input: impl Read) -> Result<Self> {
262        let search_radius = input.read_f32::<LE>()?;
263        let work_rate = input.read_f32::<LE>()?;
264        Ok(Self {
265            search_radius,
266            work_rate,
267        })
268    }
269
270    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
271        output.write_f32::<LE>(self.search_radius)?;
272        output.write_f32::<LE>(self.work_rate)?;
273        Ok(())
274    }
275}
276
277#[derive(Debug, Default, Clone, Copy)]
278pub struct HitType {
279    pub hit_type: u16,
280    pub amount: i16,
281}
282
283impl HitType {
284    pub fn read_from(mut input: impl Read) -> Result<Self> {
285        let hit_type = input.read_u16::<LE>()?;
286        let amount = input.read_i16::<LE>()?;
287        Ok(Self { hit_type, amount })
288    }
289}
290
291#[derive(Debug, Default, Clone)]
292pub struct BaseCombatUnitAttributes {
293    pub base_armor: u16,
294    pub attacks: Vec<HitType>,
295    pub armors: Vec<HitType>,
296    pub attack_speed: f32,
297    pub weapon_range_max: f32,
298    pub base_hit_chance: u16,
299    pub projectile_object_id: Option<UnitTypeID>,
300    pub defense_terrain_bonus: Option<u16>,
301    pub weapon_range_max_2: f32,
302    pub area_of_effect: f32,
303    pub weapon_range_min: f32,
304}
305
306impl BaseCombatUnitAttributes {
307    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
308        let mut attrs = Self::default();
309        attrs.base_armor = if version >= 11.52 {
310            input.read_u16::<LE>()?
311        } else {
312            input.read_u8()?.into()
313        };
314        let num_attacks = input.read_u16::<LE>()?;
315        for _ in 0..num_attacks {
316            attrs.attacks.push(HitType::read_from(&mut input)?);
317        }
318        let num_armors = input.read_u16::<LE>()?;
319        for _ in 0..num_armors {
320            attrs.armors.push(HitType::read_from(&mut input)?);
321        }
322        attrs.attack_speed = input.read_f32::<LE>()?;
323        attrs.weapon_range_max = input.read_f32::<LE>()?;
324        attrs.base_hit_chance = input.read_u16::<LE>()?;
325        attrs.projectile_object_id = match input.read_i16::<LE>()? {
326            -1 => None,
327            id => Some(id.try_into().unwrap()),
328        };
329        attrs.defense_terrain_bonus = match input.read_i16::<LE>()? {
330            -1 => None,
331            id => Some(id.try_into().unwrap()),
332        };
333        attrs.weapon_range_max_2 = input.read_f32::<LE>()?;
334        attrs.area_of_effect = input.read_f32::<LE>()?;
335        attrs.weapon_range_min = input.read_f32::<LE>()?;
336        Ok(attrs)
337    }
338
339    pub fn write_to(&self, _output: impl Write) -> Result<()> {
340        todo!()
341    }
342}
343
344#[derive(Debug, Default, Clone)]
345pub struct MissileUnitAttributes {
346    pub targeting_type: u8,
347}
348
349impl MissileUnitAttributes {
350    pub fn read_from(mut input: impl Read) -> Result<Self> {
351        let targeting_type = input.read_u8()?;
352        Ok(Self { targeting_type })
353    }
354
355    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
356        output.write_u8(self.targeting_type)?;
357        Ok(())
358    }
359}
360
361#[derive(Debug, Default, Clone)]
362pub struct CombatUnitAttributes {
363    pub costs: ArrayVec<[AttributeCost; 3]>,
364    pub create_time: u16,
365    pub original_pierce_armor: Option<u16>,
366    pub original_armor: Option<u16>,
367    pub original_weapon: Option<u16>,
368    pub original_weapon_range: Option<f32>,
369    pub area_effect_level: Option<u8>,
370    pub frame_delay: Option<u16>,
371    pub create_at_building: Option<UnitTypeID>,
372    pub create_button: Option<i8>,
373    pub rear_attack_modifier: Option<f32>,
374    pub hero_flag: Option<u8>,
375    pub volley_fire_amount: f32,
376    pub max_attacks_in_volley: u8,
377}
378
379impl CombatUnitAttributes {
380    pub fn read_from(mut input: impl Read) -> Result<Self> {
381        let mut attrs = Self::default();
382        for _ in 0..3 {
383            let attr = AttributeCost::read_from(&mut input)?;
384            if attr.attribute_type >= 0 {
385                attrs.costs.push(attr);
386            }
387        }
388        let create_time = input.read_u16::<LE>()?;
389        // UserPatch data
390        if create_time == u16::max_value() - 15 {
391            attrs.original_pierce_armor = Some(input.read_u16::<LE>()?);
392            attrs.original_armor = Some(input.read_u16::<LE>()?);
393            attrs.original_weapon = Some(input.read_u16::<LE>()?);
394            attrs.original_weapon_range = Some(input.read_f32::<LE>()?);
395            attrs.area_effect_level = Some(input.read_u8()?);
396            attrs.frame_delay = Some(input.read_u16::<LE>()?);
397            attrs.create_at_building = match input.read_i16::<LE>()? {
398                -1 => None,
399                id => Some(id.try_into().unwrap()),
400            };
401            attrs.create_button = Some(input.read_i8()?);
402            attrs.rear_attack_modifier = Some(input.read_f32::<LE>()?);
403            attrs.hero_flag = Some(input.read_u8()?);
404            attrs.create_time = input.read_u16::<LE>()?;
405        } else {
406            attrs.create_time = create_time;
407        };
408        attrs.volley_fire_amount = input.read_f32::<LE>()?;
409        attrs.max_attacks_in_volley = input.read_u8()?;
410        Ok(attrs)
411    }
412
413    pub fn write_to(&self, _output: impl Write) -> Result<()> {
414        todo!()
415    }
416}
417
418#[derive(Debug, Default, Clone)]
419pub struct BuildingUnitAttributes {
420    pub facet: i16,
421    // &facet + 62
422    needs_a_name: Option<u8>,
423    // &facet + 12
424    needs_a_name_2: Option<u8>,
425    pub garrison_heal_rate: Option<f32>,
426}
427
428impl BuildingUnitAttributes {
429    pub fn read_from(mut input: impl Read) -> Result<Self> {
430        let mut attrs = Self::default();
431        let facet = input.read_i16::<LE>()?;
432        // UserPatch data
433        if facet == 32767 {
434            attrs.needs_a_name = Some(input.read_u8()?);
435            attrs.needs_a_name_2 = Some(input.read_u8()?);
436            attrs.garrison_heal_rate = Some(input.read_f32::<LE>()?);
437            attrs.facet = input.read_i16::<LE>()?;
438        } else {
439            attrs.facet = facet;
440        }
441        Ok(attrs)
442    }
443
444    pub fn write_to(&self, _output: impl Write) -> Result<()> {
445        todo!()
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn cmp_unit_base_class() {
455        assert!(UnitBaseClass::Static == UnitBaseClass::Static);
456        assert!(UnitBaseClass::Static < UnitBaseClass::Animated);
457        assert!(UnitBaseClass::Static < UnitBaseClass::Doppelganger);
458        assert!(UnitBaseClass::Animated < UnitBaseClass::Doppelganger);
459        assert!(!(UnitBaseClass::Moving < UnitBaseClass::Doppelganger));
460        assert!(!(UnitBaseClass::Moving > UnitBaseClass::Doppelganger));
461    }
462}