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 Base(Box<BaseUnitType>),
18 Tree(Box<TreeUnitType>),
20 Animated(Box<AnimatedUnitType>),
22 Doppleganger(Box<DopplegangerUnitType>),
25 Moving(Box<MovingUnitType>),
27 Action(Box<ActionUnitType>),
29 BaseCombat(Box<BaseCombatUnitType>),
31 Missile(Box<MissileUnitType>),
33 Combat(Box<CombatUnitType>),
35 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
71inherit_unit_type!(AnimatedUnitType, BaseUnitType);
73inherit_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 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 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 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 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 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 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 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 pub fn write_to(&self, mut output: impl Write, _version: f32) -> Result<()> {
419 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Default, Clone, Copy)]
908pub struct AttributeCost {
909 pub attribute_type: i16,
911 pub amount: i16,
913 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 pub costs: ArrayVec<[AttributeCost; 3]>,
943 pub create_time: u16,
944 pub create_at_building: Option<UnitTypeID>,
946 pub create_button: i8,
949 pub rear_attack_modifier: f32,
950 pub flank_attack_modifier: f32,
951 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 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 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#[derive(Debug, Default, Clone)]
1046pub struct LinkedBuilding {
1047 pub unit_id: UnitTypeID,
1049 pub x_offset: f32,
1051 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#[derive(Debug, Default, Clone)]
1079pub struct BuildingUnitType {
1080 superclass: CombatUnitType,
1081 pub construction_sprite: Option<SpriteID>,
1083 pub snow_sprite: Option<SpriteID>,
1085 pub connect_flag: u8,
1087 pub facet: i16,
1089 pub destroy_on_build: bool,
1091 pub on_build_make_unit: Option<UnitTypeID>,
1093 pub on_build_make_tile: Option<TerrainID>,
1095 pub on_build_make_overlay: i16,
1097 pub on_build_make_tech: Option<TechID>,
1099 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 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 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}