genie_dat/
unit_type.rs

1use crate::sound::SoundID;
2use crate::sprite::{GraphicID, SpriteID};
3use crate::task::TaskList;
4use crate::terrain::TerrainID;
5use arrayvec::ArrayVec;
6use byteorder::{ReadBytesExt, WriteBytesExt, LE};
7pub use genie_support::UnitTypeID;
8use genie_support::{read_opt_u16, read_opt_u32, MapInto, StringKey, TechID};
9use std::convert::TryInto;
10use std::io::{self, Read, Result, Write};
11
12pub type UnitClass = u16;
13
14#[derive(Debug, Clone)]
15pub enum UnitType {
16    /// The base unit type, for units that do not do anything.
17    Base(Box<BaseUnitType>),
18    /// The tree unit type.
19    Tree(Box<TreeUnitType>),
20    /// Unit type that supports animated sprites.
21    Animated(Box<AnimatedUnitType>),
22    /// Unit type for the "fake" units you see in the fog of war, after the actual unit has been
23    /// destroyed.
24    Doppleganger(Box<DopplegangerUnitType>),
25    /// Unit type that supports movement.
26    Moving(Box<MovingUnitType>),
27    /// Unit type that supports being tasked by a player.
28    Action(Box<ActionUnitType>),
29    /// Unit type that supports combat.
30    BaseCombat(Box<BaseCombatUnitType>),
31    /// Unit type for projectiles/missiles/arrows.
32    Missile(Box<MissileUnitType>),
33    /// Unit type that supports combat (with additional Age of Empires specific data).
34    Combat(Box<CombatUnitType>),
35    /// Unit type for buildings.
36    Building(Box<BuildingUnitType>),
37}
38
39macro_rules! cast_unit_type {
40    ($struct:ident, $tag:ident) => {
41        impl From<$struct> for UnitType {
42            fn from(v: $struct) -> Self {
43                UnitType::$tag(Box::new(v))
44            }
45        }
46    };
47}
48
49macro_rules! inherit_unit_type {
50    ($struct:ident, $super:ident) => {
51        impl std::ops::Deref for $struct {
52            type Target = $super;
53            fn deref(&self) -> &Self::Target {
54                &self.superclass
55            }
56        }
57    };
58}
59
60cast_unit_type!(BaseUnitType, Base);
61cast_unit_type!(TreeUnitType, Tree);
62cast_unit_type!(AnimatedUnitType, Animated);
63cast_unit_type!(DopplegangerUnitType, Doppleganger);
64cast_unit_type!(MovingUnitType, Moving);
65cast_unit_type!(ActionUnitType, Action);
66cast_unit_type!(BaseCombatUnitType, BaseCombat);
67cast_unit_type!(MissileUnitType, Missile);
68cast_unit_type!(CombatUnitType, Combat);
69cast_unit_type!(BuildingUnitType, Building);
70
71// inherit_unit_type!(TreeUnitType, BaseUnitType);
72inherit_unit_type!(AnimatedUnitType, BaseUnitType);
73// inherit_unit_type!(DopplegangerUnitType, AnimatedUnitType);
74inherit_unit_type!(MovingUnitType, AnimatedUnitType);
75inherit_unit_type!(ActionUnitType, MovingUnitType);
76inherit_unit_type!(BaseCombatUnitType, ActionUnitType);
77inherit_unit_type!(MissileUnitType, BaseCombatUnitType);
78inherit_unit_type!(CombatUnitType, BaseCombatUnitType);
79inherit_unit_type!(BuildingUnitType, CombatUnitType);
80
81impl UnitType {
82    fn type_id(&self) -> u8 {
83        use UnitType::*;
84        match self {
85            Base(_) => 10,
86            Tree(_) => 15,
87            Animated(_) => 20,
88            Doppleganger(_) => 25,
89            Moving(_) => 30,
90            Action(_) => 40,
91            BaseCombat(_) => 50,
92            Missile(_) => 60,
93            Combat(_) => 70,
94            Building(_) => 80,
95        }
96    }
97
98    /// Read a unit type from an input stream.
99    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
100        let unit_type = input.read_u8()?;
101        match unit_type {
102            10 => BaseUnitType::read_from(input, version).map_into(),
103            15 => TreeUnitType::read_from(input, version).map_into(),
104            20 => AnimatedUnitType::read_from(input, version).map_into(),
105            25 => DopplegangerUnitType::read_from(input, version).map_into(),
106            30 => MovingUnitType::read_from(input, version).map_into(),
107            40 => ActionUnitType::read_from(input, version).map_into(),
108            50 => BaseCombatUnitType::read_from(input, version).map_into(),
109            60 => MissileUnitType::read_from(input, version).map_into(),
110            70 => CombatUnitType::read_from(input, version).map_into(),
111            80 => BuildingUnitType::read_from(input, version).map_into(),
112            _ => panic!("unexpected unit type {}, this is probably a bug", unit_type),
113        }
114    }
115
116    /// Write this unit type to an output stream.
117    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
118        use UnitType::*;
119        output.write_u8(self.type_id())?;
120
121        match self {
122            Base(unit) => unit.write_to(output, version)?,
123            Tree(unit) => unit.write_to(output, version)?,
124            Animated(unit) => unit.write_to(output, version)?,
125            Doppleganger(unit) => unit.write_to(output, version)?,
126            Moving(unit) => unit.write_to(output, version)?,
127            Action(unit) => unit.write_to(output, version)?,
128            BaseCombat(unit) => unit.write_to(output, version)?,
129            Missile(unit) => unit.write_to(output, version)?,
130            Combat(unit) => unit.write_to(output, version)?,
131            Building(unit) => unit.write_to(output, version)?,
132        }
133
134        Ok(())
135    }
136
137    /// Get the base unit type properties for this unit.
138    pub fn base(&self) -> &BaseUnitType {
139        use UnitType::*;
140        match self {
141            Base(unit) => &unit,
142            Tree(unit) => &unit.0,
143            Animated(unit) => &unit,
144            Doppleganger(unit) => &unit.0,
145            Moving(unit) => &unit,
146            Action(unit) => &unit,
147            BaseCombat(unit) => &unit,
148            Missile(unit) => &unit,
149            Combat(unit) => &unit,
150            Building(unit) => &unit,
151        }
152    }
153
154    /// Get the animated unit type properties for this unit.
155    pub fn animated(&self) -> Option<&AnimatedUnitType> {
156        use UnitType::*;
157        match self {
158            Base(_) | Tree(_) => None,
159            Animated(unit) => Some(&unit),
160            Doppleganger(unit) => Some(&unit.0),
161            Moving(unit) => Some(&unit),
162            Action(unit) => Some(&unit),
163            BaseCombat(unit) => Some(&unit),
164            Missile(unit) => Some(&unit),
165            Combat(unit) => Some(&unit),
166            Building(unit) => Some(&unit),
167        }
168    }
169
170    /// Get the moving unit type properties for this unit.
171    pub fn moving(&self) -> Option<&MovingUnitType> {
172        use UnitType::*;
173        match self {
174            Base(_) | Tree(_) | Animated(_) | Doppleganger(_) => None,
175            Moving(unit) => Some(&unit),
176            Action(unit) => Some(&unit),
177            BaseCombat(unit) => Some(&unit),
178            Missile(unit) => Some(&unit),
179            Combat(unit) => Some(&unit),
180            Building(unit) => Some(&unit),
181        }
182    }
183
184    /// Get the action unit type properties for this unit.
185    pub fn action(&self) -> Option<&ActionUnitType> {
186        use UnitType::*;
187        match self {
188            Base(_) | Tree(_) | Animated(_) | Doppleganger(_) | Moving(_) => None,
189            Action(unit) => Some(&unit),
190            BaseCombat(unit) => Some(&unit),
191            Missile(unit) => Some(&unit),
192            Combat(unit) => Some(&unit),
193            Building(unit) => Some(&unit),
194        }
195    }
196}
197
198#[derive(Debug, Default, Clone, Copy)]
199pub struct UnitAttribute {
200    pub attribute_type: u16,
201    pub amount: f32,
202    pub flag: u8,
203}
204
205impl UnitAttribute {
206    pub fn read_from(mut input: impl Read) -> Result<Self> {
207        Ok(Self {
208            attribute_type: input.read_u16::<LE>()?,
209            amount: input.read_f32::<LE>()?,
210            flag: input.read_u8()?,
211        })
212    }
213
214    pub fn write_to(self, mut output: impl Write) -> Result<()> {
215        output.write_u16::<LE>(self.attribute_type)?;
216        output.write_f32::<LE>(self.amount)?;
217        output.write_u8(self.flag)?;
218        Ok(())
219    }
220
221    fn write_empty(mut output: impl Write) -> Result<()> {
222        output.write_u16::<LE>(0xFFFF)?;
223        output.write_f32::<LE>(0.0)?;
224        output.write_u8(0)?;
225        Ok(())
226    }
227}
228
229#[derive(Debug, Default, Clone)]
230pub struct DamageSprite {
231    pub sprite: SpriteID,
232    pub damage_percent: u16,
233    pub flag: u8,
234}
235
236impl DamageSprite {
237    pub fn read_from(mut input: impl Read) -> Result<Self> {
238        Ok(Self {
239            sprite: input.read_u16::<LE>()?.into(),
240            damage_percent: input.read_u16::<LE>()?,
241            flag: input.read_u8()?,
242        })
243    }
244
245    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
246        output.write_u16::<LE>(self.sprite.into())?;
247        output.write_u16::<LE>(self.damage_percent)?;
248        output.write_u8(self.flag)?;
249        Ok(())
250    }
251}
252
253#[derive(Debug, Default, Clone)]
254pub struct BaseUnitType {
255    name: String,
256    pub id: UnitTypeID,
257    pub string_id: StringKey,
258    string_id2: Option<StringKey>,
259    pub unit_class: UnitClass,
260    pub standing_sprite_1: Option<SpriteID>,
261    pub standing_sprite_2: Option<SpriteID>,
262    pub dying_sprite: Option<SpriteID>,
263    pub undead_sprite: Option<SpriteID>,
264    pub undead_flag: u8,
265    pub hp: u16,
266    pub los: f32,
267    pub garrison_capacity: u8,
268    pub radius: (f32, f32, f32),
269    pub train_sound: Option<SoundID>,
270    pub damage_sound: Option<SoundID>,
271    pub death_spawn: Option<UnitTypeID>,
272    pub sort_number: u8,
273    pub can_be_built_on: bool,
274    pub button_picture: Option<GraphicID>,
275    pub hide_in_scenario_editor: bool,
276    pub portrait_picture: Option<GraphicID>,
277    pub enabled: bool,
278    pub disabled: bool,
279    pub tile_req: (i16, i16),
280    pub center_tile_req: (i16, i16),
281    pub construction_radius: (f32, f32),
282    pub elevation_flag: bool,
283    pub fog_flag: bool,
284    pub terrain_restriction_id: u16,
285    pub movement_type: u8,
286    pub attribute_max_amount: u16,
287    pub attribute_rot: f32,
288    pub area_effect_level: u8,
289    pub combat_level: u8,
290    pub select_level: u8,
291    pub map_draw_level: u8,
292    pub unit_level: u8,
293    pub multiple_attribute_mod: f32,
294    pub map_color: u8,
295    pub help_string_id: StringKey,
296    pub help_page_id: u32,
297    pub hotkey_id: u32,
298    pub recyclable: bool,
299    pub track_as_resource: bool,
300    pub create_doppleganger: bool,
301    pub resource_group: u8,
302    pub occlusion_mask: u8,
303    pub obstruction_type: u8,
304    pub selection_shape: u8,
305    pub object_flags: u32,
306    pub civilization: u8,
307    pub attribute_piece: u8,
308    pub outline_radius: (f32, f32, f32),
309    pub attributes: ArrayVec<[UnitAttribute; 3]>,
310    pub damage_sprites: Vec<DamageSprite>,
311    pub selected_sound: Option<SoundID>,
312    pub death_sound: Option<SoundID>,
313    pub attack_reaction: u8,
314    pub convert_terrain_flag: u8,
315    pub copy_id: u16,
316    pub unit_group: u16,
317}
318
319impl BaseUnitType {
320    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
321        let mut unit_type = Self::default();
322        let name_len = input.read_u16::<LE>()?;
323        unit_type.id = input.read_u16::<LE>()?.into();
324        unit_type.string_id = input.read_u16::<LE>()?.into();
325        unit_type.string_id2 = read_opt_u16(&mut input)?;
326        unit_type.unit_class = input.read_u16::<LE>()?;
327        unit_type.standing_sprite_1 = read_opt_u16(&mut input)?;
328        unit_type.standing_sprite_2 = read_opt_u16(&mut input)?;
329        unit_type.dying_sprite = read_opt_u16(&mut input)?;
330        unit_type.undead_sprite = read_opt_u16(&mut input)?;
331        unit_type.undead_flag = input.read_u8()?;
332        unit_type.hp = input.read_u16::<LE>()?;
333        unit_type.los = input.read_f32::<LE>()?;
334        unit_type.garrison_capacity = input.read_u8()?;
335        unit_type.radius = (
336            input.read_f32::<LE>()?,
337            input.read_f32::<LE>()?,
338            input.read_f32::<LE>()?,
339        );
340        unit_type.train_sound = read_opt_u16(&mut input)?;
341        unit_type.damage_sound = read_opt_u16(&mut input)?;
342        unit_type.death_spawn = read_opt_u16(&mut input)?;
343        unit_type.sort_number = input.read_u8()?;
344        unit_type.can_be_built_on = input.read_u8()? != 0;
345        unit_type.button_picture = read_opt_u16(&mut input)?;
346        unit_type.hide_in_scenario_editor = input.read_u8()? != 0;
347        unit_type.portrait_picture = read_opt_u16(&mut input)?;
348        unit_type.enabled = input.read_u8()? != 0;
349        unit_type.disabled = input.read_u8()? != 0;
350        unit_type.tile_req = (input.read_i16::<LE>()?, input.read_i16::<LE>()?);
351        unit_type.center_tile_req = (input.read_i16::<LE>()?, input.read_i16::<LE>()?);
352        unit_type.construction_radius = (input.read_f32::<LE>()?, input.read_f32::<LE>()?);
353        unit_type.elevation_flag = input.read_u8()? != 0;
354        unit_type.fog_flag = input.read_u8()? != 0;
355        unit_type.terrain_restriction_id = input.read_u16::<LE>()?;
356        unit_type.movement_type = input.read_u8()?;
357        unit_type.attribute_max_amount = input.read_u16::<LE>()?;
358        unit_type.attribute_rot = input.read_f32::<LE>()?;
359        unit_type.area_effect_level = input.read_u8()?;
360        unit_type.combat_level = input.read_u8()?;
361        unit_type.select_level = input.read_u8()?;
362        unit_type.map_draw_level = input.read_u8()?;
363        unit_type.unit_level = input.read_u8()?;
364        unit_type.multiple_attribute_mod = input.read_f32::<LE>()?;
365        unit_type.map_color = input.read_u8()?;
366        unit_type.help_string_id = input.read_u32::<LE>()?.into();
367        unit_type.help_page_id = input.read_u32::<LE>()?;
368        unit_type.hotkey_id = input.read_u32::<LE>()?;
369        unit_type.recyclable = input.read_u8()? != 0;
370        unit_type.track_as_resource = input.read_u8()? != 0;
371        unit_type.create_doppleganger = input.read_u8()? != 0;
372        unit_type.resource_group = input.read_u8()?;
373        unit_type.occlusion_mask = input.read_u8()?;
374        unit_type.obstruction_type = input.read_u8()?;
375        unit_type.selection_shape = input.read_u8()?;
376        unit_type.object_flags = if version < 11.55 {
377            0
378        } else {
379            input.read_u32::<LE>()?
380        };
381        unit_type.civilization = input.read_u8()?;
382        unit_type.attribute_piece = input.read_u8()?;
383        unit_type.outline_radius = (
384            input.read_f32::<LE>()?,
385            input.read_f32::<LE>()?,
386            input.read_f32::<LE>()?,
387        );
388        for _ in 0..3 {
389            let attr = UnitAttribute::read_from(&mut input)?;
390            if attr.attribute_type != 0xFFFF {
391                unit_type.attributes.push(attr);
392            }
393        }
394        unit_type.damage_sprites = {
395            let num_damage_sprites = input.read_u8()?;
396            let mut damage_sprites = vec![];
397            for _ in 0..num_damage_sprites {
398                damage_sprites.push(DamageSprite::read_from(&mut input)?);
399            }
400            damage_sprites
401        };
402        unit_type.selected_sound = read_opt_u16(&mut input)?;
403        unit_type.death_sound = read_opt_u16(&mut input)?;
404        unit_type.attack_reaction = input.read_u8()?;
405        unit_type.convert_terrain_flag = input.read_u8()?;
406        unit_type.name = {
407            // TODO use not-UTF8 for the name
408            let mut bytes = vec![0; usize::from(name_len)];
409            input.read_exact(&mut bytes)?;
410            String::from_utf8(bytes.iter().cloned().take_while(|b| *b != 0).collect()).unwrap()
411        };
412        unit_type.copy_id = input.read_u16::<LE>()?;
413        unit_type.unit_group = input.read_u16::<LE>()?;
414        Ok(unit_type)
415    }
416
417    /// Write this unit type to an output stream.
418    pub fn write_to(&self, mut output: impl Write, _version: f32) -> Result<()> {
419        // TODO use not-UTF8 for the name
420        output.write_u16::<LE>(self.name.len() as u16)?;
421        output.write_u16::<LE>(self.id.into())?;
422        output.write_i16::<LE>((&self.string_id).try_into().unwrap())?;
423        write_opt_string_key(&mut output, &self.string_id2)?;
424        output.write_u16::<LE>(self.unit_class)?;
425        output.write_i16::<LE>(
426            self.standing_sprite_1
427                .map(|id| id.try_into().unwrap())
428                .unwrap_or(-1),
429        )?;
430        output.write_i16::<LE>(
431            self.standing_sprite_2
432                .map(|id| id.try_into().unwrap())
433                .unwrap_or(-1),
434        )?;
435        output.write_i16::<LE>(
436            self.dying_sprite
437                .map(|id| id.try_into().unwrap())
438                .unwrap_or(-1),
439        )?;
440        output.write_i16::<LE>(
441            self.undead_sprite
442                .map(|id| id.try_into().unwrap())
443                .unwrap_or(-1),
444        )?;
445        output.write_u8(self.undead_flag)?;
446        output.write_u16::<LE>(self.hp)?;
447        output.write_f32::<LE>(self.los)?;
448        output.write_u8(self.garrison_capacity)?;
449        output.write_f32::<LE>(self.radius.0)?;
450        output.write_f32::<LE>(self.radius.1)?;
451        output.write_f32::<LE>(self.radius.2)?;
452        output.write_i16::<LE>(
453            self.train_sound
454                .map(|id| id.try_into().unwrap())
455                .unwrap_or(-1),
456        )?;
457        output.write_i16::<LE>(
458            self.damage_sound
459                .map(|id| id.try_into().unwrap())
460                .unwrap_or(-1),
461        )?;
462        output.write_i16::<LE>(
463            self.death_spawn
464                .map(|id| id.try_into().unwrap())
465                .unwrap_or(-1),
466        )?;
467        output.write_u8(self.sort_number)?;
468        output.write_u8(if self.can_be_built_on { 1 } else { 0 })?;
469        output.write_i16::<LE>(
470            self.button_picture
471                .map(|id| id.try_into().unwrap())
472                .unwrap_or(-1),
473        )?;
474        output.write_u8(if self.hide_in_scenario_editor { 1 } else { 0 })?;
475        output.write_i16::<LE>(
476            self.portrait_picture
477                .map(|id| id.try_into().unwrap())
478                .unwrap_or(-1),
479        )?;
480        output.write_u8(if self.enabled { 1 } else { 0 })?;
481        output.write_u8(if self.disabled { 1 } else { 0 })?;
482        output.write_i16::<LE>(self.tile_req.0)?;
483        output.write_i16::<LE>(self.tile_req.1)?;
484        output.write_i16::<LE>(self.center_tile_req.0)?;
485        output.write_i16::<LE>(self.center_tile_req.1)?;
486        output.write_f32::<LE>(self.construction_radius.0)?;
487        output.write_f32::<LE>(self.construction_radius.1)?;
488        output.write_u8(if self.elevation_flag { 1 } else { 0 })?;
489        output.write_u8(if self.fog_flag { 1 } else { 0 })?;
490        output.write_u16::<LE>(self.terrain_restriction_id)?;
491        output.write_u8(self.movement_type)?;
492        output.write_u16::<LE>(self.attribute_max_amount)?;
493        output.write_f32::<LE>(self.attribute_rot)?;
494        output.write_u8(self.area_effect_level)?;
495        output.write_u8(self.combat_level)?;
496        output.write_u8(self.select_level)?;
497        output.write_u8(self.map_draw_level)?;
498        output.write_u8(self.unit_level)?;
499        output.write_f32::<LE>(self.multiple_attribute_mod)?;
500        output.write_u8(self.map_color)?;
501        output.write_u32::<LE>((&self.help_string_id).try_into().unwrap())?;
502        output.write_u32::<LE>(self.help_page_id)?;
503        output.write_u32::<LE>(self.hotkey_id)?;
504        output.write_u8(if self.recyclable { 1 } else { 0 })?;
505        output.write_u8(if self.track_as_resource { 1 } else { 0 })?;
506        output.write_u8(if self.create_doppleganger { 1 } else { 0 })?;
507        output.write_u8(self.resource_group)?;
508        output.write_u8(self.occlusion_mask)?;
509        output.write_u8(self.obstruction_type)?;
510        output.write_u8(self.selection_shape)?;
511        output.write_u32::<LE>(self.object_flags)?;
512        output.write_u8(self.civilization)?;
513        output.write_u8(self.attribute_piece)?;
514        output.write_f32::<LE>(self.outline_radius.0)?;
515        output.write_f32::<LE>(self.outline_radius.1)?;
516        output.write_f32::<LE>(self.outline_radius.2)?;
517        for index in 0..self.attributes.capacity() {
518            match self.attributes.get(index) {
519                Some(attr) => attr.write_to(&mut output)?,
520                None => UnitAttribute::write_empty(&mut output)?,
521            }
522        }
523        output.write_u8(self.damage_sprites.len().try_into().unwrap())?;
524        for sprite in &self.damage_sprites {
525            sprite.write_to(&mut output)?;
526        }
527        output.write_i16::<LE>(
528            self.selected_sound
529                .map(|id| id.try_into().unwrap())
530                .unwrap_or(-1),
531        )?;
532        output.write_i16::<LE>(
533            self.death_sound
534                .map(|id| id.try_into().unwrap())
535                .unwrap_or(-1),
536        )?;
537        output.write_u8(self.attack_reaction)?;
538        output.write_u8(self.convert_terrain_flag)?;
539        output.write_all(self.name.as_bytes())?;
540        output.write_u16::<LE>(self.copy_id)?;
541        output.write_u16::<LE>(self.unit_group)?;
542        Ok(())
543    }
544}
545
546#[derive(Debug, Default, Clone)]
547pub struct TreeUnitType(BaseUnitType);
548
549impl TreeUnitType {
550    pub fn read_from(input: impl Read, version: f32) -> Result<Self> {
551        BaseUnitType::read_from(input, version).map(Self)
552    }
553
554    /// Write this unit type to an output stream.
555    pub fn write_to(&self, output: impl Write, version: f32) -> Result<()> {
556        self.0.write_to(output, version)
557    }
558}
559
560#[derive(Debug, Default, Clone)]
561pub struct AnimatedUnitType {
562    superclass: BaseUnitType,
563    pub speed: f32,
564}
565
566impl AnimatedUnitType {
567    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
568        Ok(Self {
569            superclass: BaseUnitType::read_from(&mut input, version)?,
570            speed: input.read_f32::<LE>()?,
571        })
572    }
573
574    /// Write this unit type to an output stream.
575    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
576        self.superclass.write_to(&mut output, version)?;
577        output.write_f32::<LE>(self.speed)?;
578        Ok(())
579    }
580}
581
582#[derive(Debug, Default, Clone)]
583pub struct DopplegangerUnitType(AnimatedUnitType);
584
585impl DopplegangerUnitType {
586    pub fn read_from(input: impl Read, version: f32) -> Result<Self> {
587        AnimatedUnitType::read_from(input, version).map(Self)
588    }
589
590    /// Write this unit type to an output stream.
591    pub fn write_to(&self, output: impl Write, version: f32) -> Result<()> {
592        self.0.write_to(output, version)
593    }
594}
595
596#[derive(Debug, Default, Clone)]
597pub struct MovingUnitType {
598    superclass: AnimatedUnitType,
599    pub move_sprite: Option<SpriteID>,
600    pub run_sprite: Option<SpriteID>,
601    pub turn_speed: f32,
602    pub size_class: u8,
603    pub trailing_unit: Option<UnitTypeID>,
604    pub trailing_options: u8,
605    pub trailing_spacing: f32,
606    pub move_algorithm: u8,
607    pub turn_radius: f32,
608    pub turn_radius_speed: f32,
609    pub maximum_yaw_per_second_moving: f32,
610    pub stationary_yaw_revolution_time: f32,
611    pub maximum_yaw_per_second_stationary: f32,
612}
613
614impl MovingUnitType {
615    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
616        let mut unit_type = Self {
617            superclass: AnimatedUnitType::read_from(&mut input, version)?,
618            ..Default::default()
619        };
620        unit_type.move_sprite = read_opt_u16(&mut input)?;
621        unit_type.run_sprite = read_opt_u16(&mut input)?;
622        unit_type.turn_speed = input.read_f32::<LE>()?;
623        unit_type.size_class = input.read_u8()?;
624        unit_type.trailing_unit = read_opt_u16(&mut input)?;
625        unit_type.trailing_options = input.read_u8()?;
626        unit_type.trailing_spacing = input.read_f32::<LE>()?;
627        unit_type.move_algorithm = input.read_u8()?;
628        unit_type.turn_radius = input.read_f32::<LE>()?;
629        unit_type.turn_radius_speed = input.read_f32::<LE>()?;
630        unit_type.maximum_yaw_per_second_moving = input.read_f32::<LE>()?;
631        unit_type.stationary_yaw_revolution_time = input.read_f32::<LE>()?;
632        unit_type.maximum_yaw_per_second_stationary = input.read_f32::<LE>()?;
633        Ok(unit_type)
634    }
635
636    /// Write this unit type to an output stream.
637    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
638        self.superclass.write_to(&mut output, version)?;
639        output.write_i16::<LE>(
640            self.move_sprite
641                .map(|id| id.try_into().unwrap())
642                .unwrap_or(-1),
643        )?;
644        output.write_i16::<LE>(
645            self.run_sprite
646                .map(|id| id.try_into().unwrap())
647                .unwrap_or(-1),
648        )?;
649        output.write_f32::<LE>(self.turn_speed)?;
650        output.write_u8(self.size_class)?;
651        output.write_i16::<LE>(
652            self.trailing_unit
653                .map(|id| id.try_into().unwrap())
654                .unwrap_or(-1),
655        )?;
656        output.write_u8(self.trailing_options)?;
657        output.write_f32::<LE>(self.trailing_spacing)?;
658        output.write_u8(self.move_algorithm)?;
659        output.write_f32::<LE>(self.turn_radius)?;
660        output.write_f32::<LE>(self.turn_radius_speed)?;
661        output.write_f32::<LE>(self.maximum_yaw_per_second_moving)?;
662        output.write_f32::<LE>(self.stationary_yaw_revolution_time)?;
663        output.write_f32::<LE>(self.maximum_yaw_per_second_stationary)?;
664        Ok(())
665    }
666}
667
668#[derive(Debug, Default, Clone)]
669pub struct ActionUnitType {
670    superclass: MovingUnitType,
671    pub default_task: Option<u16>,
672    pub search_radius: f32,
673    pub work_rate: f32,
674    pub drop_site: Option<UnitTypeID>,
675    pub backup_drop_site: Option<UnitTypeID>,
676    pub task_by_group: u8,
677    pub command_sound: Option<SoundID>,
678    pub move_sound: Option<SoundID>,
679    /// Task list for older versions; newer game versions store the task list at the root of the
680    /// dat file, and use `unit_type.copy_id` to refer to one of those task lists.
681    pub tasks: Option<TaskList>,
682    pub run_pattern: u8,
683}
684
685impl ActionUnitType {
686    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
687        let mut unit_type = Self {
688            superclass: MovingUnitType::read_from(&mut input, version)?,
689            ..Default::default()
690        };
691        unit_type.default_task = read_opt_u16(&mut input)?;
692        unit_type.search_radius = input.read_f32::<LE>()?;
693        unit_type.work_rate = input.read_f32::<LE>()?;
694        unit_type.drop_site = read_opt_u16(&mut input)?;
695        unit_type.backup_drop_site = read_opt_u16(&mut input)?;
696        unit_type.task_by_group = input.read_u8()?;
697        unit_type.command_sound = read_opt_u16(&mut input)?;
698        unit_type.move_sound = read_opt_u16(&mut input)?;
699        unit_type.run_pattern = input.read_u8()?;
700        Ok(unit_type)
701    }
702
703    /// Write this unit type to an output stream.
704    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
705        self.superclass.write_to(&mut output, version)?;
706        output.write_i16::<LE>(
707            self.default_task
708                .map(|id| id.try_into().unwrap())
709                .unwrap_or(-1),
710        )?;
711        output.write_f32::<LE>(self.search_radius)?;
712        output.write_f32::<LE>(self.work_rate)?;
713        output.write_i16::<LE>(
714            self.drop_site
715                .map(|id| id.try_into().unwrap())
716                .unwrap_or(-1),
717        )?;
718        output.write_i16::<LE>(
719            self.backup_drop_site
720                .map(|id| id.try_into().unwrap())
721                .unwrap_or(-1),
722        )?;
723        output.write_u8(self.task_by_group)?;
724        output.write_i16::<LE>(
725            self.command_sound
726                .map(|id| id.try_into().unwrap())
727                .unwrap_or(-1),
728        )?;
729        output.write_i16::<LE>(
730            self.move_sound
731                .map(|id| id.try_into().unwrap())
732                .unwrap_or(-1),
733        )?;
734        output.write_u8(self.run_pattern)?;
735        Ok(())
736    }
737}
738
739#[derive(Debug, Default, Clone, Copy)]
740pub struct WeaponInfo {
741    pub weapon_type: i16,
742    pub value: i16,
743}
744
745impl WeaponInfo {
746    pub fn read_from(mut input: impl Read) -> Result<Self> {
747        Ok(Self {
748            weapon_type: input.read_i16::<LE>()?,
749            value: input.read_i16::<LE>()?,
750        })
751    }
752    pub fn write_to(self, mut output: impl Write) -> Result<()> {
753        output.write_i16::<LE>(self.weapon_type)?;
754        output.write_i16::<LE>(self.value)?;
755        Ok(())
756    }
757}
758
759#[derive(Debug, Default, Clone)]
760pub struct BaseCombatUnitType {
761    superclass: ActionUnitType,
762    pub base_armor: u16,
763    pub weapons: Vec<WeaponInfo>,
764    pub armors: Vec<WeaponInfo>,
765    pub defense_terrain_bonus: Option<u16>,
766    pub weapon_range_max: f32,
767    pub area_effect_range: f32,
768    pub attack_speed: f32,
769    pub missile_id: Option<UnitTypeID>,
770    pub base_hit_chance: i16,
771    pub break_off_combat: i8,
772    pub frame_delay: i16,
773    pub weapon_offset: (f32, f32, f32),
774    pub blast_level_offense: i8,
775    pub weapon_range_min: f32,
776    pub missed_missile_spread: f32,
777    pub fight_sprite: Option<SpriteID>,
778    pub displayed_armor: i16,
779    pub displayed_attack: i16,
780    pub displayed_range: f32,
781    pub displayed_reload_time: f32,
782}
783
784impl BaseCombatUnitType {
785    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
786        let mut unit_type = Self {
787            superclass: ActionUnitType::read_from(&mut input, version)?,
788            ..Default::default()
789        };
790        unit_type.base_armor = if version < 11.52 {
791            input.read_u8()?.into()
792        } else {
793            input.read_u16::<LE>()?
794        };
795        let num_weapons = input.read_u16::<LE>()?;
796        for _ in 0..num_weapons {
797            unit_type.weapons.push(WeaponInfo::read_from(&mut input)?);
798        }
799        let num_armors = input.read_u16::<LE>()?;
800        for _ in 0..num_armors {
801            unit_type.armors.push(WeaponInfo::read_from(&mut input)?);
802        }
803        unit_type.defense_terrain_bonus = read_opt_u16(&mut input)?;
804        unit_type.weapon_range_max = input.read_f32::<LE>()?;
805        unit_type.area_effect_range = input.read_f32::<LE>()?;
806        unit_type.attack_speed = input.read_f32::<LE>()?;
807        unit_type.missile_id = read_opt_u16(&mut input)?;
808        unit_type.base_hit_chance = input.read_i16::<LE>()?;
809        unit_type.break_off_combat = input.read_i8()?;
810        unit_type.frame_delay = input.read_i16::<LE>()?;
811        unit_type.weapon_offset = (
812            input.read_f32::<LE>()?,
813            input.read_f32::<LE>()?,
814            input.read_f32::<LE>()?,
815        );
816        unit_type.blast_level_offense = input.read_i8()?;
817        unit_type.weapon_range_min = input.read_f32::<LE>()?;
818        unit_type.missed_missile_spread = input.read_f32::<LE>()?;
819        unit_type.fight_sprite = read_opt_u16(&mut input)?;
820        unit_type.displayed_armor = input.read_i16::<LE>()?;
821        unit_type.displayed_attack = input.read_i16::<LE>()?;
822        unit_type.displayed_range = input.read_f32::<LE>()?;
823        unit_type.displayed_reload_time = input.read_f32::<LE>()?;
824        Ok(unit_type)
825    }
826
827    /// Write this unit type to an output stream.
828    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
829        self.superclass.write_to(&mut output, version)?;
830        if version < 11.52 {
831            output.write_u8(self.base_armor.try_into().unwrap())?;
832        } else {
833            output.write_u16::<LE>(self.base_armor)?;
834        };
835        output.write_u16::<LE>(self.weapons.len() as u16)?;
836        for weapon in &self.weapons {
837            weapon.write_to(&mut output)?;
838        }
839        output.write_u16::<LE>(self.armors.len() as u16)?;
840        for armor in &self.armors {
841            armor.write_to(&mut output)?;
842        }
843        output.write_u16::<LE>(self.defense_terrain_bonus.unwrap_or(0xFFFF))?;
844        output.write_f32::<LE>(self.weapon_range_max)?;
845        output.write_f32::<LE>(self.area_effect_range)?;
846        output.write_f32::<LE>(self.attack_speed)?;
847        output.write_u16::<LE>(self.missile_id.map_into().unwrap_or(0xFFFF))?;
848        output.write_i16::<LE>(self.base_hit_chance)?;
849        output.write_i8(self.break_off_combat)?;
850        output.write_i16::<LE>(self.frame_delay)?;
851        output.write_f32::<LE>(self.weapon_offset.0)?;
852        output.write_f32::<LE>(self.weapon_offset.1)?;
853        output.write_f32::<LE>(self.weapon_offset.2)?;
854        output.write_i8(self.blast_level_offense)?;
855        output.write_f32::<LE>(self.weapon_range_min)?;
856        output.write_f32::<LE>(self.missed_missile_spread)?;
857        output.write_u16::<LE>(self.fight_sprite.map_into().unwrap_or(0xFFFF))?;
858        output.write_i16::<LE>(self.displayed_armor)?;
859        output.write_i16::<LE>(self.displayed_attack)?;
860        output.write_f32::<LE>(self.displayed_range)?;
861        output.write_f32::<LE>(self.displayed_reload_time)?;
862        Ok(())
863    }
864}
865
866#[derive(Debug, Default, Clone)]
867pub struct MissileUnitType {
868    superclass: BaseCombatUnitType,
869    pub missile_type: u8,
870    pub targetting_type: u8,
871    pub missile_hit_info: u8,
872    pub missile_die_info: u8,
873    pub area_effect_specials: u8,
874    pub ballistics_ratio: f32,
875}
876
877impl MissileUnitType {
878    /// Read this unit type from an input stream.
879    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
880        let mut unit_type = Self {
881            superclass: BaseCombatUnitType::read_from(&mut input, version)?,
882            ..Default::default()
883        };
884        unit_type.missile_type = input.read_u8()?;
885        unit_type.targetting_type = input.read_u8()?;
886        unit_type.missile_hit_info = input.read_u8()?;
887        unit_type.missile_die_info = input.read_u8()?;
888        unit_type.area_effect_specials = input.read_u8()?;
889        unit_type.ballistics_ratio = input.read_f32::<LE>()?;
890        Ok(unit_type)
891    }
892
893    /// Write this unit type to an output stream.
894    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
895        self.superclass.write_to(&mut output, version)?;
896        output.write_u8(self.missile_type)?;
897        output.write_u8(self.targetting_type)?;
898        output.write_u8(self.missile_hit_info)?;
899        output.write_u8(self.missile_die_info)?;
900        output.write_u8(self.area_effect_specials)?;
901        output.write_f32::<LE>(self.ballistics_ratio)?;
902        Ok(())
903    }
904}
905
906/// Resource cost for a unit.
907#[derive(Debug, Default, Clone, Copy)]
908pub struct AttributeCost {
909    /// The player attribute type to give/take.
910    pub attribute_type: i16,
911    /// The amount of that attribute that should be taken/given.
912    pub amount: i16,
913    /// Flag determining how and when this cost is counted.
914    ///
915    /// TODO make this an enum
916    pub flag: u8,
917}
918
919impl AttributeCost {
920    pub fn read_from(mut input: impl Read) -> Result<Self> {
921        let cost = Self {
922            attribute_type: input.read_i16::<LE>()?,
923            amount: input.read_i16::<LE>()?,
924            flag: input.read_u8()?,
925        };
926        let _padding = input.read_u8()?;
927        Ok(cost)
928    }
929    pub fn write_to(self, mut output: impl Write) -> Result<()> {
930        output.write_i16::<LE>(self.attribute_type)?;
931        output.write_i16::<LE>(self.amount)?;
932        output.write_u8(self.flag)?;
933        output.write_u8(0)?;
934        Ok(())
935    }
936}
937
938#[derive(Debug, Default, Clone)]
939pub struct CombatUnitType {
940    superclass: BaseCombatUnitType,
941    /// The costs of creating a unit of this type.
942    pub costs: ArrayVec<[AttributeCost; 3]>,
943    pub create_time: u16,
944    /// Unit type ID of the building or unit where this unit can be created.
945    pub create_at_building: Option<UnitTypeID>,
946    /// Button location index where the button to create this unit should be shown when a
947    /// `create_at_building` unit is selected.
948    pub create_button: i8,
949    pub rear_attack_modifier: f32,
950    pub flank_attack_modifier: f32,
951    /// Is this unit a hero unit?
952    ///
953    /// TODO what is special about hero units? Does it just opt into the healing behaviour?
954    pub hero_flag: u8,
955    pub garrison_sprite: Option<SpriteID>,
956    pub volley_fire_amount: f32,
957    pub max_attacks_in_volley: i8,
958    pub volley_spread: (f32, f32),
959    pub volley_start_spread_adjustment: f32,
960    pub volley_missile: Option<UnitTypeID>,
961    pub special_attack_sprite: Option<SpriteID>,
962    pub special_attack_flag: i8,
963    pub displayed_pierce_armor: i16,
964}
965
966impl CombatUnitType {
967    /// Read this unit type from an input stream.
968    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
969        let mut unit_type = Self {
970            superclass: BaseCombatUnitType::read_from(&mut input, version)?,
971            ..Default::default()
972        };
973
974        for _ in 0..3 {
975            let attr = AttributeCost::read_from(&mut input)?;
976            if attr.attribute_type >= 0 {
977                unit_type.costs.push(attr);
978            }
979        }
980        unit_type.create_time = input.read_u16::<LE>()?;
981        unit_type.create_at_building = read_opt_u16(&mut input)?;
982        unit_type.create_button = input.read_i8()?;
983        unit_type.rear_attack_modifier = input.read_f32::<LE>()?;
984        unit_type.flank_attack_modifier = input.read_f32::<LE>()?;
985        let _tribe_unit_type = input.read_u8()?;
986        unit_type.hero_flag = input.read_u8()?;
987        unit_type.garrison_sprite = {
988            let n = input.read_i32::<LE>()?;
989            if n < 0 {
990                None
991            } else {
992                Some(n.try_into().unwrap())
993            }
994        };
995        unit_type.volley_fire_amount = input.read_f32::<LE>()?;
996        unit_type.max_attacks_in_volley = input.read_i8()?;
997        unit_type.volley_spread = (input.read_f32::<LE>()?, input.read_f32::<LE>()?);
998        unit_type.volley_start_spread_adjustment = input.read_f32::<LE>()?;
999        unit_type.volley_missile = read_opt_u32(&mut input)?;
1000        unit_type.special_attack_sprite = read_opt_u32(&mut input)?;
1001        unit_type.special_attack_flag = input.read_i8()?;
1002        unit_type.displayed_pierce_armor = input.read_i16::<LE>()?;
1003
1004        Ok(unit_type)
1005    }
1006
1007    /// Write this unit type to an output stream.
1008    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
1009        self.superclass.write_to(&mut output, version)?;
1010        for i in 0..3 {
1011            match self.costs.get(i) {
1012                Some(cost) => cost.write_to(&mut output)?,
1013                None => AttributeCost {
1014                    attribute_type: -1,
1015                    amount: 0,
1016                    flag: 0,
1017                }
1018                .write_to(&mut output)?,
1019            }
1020        }
1021        output.write_u16::<LE>(self.create_time)?;
1022        output.write_u16::<LE>(self.create_at_building.map_into().unwrap_or(0xFFFF))?;
1023        output.write_i8(self.create_button)?;
1024        output.write_f32::<LE>(self.rear_attack_modifier)?;
1025        output.write_f32::<LE>(self.flank_attack_modifier)?;
1026        output.write_u8(0)?;
1027        output.write_u8(self.hero_flag)?;
1028        output.write_u32::<LE>(self.garrison_sprite.map_into().unwrap_or(0xFFFF_FFFF))?;
1029        output.write_f32::<LE>(self.volley_fire_amount)?;
1030        output.write_i8(self.max_attacks_in_volley)?;
1031        output.write_f32::<LE>(self.volley_spread.0)?;
1032        output.write_f32::<LE>(self.volley_spread.1)?;
1033        output.write_f32::<LE>(self.volley_start_spread_adjustment)?;
1034        output.write_u32::<LE>(self.volley_missile.map_into().unwrap_or(0xFFFF_FFFF))?;
1035        output.write_u32::<LE>(self.special_attack_sprite.map_into().unwrap_or(0xFFFF_FFFF))?;
1036        output.write_i8(self.special_attack_flag)?;
1037        output.write_i16::<LE>(self.displayed_pierce_armor)?;
1038        Ok(())
1039    }
1040}
1041
1042/// A linked, or "Annex" building. These allow for buildings made up of multiple pieces
1043/// with different behaviour, like the Town Centre with some walkable tiles and some non-walkable
1044/// tiles.
1045#[derive(Debug, Default, Clone)]
1046pub struct LinkedBuilding {
1047    /// Unit type ID for this linked building.
1048    pub unit_id: UnitTypeID,
1049    /// X offset in tiles from the centre of the "owner" building.
1050    pub x_offset: f32,
1051    /// Y offset in tiles from the centre of the "owner" building.
1052    pub y_offset: f32,
1053}
1054
1055impl LinkedBuilding {
1056    pub fn read_from(mut input: impl Read) -> Result<Self> {
1057        Ok(Self {
1058            unit_id: input.read_u16::<LE>()?.into(),
1059            x_offset: input.read_f32::<LE>()?,
1060            y_offset: input.read_f32::<LE>()?,
1061        })
1062    }
1063    pub fn write_to(&self, mut output: impl Write) -> Result<()> {
1064        output.write_u16::<LE>(self.unit_id.into())?;
1065        output.write_f32::<LE>(self.x_offset)?;
1066        output.write_f32::<LE>(self.y_offset)?;
1067        Ok(())
1068    }
1069    fn write_empty(mut output: impl Write) -> Result<()> {
1070        output.write_u16::<LE>(0xFFFF)?;
1071        output.write_f32::<LE>(0.0)?;
1072        output.write_f32::<LE>(0.0)?;
1073        Ok(())
1074    }
1075}
1076
1077/// Unit type class for buildings.
1078#[derive(Debug, Default, Clone)]
1079pub struct BuildingUnitType {
1080    superclass: CombatUnitType,
1081    /// Sprite to use during construction.
1082    pub construction_sprite: Option<SpriteID>,
1083    /// Sprite to use when this building is finished and built on snow.
1084    pub snow_sprite: Option<SpriteID>,
1085    /// TODO document
1086    pub connect_flag: u8,
1087    /// TODO document
1088    pub facet: i16,
1089    /// Whether the building should be immediately destroyed on completion.
1090    pub destroy_on_build: bool,
1091    /// Unit to spawn at the build site on completion.
1092    pub on_build_make_unit: Option<UnitTypeID>,
1093    /// Change the underlying terrain to this terrain ID on completion.
1094    pub on_build_make_tile: Option<TerrainID>,
1095    /// TODO document
1096    pub on_build_make_overlay: i16,
1097    /// Research this tech on completion.
1098    pub on_build_make_tech: Option<TechID>,
1099    /// Whether this building…can burn?
1100    ///
1101    /// TODO document the details
1102    pub can_burn: bool,
1103    pub linked_buildings: ArrayVec<[LinkedBuilding; 4]>,
1104    pub construction_unit: Option<UnitTypeID>,
1105    pub transform_unit: Option<UnitTypeID>,
1106    pub transform_sound: Option<SoundID>,
1107    pub construction_sound: Option<SoundID>,
1108    pub garrison_type: i8,
1109    pub garrison_heal_rate: f32,
1110    pub garrison_repair_rate: f32,
1111    pub salvage_unit: Option<UnitTypeID>,
1112    pub salvage_attributes: ArrayVec<[i8; 6]>,
1113}
1114
1115impl BuildingUnitType {
1116    /// Read this unit type from an input stream.
1117    pub fn read_from(mut input: impl Read, version: f32) -> Result<Self> {
1118        let mut unit_type = Self {
1119            superclass: CombatUnitType::read_from(&mut input, version)?,
1120            ..Default::default()
1121        };
1122        unit_type.construction_sprite = read_opt_u16(&mut input)?;
1123        unit_type.snow_sprite = if version < 11.53 {
1124            None
1125        } else {
1126            read_opt_u16(&mut input)?
1127        };
1128        unit_type.connect_flag = input.read_u8()?;
1129        unit_type.facet = input.read_i16::<LE>()?;
1130        unit_type.destroy_on_build = input.read_u8()? != 0;
1131        unit_type.on_build_make_unit = read_opt_u16(&mut input)?;
1132        unit_type.on_build_make_tile = read_opt_u16(&mut input)?;
1133        unit_type.on_build_make_overlay = input.read_i16::<LE>()?;
1134        unit_type.on_build_make_tech = read_opt_u16(&mut input)?;
1135        unit_type.can_burn = input.read_u8()? != 0;
1136        for _ in 0..unit_type.linked_buildings.capacity() {
1137            let link = LinkedBuilding::read_from(&mut input)?;
1138            if link.unit_id != 0xFFFF.into() {
1139                unit_type.linked_buildings.push(link);
1140            }
1141        }
1142
1143        unit_type.construction_unit = read_opt_u16(&mut input)?;
1144        unit_type.transform_unit = read_opt_u16(&mut input)?;
1145        unit_type.transform_sound = read_opt_u16(&mut input)?;
1146        unit_type.construction_sound = read_opt_u16(&mut input)?;
1147        unit_type.garrison_type = input.read_i8()?;
1148        unit_type.garrison_heal_rate = input.read_f32::<LE>()?;
1149        unit_type.garrison_repair_rate = input.read_f32::<LE>()?;
1150        unit_type.salvage_unit = read_opt_u16(&mut input)?;
1151        for _ in 0..unit_type.salvage_attributes.capacity() {
1152            let attr = input.read_i8()?;
1153            unit_type.salvage_attributes.push(attr);
1154        }
1155        Ok(unit_type)
1156    }
1157
1158    /// Write the unit type to an output stream.
1159    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
1160        self.superclass.write_to(&mut output, version)?;
1161        output.write_u16::<LE>(self.construction_sprite.map_into().unwrap_or(0xFFFF))?;
1162        if version >= 11.53 {
1163            output.write_u16::<LE>(self.snow_sprite.map_into().unwrap_or(0xFFFF))?;
1164        }
1165        output.write_u8(self.connect_flag)?;
1166        output.write_i16::<LE>(self.facet)?;
1167        output.write_u8(if self.destroy_on_build { 1 } else { 0 })?;
1168        output.write_u16::<LE>(self.on_build_make_unit.map_into().unwrap_or(0xFFFF))?;
1169        output.write_u16::<LE>(self.on_build_make_tile.map_into().unwrap_or(0xFFFF))?;
1170        output.write_i16::<LE>(self.on_build_make_overlay)?;
1171        output.write_u16::<LE>(self.on_build_make_tech.map_into().unwrap_or(0xFFFF))?;
1172        output.write_u8(if self.can_burn { 1 } else { 0 })?;
1173        for i in 0..self.linked_buildings.capacity() {
1174            match self.linked_buildings.get(i) {
1175                Some(link) => link.write_to(&mut output)?,
1176                None => LinkedBuilding::write_empty(&mut output)?,
1177            }
1178        }
1179        output.write_u16::<LE>(self.construction_unit.map_into().unwrap_or(0xFFFF))?;
1180        output.write_u16::<LE>(self.transform_unit.map_into().unwrap_or(0xFFFF))?;
1181        output.write_u16::<LE>(self.transform_sound.map_into().unwrap_or(0xFFFF))?;
1182        output.write_u16::<LE>(self.construction_sound.map_into().unwrap_or(0xFFFF))?;
1183        output.write_i8(self.garrison_type)?;
1184        output.write_f32::<LE>(self.garrison_heal_rate)?;
1185        output.write_f32::<LE>(self.garrison_repair_rate)?;
1186        output.write_u16::<LE>(self.salvage_unit.map_into().unwrap_or(0xFFFF))?;
1187        for attr in &self.salvage_attributes {
1188            output.write_i8(*attr)?;
1189        }
1190        Ok(())
1191    }
1192}
1193
1194fn write_opt_string_key(mut output: impl Write, opt_key: &Option<StringKey>) -> Result<()> {
1195    output.write_i16::<LE>(if let Some(key) = opt_key {
1196        key.try_into()
1197            .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?
1198    } else {
1199        -1
1200    })?;
1201    Ok(())
1202}