Skip to main content

genie_scx/
format.rs

1//! This module contains the data format reading/writing.
2
3#![allow(clippy::cognitive_complexity)]
4
5use crate::ai::AIInfo;
6use crate::bitmap::Bitmap;
7use crate::header::SCXHeader;
8use crate::map::Map;
9use crate::player::*;
10use crate::triggers::TriggerSystem;
11use crate::types::*;
12use crate::victory::*;
13use crate::{Error, Result, VersionBundle};
14use byteorder::{ReadBytesExt, WriteBytesExt, LE};
15use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
16use genie_support::{
17    f32_eq, read_opt_u32, read_str, write_opt_str, write_str, StringKey, UnitTypeID,
18};
19use std::convert::{TryFrom, TryInto};
20use std::io::{self, Read, Write};
21
22// pub enum LostInformation {
23//     DisabledTechs(i32, i32),
24//     DisabledUnits(i32, i32),
25//     DisabledBuildings(i32, i32),
26//     MapType,
27// }
28
29/// An object placed in the scenario.
30#[derive(Debug, Clone, Default)]
31pub struct ScenarioObject {
32    /// Position (x, y, z) of this object.
33    pub position: (f32, f32, f32),
34    /// This object's unique ID.
35    pub id: i32,
36    /// The type ID of this object.
37    pub object_type: UnitTypeID,
38    /// State value.
39    pub state: u8,
40    /// Radian angle this unit is facing.
41    pub angle: f32,
42    /// Current animation frame.
43    pub frame: i16,
44    /// ID of the object this object is garrisoned in, or -1 when not
45    /// garrisoned.
46    pub garrisoned_in: Option<i32>,
47}
48
49impl ScenarioObject {
50    pub fn read_from(mut input: impl Read, version: SCXVersion) -> Result<Self> {
51        let position = (
52            input.read_f32::<LE>()?,
53            input.read_f32::<LE>()?,
54            input.read_f32::<LE>()?,
55        );
56        let id = input.read_i32::<LE>()?;
57        let object_type = input.read_u16::<LE>()?.into();
58        let state = input.read_u8()?;
59        let angle = input.read_f32::<LE>()?;
60        let frame = if version < SCXVersion(*b"1.15") {
61            -1
62        } else {
63            input.read_i16::<LE>()?
64        };
65        let garrisoned_in = if version < SCXVersion(*b"1.13") {
66            None
67        } else {
68            Some(input.read_i32::<LE>()?)
69        }
70        .and_then(|id| match id {
71            -1 => None,
72            id => Some(id),
73        })
74        .and_then(|id| match id {
75            // 0 means -1 in "recent" versions
76            0 if version > SCXVersion(*b"1.12") => None,
77            id => Some(id),
78        });
79
80        Ok(Self {
81            position,
82            id,
83            object_type,
84            state,
85            angle,
86            frame,
87            garrisoned_in,
88        })
89    }
90
91    pub fn write_to(&self, mut output: impl Write, version: SCXVersion) -> Result<()> {
92        output.write_f32::<LE>(self.position.0)?;
93        output.write_f32::<LE>(self.position.1)?;
94        output.write_f32::<LE>(self.position.2)?;
95        output.write_i32::<LE>(self.id)?;
96        output.write_u16::<LE>(self.object_type.into())?;
97        output.write_u8(self.state)?;
98        output.write_f32::<LE>(self.angle)?;
99        if version > SCXVersion(*b"1.14") {
100            output.write_i16::<LE>(self.frame)?;
101        }
102        if version > SCXVersion(*b"1.12") {
103            match self.garrisoned_in {
104                Some(id) => output.write_i32::<LE>(id)?,
105                None => output.write_i32::<LE>(-1)?,
106            }
107        }
108        Ok(())
109    }
110}
111
112#[derive(Debug, Default, Clone)]
113pub(crate) struct RGEScen {
114    /// Data version.
115    pub(crate) version: f32,
116    /// Names for each player.
117    player_names: Vec<Option<String>>,
118    /// Name IDs for each player.
119    player_string_table: Vec<Option<StringKey>>,
120    player_base_properties: Vec<PlayerBaseProperties>,
121    victory_conquest: bool,
122    /// File name of this scenario.
123    pub(crate) name: String,
124    description_string_table: Option<StringKey>,
125    hints_string_table: Option<StringKey>,
126    win_message_string_table: Option<StringKey>,
127    loss_message_string_table: Option<StringKey>,
128    history_string_table: Option<StringKey>,
129    scout_string_table: Option<StringKey>,
130    description: Option<String>,
131    hints: Option<String>,
132    win_message: Option<String>,
133    loss_message: Option<String>,
134    history: Option<String>,
135    scout: Option<String>,
136    pregame_cinematic: Option<String>,
137    victory_cinematic: Option<String>,
138    loss_cinematic: Option<String>,
139    mission_bmp: Option<String>,
140    player_build_lists: Vec<Option<String>>,
141    player_city_plans: Vec<Option<String>>,
142    player_ai_rules: Vec<Option<String>>,
143    player_files: Vec<PlayerFiles>,
144    ai_rules_types: Vec<i8>,
145}
146
147impl RGEScen {
148    pub fn read_from(mut input: impl Read) -> Result<Self> {
149        let version = input.read_f32::<LE>()?;
150        log::debug!("RGEScen version {}", version);
151        let mut player_names = vec![None; 16];
152        if version > 1.13 {
153            for name in player_names.iter_mut() {
154                *name = read_str(&mut input, 256)?;
155            }
156        }
157
158        let mut player_string_table = vec![None; 16];
159        if version > 1.16 {
160            for string_id in player_string_table.iter_mut() {
161                *string_id = read_opt_u32(&mut input)?;
162            }
163        }
164
165        let mut player_base_properties = vec![PlayerBaseProperties::default(); 16];
166        if version > 1.13 {
167            for properties in player_base_properties.iter_mut() {
168                properties.active = input.read_i32::<LE>()?;
169                properties.player_type = input.read_i32::<LE>()?;
170                properties.civilization = input.read_i32::<LE>()?;
171                properties.posture = input.read_i32::<LE>()?;
172            }
173        }
174
175        let victory_conquest = if version >= 1.07 {
176            input.read_u8()? != 0
177        } else {
178            true
179        };
180
181        dbg!(victory_conquest);
182
183        {
184            let _timeline_count = input.read_i16::<LE>()?;
185            let _timeline_available = input.read_i16::<LE>()?;
186            let _old_time = input.read_f32::<LE>()?;
187            dbg!(_timeline_count, _timeline_available, _old_time);
188            assert_eq!(_timeline_count, 0, "Unexpected RGE_Timeline");
189            // assert_eq!(_timeline_available, 0, "Unexpected RGE_Timeline");
190        }
191
192        if version >= 1.28 {
193            let _civ_lock = &mut [0; 16];
194            input.read_u32_into::<LE>(_civ_lock)?;
195        }
196
197        let name_length = input.read_i16::<LE>()? as usize;
198        // File name may be empty for embedded scenario data inside recorded games.
199        let name = read_str(&mut input, name_length)?.unwrap_or_default();
200
201        let (
202            description_string_table,
203            hints_string_table,
204            win_message_string_table,
205            loss_message_string_table,
206            history_string_table,
207        ) = if version >= 1.16 {
208            (
209                read_opt_u32(&mut input)?,
210                read_opt_u32(&mut input)?,
211                read_opt_u32(&mut input)?,
212                read_opt_u32(&mut input)?,
213                read_opt_u32(&mut input)?,
214            )
215        } else {
216            Default::default()
217        };
218
219        let scout_string_table = if version >= 1.22 {
220            read_opt_u32(&mut input)?
221        } else {
222            Default::default()
223        };
224
225        let description_length = input.read_i16::<LE>()? as usize;
226        let description = read_str(&mut input, description_length)?;
227
228        let (hints, win_message, loss_message, history) = if version >= 1.11 {
229            let hints_length = input.read_i16::<LE>()? as usize;
230            let hints = read_str(&mut input, hints_length)?;
231            let win_message_length = input.read_i16::<LE>()? as usize;
232            let win_message = read_str(&mut input, win_message_length)?;
233            let loss_message_length = input.read_i16::<LE>()? as usize;
234            let loss_message = read_str(&mut input, loss_message_length)?;
235            let history_length = input.read_i16::<LE>()? as usize;
236            let history = read_str(&mut input, history_length)?;
237            (hints, win_message, loss_message, history)
238        } else {
239            (None, None, None, None)
240        };
241
242        let scout = if version >= 1.22 {
243            let scout_length = input.read_i16::<LE>()? as usize;
244            read_str(&mut input, scout_length)?
245        } else {
246            None
247        };
248
249        if version < 1.03 {
250            // skip some stuff
251        }
252
253        let len = input.read_i16::<LE>()? as usize;
254        let pregame_cinematic = read_str(&mut input, len)?;
255        let len = input.read_i16::<LE>()? as usize;
256        let victory_cinematic = read_str(&mut input, len)?;
257        let len = input.read_i16::<LE>()? as usize;
258        let loss_cinematic = read_str(&mut input, len)?;
259
260        let mission_bmp = if version >= 1.09 {
261            let len = input.read_i16::<LE>()? as usize;
262            read_str(&mut input, len)?
263        } else {
264            None
265        };
266
267        let _mission_picture = if version >= 1.10 {
268            Bitmap::read_from(&mut input)?
269        } else {
270            None
271        };
272
273        let mut player_build_lists = vec![None; 16];
274        for build_list in player_build_lists.iter_mut() {
275            let len = input.read_u16::<LE>()? as usize;
276            *build_list = read_str(&mut input, len)?;
277        }
278
279        let mut player_city_plans = vec![None; 16];
280        for city_plan in player_city_plans.iter_mut() {
281            let len = input.read_u16::<LE>()? as usize;
282            *city_plan = read_str(&mut input, len)?;
283        }
284
285        let mut player_ai_rules = vec![None; 16];
286        if version >= 1.08 {
287            for ai_rules in player_ai_rules.iter_mut() {
288                let len = input.read_u16::<LE>()? as usize;
289                *ai_rules = read_str(&mut input, len)?;
290            }
291        }
292
293        let mut player_files = vec![PlayerFiles::default(); 16];
294        for files in player_files.iter_mut() {
295            let build_list_length = input.read_i32::<LE>()? as usize;
296            let city_plan_length = input.read_i32::<LE>()? as usize;
297            let ai_rules_length = if version >= 1.08 {
298                input.read_i32::<LE>()? as usize
299            } else {
300                0
301            };
302
303            files.build_list = read_str(&mut input, build_list_length)?;
304            files.city_plan = read_str(&mut input, city_plan_length)?;
305            files.ai_rules = read_str(&mut input, ai_rules_length)?;
306        }
307
308        let mut ai_rules_types = vec![0; 16];
309        if version >= 1.20 {
310            for rule_type in ai_rules_types.iter_mut() {
311                *rule_type = input.read_i8()?;
312            }
313        }
314
315        if version >= 1.02 {
316            let sep = input.read_i32::<LE>()?;
317            debug_assert_eq!(sep, -99);
318        }
319
320        Ok(RGEScen {
321            version,
322            player_names,
323            player_string_table,
324            player_base_properties,
325            victory_conquest,
326            name,
327            description_string_table,
328            hints_string_table,
329            win_message_string_table,
330            loss_message_string_table,
331            history_string_table,
332            scout_string_table,
333            description,
334            hints,
335            win_message,
336            loss_message,
337            history,
338            scout,
339            pregame_cinematic,
340            victory_cinematic,
341            loss_cinematic,
342            mission_bmp,
343            player_build_lists,
344            player_city_plans,
345            player_ai_rules,
346            player_files,
347            ai_rules_types,
348        })
349    }
350
351    pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
352        output.write_f32::<LE>(version)?;
353
354        if version > 1.13 {
355            assert_eq!(self.player_names.len(), 16);
356            for name in &self.player_names {
357                let mut padded_bytes = Vec::with_capacity(256);
358                if let Some(ref name) = name {
359                    let name_bytes = name.as_bytes();
360                    padded_bytes.write_all(name_bytes)?;
361                }
362                padded_bytes.extend(vec![0; 256 - padded_bytes.len()]);
363                output.write_all(&padded_bytes)?;
364            }
365        }
366
367        if version > 1.16 {
368            assert_eq!(self.player_string_table.len(), 16);
369            for id in &self.player_string_table {
370                write_opt_string_key(&mut output, id)?;
371            }
372        }
373
374        if version > 1.13 {
375            assert_eq!(self.player_base_properties.len(), 16);
376            for props in &self.player_base_properties {
377                output.write_i32::<LE>(props.active)?;
378                output.write_i32::<LE>(props.player_type)?;
379                output.write_i32::<LE>(props.civilization)?;
380                output.write_i32::<LE>(props.posture)?;
381            }
382        }
383
384        if version >= 1.07 {
385            output.write_u8(if self.victory_conquest { 1 } else { 0 })?;
386        }
387
388        // RGE_Timeline
389        output.write_i16::<LE>(0)?;
390        output.write_i16::<LE>(0)?;
391        output.write_f32::<LE>(-1.0)?;
392
393        if version >= 1.28 {
394            // Civ Lock data
395            for _ in 0..16 {
396                output.write_u32::<LE>(0)?;
397            }
398        }
399
400        write_str(&mut output, &self.name)?;
401
402        if version >= 1.16 {
403            write_opt_string_key(&mut output, &self.description_string_table)?;
404            write_opt_string_key(&mut output, &self.hints_string_table)?;
405            write_opt_string_key(&mut output, &self.win_message_string_table)?;
406            write_opt_string_key(&mut output, &self.loss_message_string_table)?;
407            write_opt_string_key(&mut output, &self.history_string_table)?;
408        }
409        if version >= 1.22 {
410            write_opt_string_key(&mut output, &self.scout_string_table)?;
411        }
412
413        write_opt_str(&mut output, &self.description)?;
414        if version >= 1.11 {
415            write_opt_str(&mut output, &self.hints)?;
416            write_opt_str(&mut output, &self.win_message)?;
417            write_opt_str(&mut output, &self.loss_message)?;
418            write_opt_str(&mut output, &self.history)?;
419        }
420        if version >= 1.22 {
421            write_opt_str(&mut output, &self.scout)?;
422        }
423
424        write_opt_str(&mut output, &self.pregame_cinematic)?;
425        write_opt_str(&mut output, &self.victory_cinematic)?;
426        write_opt_str(&mut output, &self.loss_cinematic)?;
427        if version >= 1.09 {
428            // mission_bmp
429            write_opt_str(&mut output, &None)?;
430        }
431
432        if version >= 1.10 {
433            // mission_picture
434            output.write_u32::<LE>(0)?;
435            output.write_u32::<LE>(0)?;
436            output.write_u32::<LE>(0)?;
437            output.write_u16::<LE>(1)?;
438        }
439
440        assert_eq!(self.player_build_lists.len(), 16);
441        for build_list in &self.player_build_lists {
442            write_opt_str(&mut output, build_list)?;
443        }
444
445        assert_eq!(self.player_city_plans.len(), 16);
446        for city_plan in &self.player_city_plans {
447            write_opt_str(&mut output, city_plan)?;
448        }
449
450        if version >= 1.08 {
451            assert_eq!(self.player_ai_rules.len(), 16);
452            for ai_rules in &self.player_ai_rules {
453                write_opt_str(&mut output, ai_rules)?;
454            }
455        }
456
457        assert_eq!(self.player_files.len(), 16);
458        for files in &self.player_files {
459            if let Some(build_list) = &files.build_list {
460                output.write_u32::<LE>(build_list.len() as u32)?;
461            } else {
462                output.write_u32::<LE>(0)?;
463            }
464            if let Some(city_plan) = &files.city_plan {
465                output.write_u32::<LE>(city_plan.len() as u32)?;
466            } else {
467                output.write_u32::<LE>(0)?;
468            }
469            if version >= 1.08 {
470                if let Some(ai_rules) = &files.ai_rules {
471                    output.write_u32::<LE>(ai_rules.len() as u32)?;
472                } else {
473                    output.write_u32::<LE>(0)?;
474                }
475            }
476            if let Some(build_list) = &files.build_list {
477                output.write_all(build_list.as_bytes())?;
478            }
479            if let Some(city_plan) = &files.city_plan {
480                output.write_all(city_plan.as_bytes())?;
481            }
482            if version >= 1.08 {
483                if let Some(ai_rules) = &files.ai_rules {
484                    output.write_all(ai_rules.as_bytes())?;
485                }
486            }
487        }
488
489        if version >= 1.20 {
490            assert_eq!(self.ai_rules_types.len(), 16);
491            for ai_rules_type in &self.ai_rules_types {
492                output.write_i8(*ai_rules_type)?;
493            }
494        }
495
496        output.write_i32::<LE>(-99)?;
497
498        Ok(())
499    }
500}
501
502/// Embeddable scenario data. This includes all scenario settings, but not map data, triggers, and
503/// placed objects.
504///
505/// The game saves this structure in scenario files, and also in saved and recorded game files.
506#[derive(Debug, Default, Clone)]
507pub struct TribeScen {
508    /// "Engine" data.
509    ///
510    /// This distinction doesn't make much sense as a user of this library, but
511    /// it exists internally in AoC and affects the storage format (eg.  some
512    /// things are duplicate).
513    pub(crate) base: RGEScen,
514    /// Starting resources for players.
515    player_start_resources: Vec<PlayerStartResources>,
516    /// Victory settings.
517    victory: VictoryInfo,
518    /// Whether all victory conditions need to be met for victory to occur.
519    victory_all_flag: bool,
520    /// Type of victory condition to use in multiplayer games.
521    mp_victory_type: i32,
522    /// Required score to attain multiplayer victory.
523    victory_score: i32,
524    /// Time at which the highest-scoring player will win the multiplayer match.
525    victory_time: i32,
526    /// Initial diplomacy stances between players.
527    diplomacy: Vec<Vec<DiplomaticStance>>,
528    legacy_victory_info: Vec<Vec<LegacyVictoryInfo>>,
529    /// Whether Allied Victory is enabled for each player.
530    allied_victory: Vec<i32>,
531    teams_locked: bool,
532    can_change_teams: bool,
533    random_start_locations: bool,
534    max_teams: u8,
535    /// Number of disabled techs per player.
536    ///
537    /// TODO only use `disabled_techs` for this
538    num_disabled_techs: Vec<i32>,
539    /// Disabled tech IDs per player.
540    disabled_techs: Vec<Vec<i32>>,
541    /// Number of disabled units per player.
542    ///
543    /// TODO only use `disabled_units` for this
544    num_disabled_units: Vec<i32>,
545    /// Disabled unit IDs per player.
546    disabled_units: Vec<Vec<i32>>,
547    /// Number of disabled buildings per player.
548    ///
549    /// TODO only use `disabled_buildings` for this
550    num_disabled_buildings: Vec<i32>,
551    /// Disabled building IDs per player.
552    disabled_buildings: Vec<Vec<i32>>,
553    /// (What exactly?)
554    ///
555    /// According to [AoE2ScenarioParser][].
556    /// [AoE2ScenarioParser]: https://github.com/KSneijders/AoE2ScenarioParser/blob/8e3abd422164961aa5c7857350475088790804f8/AoE2ScenarioParser/pieces/options.py
557    combat_mode: i32,
558    /// (What exactly?)
559    ///
560    /// According to [AoE2ScenarioParser][].
561    /// [AoE2ScenarioParser]: https://github.com/KSneijders/AoE2ScenarioParser/blob/8e3abd422164961aa5c7857350475088790804f8/AoE2ScenarioParser/pieces/options.py
562    naval_mode: i32,
563    /// Whether "All Techs" is enabled.
564    all_techs: bool,
565    /// The starting age per player.
566    player_start_ages: Vec<StartingAge>,
567    /// The initial camera location.
568    view: (i32, i32),
569    /// The map type.
570    map_type: Option<i32>,
571    base_priorities: Vec<i8>,
572    /// The water definition type used. (DE2 and up)
573    water_definition: Option<String>,
574    /// The colour mood used. (DE2 and up)
575    color_mood: Option<String>,
576    /// Is collide-and-correct pathing enabled?
577    ///
578    /// Only supported for DE2 and up; defaults to `false` in earlier versions.
579    collide_and_correct: bool,
580    /// Is villager force drop enabled?
581    ///
582    /// Only supported for DE2 and up; defaults to `false` in earlier versions.
583    villager_force_drop: bool,
584}
585
586impl TribeScen {
587    #[deprecated = "Use TribeScen::read_from instead"]
588    #[doc(hidden)]
589    pub fn from(input: impl Read) -> Result<Self> {
590        Self::read_from(input)
591    }
592
593    /// Read scenario data from an input stream.
594    pub fn read_from(mut input: impl Read) -> Result<Self> {
595        let mut base = RGEScen::read_from(&mut input)?;
596        let version = base.version;
597
598        let mut player_start_resources = vec![PlayerStartResources::default(); 16];
599
600        // Moved to RGEScen in 1.13
601        if version <= 1.13 {
602            for name in base.player_names.iter_mut() {
603                *name = read_str(&mut input, 256)?;
604            }
605
606            for i in 0..16 {
607                let properties = &mut base.player_base_properties[i];
608                properties.active = input.read_i32::<LE>()?;
609                let resources = PlayerStartResources::read_from(&mut input, version)?;
610                properties.player_type = input.read_i32::<LE>()?;
611                properties.civilization = input.read_i32::<LE>()?;
612                properties.posture = input.read_i32::<LE>()?;
613                player_start_resources[i] = resources;
614            }
615        } else {
616            for resources in player_start_resources.iter_mut() {
617                *resources = PlayerStartResources::read_from(&mut input, version)?;
618            }
619        }
620
621        if version >= 1.02 {
622            let sep = input.read_i32::<LE>()?;
623            debug_assert_eq!(sep, -99);
624        }
625
626        let victory = VictoryInfo::read_from(&mut input)?;
627        let victory_all_flag = input.read_i32::<LE>()? != 0;
628
629        let mp_victory_type = if version >= 1.13 {
630            input.read_i32::<LE>()?
631        } else {
632            4
633        };
634        let victory_score = if version >= 1.13 {
635            input.read_i32::<LE>()?
636        } else {
637            900
638        };
639        let victory_time = if version >= 1.13 {
640            input.read_i32::<LE>()?
641        } else {
642            9000
643        };
644
645        log::debug!(
646            "Victory values: {} {} {}",
647            mp_victory_type,
648            victory_score,
649            victory_time
650        );
651
652        let mut diplomacy = vec![vec![DiplomaticStance::Neutral; 16]; 16];
653        for player_diplomacy in diplomacy.iter_mut() {
654            for stance in player_diplomacy.iter_mut() {
655                *stance = DiplomaticStance::try_from(input.read_i32::<LE>()?)?;
656            }
657        }
658
659        let mut legacy_victory_info = vec![vec![LegacyVictoryInfo::default(); 12]; 16];
660        for list in legacy_victory_info.iter_mut() {
661            for victory_info in list.iter_mut() {
662                *victory_info = LegacyVictoryInfo::read_from(&mut input)?;
663            }
664        }
665
666        if version >= 1.02 {
667            let sep = input.read_i32::<LE>()?;
668            debug_assert_eq!(sep, -99);
669        }
670
671        let mut allied_victory = vec![0i32; 16];
672        for setting in allied_victory.iter_mut() {
673            *setting = input.read_i32::<LE>()?;
674        }
675
676        let (teams_locked, can_change_teams, random_start_locations, max_teams) = if version >= 1.24
677        {
678            (
679                input.read_i8()? != 0,
680                input.read_i8()? != 0,
681                input.read_i8()? != 0,
682                input.read_u8()?,
683            )
684        } else if f32_eq!(version, 1.23) {
685            (input.read_i32::<LE>()? != 0, true, true, 4)
686        } else {
687            (false, true, true, 4)
688        };
689
690        let mut num_disabled_techs = vec![0; 16];
691        let mut disabled_techs = vec![vec![]; 16];
692        let mut num_disabled_units = vec![0; 16];
693        let mut disabled_units = vec![vec![]; 16];
694        let mut num_disabled_buildings = vec![0; 16];
695        let mut disabled_buildings = vec![vec![]; 16];
696
697        if version >= 1.28 {
698            // Definitive Edition only stores the exact number of disabled techs/units/buildings.
699            input.read_i32_into::<LE>(&mut num_disabled_techs)?;
700            for (player_disabled_techs, &num) in
701                disabled_techs.iter_mut().zip(num_disabled_techs.iter())
702            {
703                *player_disabled_techs = vec![0; num as usize];
704                input.read_i32_into::<LE>(player_disabled_techs)?;
705            }
706
707            input.read_i32_into::<LE>(&mut num_disabled_units)?;
708            for (player_disabled_units, &num) in
709                disabled_units.iter_mut().zip(num_disabled_units.iter())
710            {
711                *player_disabled_units = vec![0; num as usize];
712                input.read_i32_into::<LE>(player_disabled_units)?;
713            }
714
715            input.read_i32_into::<LE>(&mut num_disabled_buildings)?;
716            for (player_disabled_buildings, &num) in disabled_buildings
717                .iter_mut()
718                .zip(num_disabled_buildings.iter())
719            {
720                *player_disabled_buildings = vec![0; num as usize];
721                input.read_i32_into::<LE>(player_disabled_buildings)?;
722            }
723        } else if version >= 1.18 {
724            // AoC and friends store up to 20 or 30 of each.
725            input.read_i32_into::<LE>(&mut num_disabled_techs)?;
726            for player_disabled_techs in disabled_techs.iter_mut() {
727                *player_disabled_techs = vec![0; 30];
728                input.read_i32_into::<LE>(player_disabled_techs)?;
729            }
730
731            input.read_i32_into::<LE>(&mut num_disabled_units)?;
732            for player_disabled_units in disabled_units.iter_mut() {
733                *player_disabled_units = vec![0; 30];
734                input.read_i32_into::<LE>(player_disabled_units)?;
735            }
736
737            input.read_i32_into::<LE>(&mut num_disabled_buildings)?;
738            let max_disabled_buildings = if version >= 1.25 { 30 } else { 20 };
739            for player_disabled_buildings in disabled_buildings.iter_mut() {
740                *player_disabled_buildings = vec![0; max_disabled_buildings];
741                input.read_i32_into::<LE>(player_disabled_buildings)?;
742            }
743        } else if version > 1.03 {
744            // Old scenarios only allowed disabling up to 20 techs per player.
745            for i in 0..16 {
746                let player_disabled_techs = &mut disabled_techs[i];
747                *player_disabled_techs = vec![0; 20];
748                input.read_i32_into::<LE>(player_disabled_techs)?;
749                // The number of disabled techs wasn't stored either, so we need to guess it!
750                num_disabled_techs[i] = player_disabled_techs
751                    .iter()
752                    .position(|val| *val <= 0)
753                    .map(|index| (index as i32) + 1)
754                    .unwrap_or(0);
755            }
756        } else {
757            // <= 1.03 did not support disabling anything
758        }
759
760        let combat_mode = if version > 1.04 {
761            input.read_i32::<LE>()?
762        } else {
763            0
764        };
765        let (naval_mode, all_techs) = if version >= 1.12 {
766            (input.read_i32::<LE>()?, input.read_i32::<LE>()? != 0)
767        } else {
768            (0, false)
769        };
770
771        let mut player_start_ages = vec![StartingAge::Default; 16];
772        if version > 1.05 {
773            for start_age in player_start_ages.iter_mut() {
774                *start_age = StartingAge::try_from(input.read_i32::<LE>()?, version)?;
775            }
776        }
777
778        log::debug!("starting ages: {:?}", player_start_ages);
779
780        if version >= 1.02 {
781            let sep = input.read_i32::<LE>()?;
782            debug_assert_eq!(sep, -99);
783        }
784
785        let view = if version >= 1.19 {
786            (input.read_i32::<LE>()?, input.read_i32::<LE>()?)
787        } else {
788            (-1, -1)
789        };
790
791        let map_type = if version >= 1.21 {
792            match input.read_i32::<LE>()? {
793                // HD Edition uses -2 instead of -1?
794                -2 | -1 => None,
795                id => Some(
796                    id.try_into()
797                        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?,
798                ),
799            }
800        } else {
801            None
802        };
803
804        let mut base_priorities = vec![0; 16];
805        if version >= 1.24 {
806            input.read_i8_into(&mut base_priorities)?;
807        }
808
809        let mut water_definition = None;
810        let mut color_mood = None;
811        let mut collide_and_correct = false;
812        let mut villager_force_drop = false;
813
814        if version >= 1.35 {
815            // Duplicated here from TriggerSystem … we can discard it because the TriggerSystem
816            // will read the same number later.
817            let _trigger_count = input.read_u32::<LE>()?;
818        }
819        if version >= 1.30 {
820            let _str_signature = input.read_u16::<LE>()?;
821            water_definition = {
822                let len = input.read_u16::<LE>()?;
823                read_str(&mut input, len as usize)?
824            };
825        }
826
827        if version >= 1.32 {
828            let _str_signature = input.read_u16::<LE>()?;
829            color_mood = {
830                let len = input.read_u16::<LE>()?;
831                read_str(&mut input, len as usize)?
832            };
833        }
834        if version >= 1.36 {
835            collide_and_correct = input.read_u8()? != 0;
836        }
837        if version >= 1.37 {
838            villager_force_drop = input.read_u8()? != 0;
839        }
840
841        Ok(TribeScen {
842            base,
843            player_start_resources,
844            victory,
845            victory_all_flag,
846            mp_victory_type,
847            victory_score,
848            victory_time,
849            diplomacy,
850            legacy_victory_info,
851            allied_victory,
852            teams_locked,
853            can_change_teams,
854            random_start_locations,
855            max_teams,
856            num_disabled_techs,
857            disabled_techs,
858            num_disabled_units,
859            disabled_units,
860            num_disabled_buildings,
861            disabled_buildings,
862            combat_mode,
863            naval_mode,
864            all_techs,
865            player_start_ages,
866            view,
867            map_type,
868            base_priorities,
869            water_definition,
870            color_mood,
871            collide_and_correct,
872            villager_force_drop,
873        })
874    }
875
876    /// Write scenario data to an output stream.
877    pub fn write_to(&self, mut output: impl Write, version: f32, num_triggers: u32) -> Result<()> {
878        self.base.write_to(&mut output, version)?;
879
880        if version <= 1.13 {
881            assert_eq!(self.base.player_names.len(), 16);
882            for name in &self.base.player_names {
883                let mut padded_bytes = Vec::with_capacity(256);
884                if let Some(ref name) = name {
885                    let name_bytes = name.as_bytes();
886                    padded_bytes.write_all(name_bytes)?;
887                }
888                padded_bytes.extend(vec![0; 256 - padded_bytes.len()]);
889                output.write_all(&padded_bytes)?;
890            }
891
892            assert_eq!(self.base.player_base_properties.len(), 16);
893            assert_eq!(self.player_start_resources.len(), 16);
894            for i in 0..16 {
895                let properties = &self.base.player_base_properties[i];
896                let resources = &self.player_start_resources[i];
897                output.write_i32::<LE>(properties.active)?;
898                resources.write_to(&mut output, version)?;
899                output.write_i32::<LE>(properties.player_type)?;
900                output.write_i32::<LE>(properties.civilization)?;
901                output.write_i32::<LE>(properties.posture)?;
902            }
903        } else {
904            assert_eq!(self.player_start_resources.len(), 16);
905            for start_resources in &self.player_start_resources {
906                start_resources.write_to(&mut output, version)?;
907            }
908        }
909
910        if version >= 1.02 {
911            output.write_i32::<LE>(-99)?;
912        }
913
914        self.victory.write_to(&mut output)?;
915        output.write_i32::<LE>(if self.victory_all_flag { 1 } else { 0 })?;
916
917        if version >= 1.13 {
918            output.write_i32::<LE>(self.mp_victory_type)?;
919            output.write_i32::<LE>(self.victory_score)?;
920            output.write_i32::<LE>(self.victory_time)?;
921        }
922
923        assert_eq!(self.diplomacy.len(), 16);
924        for player_diplomacy in &self.diplomacy {
925            assert_eq!(player_diplomacy.len(), 16);
926            for stance in player_diplomacy {
927                output.write_i32::<LE>((*stance).into())?;
928            }
929        }
930
931        assert_eq!(self.legacy_victory_info.len(), 16);
932        for list in &self.legacy_victory_info {
933            for entry in list {
934                entry.write_to(&mut output)?;
935            }
936        }
937
938        if version >= 1.02 {
939            output.write_i32::<LE>(-99)?;
940        }
941
942        for value in &self.allied_victory {
943            output.write_i32::<LE>(*value)?;
944        }
945
946        if version >= 1.24 {
947            output.write_i8(if self.teams_locked { 1 } else { 0 })?;
948            output.write_i8(if self.can_change_teams { 1 } else { 0 })?;
949            output.write_i8(if self.random_start_locations { 1 } else { 0 })?;
950            output.write_u8(self.max_teams)?;
951        } else if f32_eq!(version, 1.23) {
952            output.write_i32::<LE>(if self.teams_locked { 1 } else { 0 })?;
953        }
954
955        if version >= 1.28 {
956            for num in &self.num_disabled_techs {
957                output.write_i32::<LE>(*num)?;
958            }
959            for (player_disabled_techs, &num) in self
960                .disabled_techs
961                .iter()
962                .zip(self.num_disabled_techs.iter())
963            {
964                for i in 0..num as usize {
965                    output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
966                }
967            }
968
969            for num in &self.num_disabled_units {
970                output.write_i32::<LE>(*num)?;
971            }
972            for (player_disabled_units, &num) in self
973                .disabled_units
974                .iter()
975                .zip(self.num_disabled_units.iter())
976            {
977                for i in 0..num as usize {
978                    output.write_i32::<LE>(*player_disabled_units.get(i).unwrap_or(&-1))?;
979                }
980            }
981
982            for num in &self.num_disabled_buildings {
983                output.write_i32::<LE>(*num)?;
984            }
985            for (player_disabled_buildings, &num) in self
986                .disabled_buildings
987                .iter()
988                .zip(self.num_disabled_buildings.iter())
989            {
990                for i in 0..num as usize {
991                    output.write_i32::<LE>(*player_disabled_buildings.get(i).unwrap_or(&-1))?;
992                }
993            }
994        } else if version >= 1.18 {
995            let max_disabled_buildings = if version >= 1.25 { 30 } else { 20 };
996            let most = *self.num_disabled_buildings.iter().max().unwrap_or(&0);
997            if most > max_disabled_buildings {
998                return Err(Error::TooManyDisabledBuildingsError(
999                    most,
1000                    max_disabled_buildings,
1001                ));
1002            }
1003
1004            for num in &self.num_disabled_techs {
1005                output.write_i32::<LE>(*num)?;
1006            }
1007            for player_disabled_techs in &self.disabled_techs {
1008                for i in 0..30 {
1009                    output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
1010                }
1011            }
1012
1013            for num in &self.num_disabled_units {
1014                output.write_i32::<LE>(*num)?;
1015            }
1016            for player_disabled_units in &self.disabled_units {
1017                for i in 0..30 {
1018                    output.write_i32::<LE>(*player_disabled_units.get(i).unwrap_or(&-1))?;
1019                }
1020            }
1021
1022            for num in &self.num_disabled_buildings {
1023                output.write_i32::<LE>(*num)?;
1024            }
1025            for player_disabled_buildings in &self.disabled_buildings {
1026                for i in 0..max_disabled_buildings as usize {
1027                    output.write_i32::<LE>(*player_disabled_buildings.get(i).unwrap_or(&-1))?;
1028                }
1029            }
1030        } else if version > 1.03 {
1031            let most = *self.num_disabled_techs.iter().max().unwrap_or(&0);
1032            if most > 20 {
1033                return Err(Error::TooManyDisabledTechsError(most));
1034            }
1035            if self.num_disabled_units.iter().any(|&n| n > 0) {
1036                return Err(Error::CannotDisableUnitsError);
1037            }
1038            if self.num_disabled_buildings.iter().any(|&n| n > 0) {
1039                return Err(Error::CannotDisableBuildingsError);
1040            }
1041
1042            // Old scenarios only allowed disabling up to 20 techs per player.
1043            for player_disabled_techs in &self.disabled_techs {
1044                for i in 0..20 {
1045                    output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
1046                }
1047            }
1048        } else {
1049            // <= 1.03 did not support disabling anything
1050            if self.num_disabled_techs.iter().any(|&n| n > 0) {
1051                return Err(Error::CannotDisableTechsError);
1052            }
1053            if self.num_disabled_units.iter().any(|&n| n > 0) {
1054                return Err(Error::CannotDisableUnitsError);
1055            }
1056            if self.num_disabled_buildings.iter().any(|&n| n > 0) {
1057                return Err(Error::CannotDisableBuildingsError);
1058            }
1059        }
1060
1061        if version > 1.04 {
1062            output.write_i32::<LE>(0)?;
1063        }
1064        if version >= 1.12 {
1065            output.write_i32::<LE>(0)?;
1066            output.write_i32::<LE>(if self.all_techs { 1 } else { 0 })?;
1067        }
1068
1069        if version > 1.05 {
1070            for start_age in &self.player_start_ages {
1071                output.write_i32::<LE>(start_age.to_i32(version))?;
1072            }
1073        }
1074
1075        if version >= 1.02 {
1076            output.write_i32::<LE>(-99)?;
1077        }
1078
1079        if version >= 1.19 {
1080            output.write_i32::<LE>(self.view.0)?;
1081            output.write_i32::<LE>(self.view.1)?;
1082        }
1083
1084        if version >= 1.21 {
1085            output.write_i32::<LE>(self.map_type.unwrap_or(-1))?;
1086        }
1087
1088        if version >= 1.24 {
1089            assert_eq!(self.base_priorities.len(), 16);
1090            for priority in &self.base_priorities {
1091                output.write_i8(*priority)?;
1092            }
1093        }
1094
1095        if version >= 1.28 {
1096            output.write_u32::<LE>(num_triggers)?;
1097            output.write_u16::<LE>(0)?;
1098            write_opt_str(&mut output, &self.water_definition)?;
1099        }
1100
1101        if version >= 1.36 {
1102            output.write_u8(0)?;
1103            output.write_u8(0)?;
1104            write_opt_str(&mut output, &self.color_mood)?;
1105            output.write_u8(if self.collide_and_correct { 1 } else { 0 })?;
1106        }
1107        if version >= 1.37 {
1108            output.write_u8(if self.villager_force_drop { 1 } else { 0 })?;
1109        }
1110
1111        Ok(())
1112    }
1113
1114    pub fn version(&self) -> f32 {
1115        self.base.version
1116    }
1117
1118    pub fn description(&self) -> Option<&str> {
1119        // Convert String to &str: https://stackoverflow.com/a/31234028
1120        self.base.description.as_ref().map(|s| &**s)
1121    }
1122}
1123
1124#[derive(Debug, Clone)]
1125pub struct SCXFormat {
1126    /// Version of the SCX format.
1127    pub(crate) version: SCXVersion,
1128    /// Uncompressed header containing metadata for display.
1129    pub(crate) header: SCXHeader,
1130    /// ID for the next-placed/created object.
1131    pub(crate) next_object_id: i32,
1132    /// Scenario data.
1133    pub(crate) tribe_scen: TribeScen,
1134    /// Map data.
1135    pub(crate) map: Map,
1136    /// Player data.
1137    world_players: Vec<WorldPlayerData>,
1138    /// Objects data.
1139    pub(crate) player_objects: Vec<Vec<ScenarioObject>>,
1140    /// Player data.
1141    scenario_players: Vec<ScenarioPlayerData>,
1142    /// Triggers (only in AoK and up).
1143    pub(crate) triggers: Option<TriggerSystem>,
1144    /// AI information (AoK and up).
1145    ai_info: Option<AIInfo>,
1146}
1147
1148impl SCXFormat {
1149    /// Extract version bundle information from a parsed SCX file.
1150    pub fn version(&self) -> VersionBundle {
1151        VersionBundle {
1152            format: self.version,
1153            header: self.header.version,
1154            data: self.tribe_scen.version(),
1155            triggers: self.triggers.as_ref().map(|triggers| triggers.version()),
1156            map: self.map.version(),
1157            ..VersionBundle::aoc()
1158        }
1159    }
1160
1161    fn load_inner(version: SCXVersion, player_version: f32, mut input: impl Read) -> Result<Self> {
1162        let header = SCXHeader::read_from(&mut input, version)?;
1163
1164        let mut input = DeflateDecoder::new(&mut input);
1165        let next_object_id = input.read_i32::<LE>()?;
1166
1167        let tribe_scen = TribeScen::read_from(&mut input)?;
1168
1169        let map = Map::read_from(&mut input)?;
1170
1171        let num_players = input.read_u32::<LE>()?;
1172        log::debug!("number of players: {}", num_players);
1173        let mut world_players = Vec::with_capacity(num_players as usize);
1174        for _ in 1..num_players {
1175            world_players.push(WorldPlayerData::read_from(&mut input, player_version)?);
1176        }
1177
1178        fn read_scenario_players(
1179            mut input: impl Read,
1180            player_version: f32,
1181        ) -> Result<Vec<ScenarioPlayerData>> {
1182            let num = input.read_u32::<LE>()?;
1183            let mut players = Vec::with_capacity(num as usize);
1184            log::debug!("number of scenario players: {}", num);
1185            for _ in 1..num {
1186                players.push(ScenarioPlayerData::read_from(&mut input, player_version)?);
1187            }
1188            Ok(players)
1189        }
1190
1191        fn read_player_objects(
1192            mut input: impl Read,
1193            num_players: u32,
1194            version: SCXVersion,
1195        ) -> Result<Vec<Vec<ScenarioObject>>> {
1196            let mut player_objects = Vec::with_capacity(num_players as usize);
1197            for _ in 0..num_players {
1198                let num_objects = input.read_u32::<LE>()?;
1199                let mut objects = Vec::with_capacity(num_objects as usize);
1200                log::debug!("number of objects: {}", num_objects);
1201                for _ in 0..num_objects {
1202                    objects.push(ScenarioObject::read_from(&mut input, version)?);
1203                }
1204                player_objects.push(objects);
1205            }
1206            Ok(player_objects)
1207        }
1208
1209        // The order is flipped … thanks DE
1210        let (scenario_players, player_objects) = if version >= SCXVersion(*b"1.36") {
1211            let players = read_scenario_players(&mut input, player_version)?;
1212            let objects = read_player_objects(&mut input, num_players, version)?;
1213            (players, objects)
1214        } else {
1215            let objects = read_player_objects(&mut input, num_players, version)?;
1216            let players = read_scenario_players(&mut input, player_version)?;
1217            (players, objects)
1218        };
1219
1220        let triggers = if version < SCXVersion(*b"1.14") {
1221            None
1222        } else {
1223            Some(TriggerSystem::read_from(&mut input)?)
1224        };
1225
1226        let ai_info = if version > SCXVersion(*b"1.17") && version < SCXVersion(*b"2.00") {
1227            AIInfo::read_from(&mut input)?
1228        } else {
1229            None
1230        };
1231
1232        Ok(SCXFormat {
1233            version,
1234            header,
1235            next_object_id,
1236            tribe_scen,
1237            map,
1238            world_players,
1239            player_objects,
1240            scenario_players,
1241            triggers,
1242            ai_info,
1243        })
1244    }
1245
1246    pub fn load_scenario(mut input: impl Read) -> Result<Self> {
1247        let mut format_version = [0; 4];
1248        input.read_exact(&mut format_version)?;
1249        let format_version = SCXVersion(format_version);
1250        if let Some(player_version) = format_version.to_player_version() {
1251            Self::load_inner(format_version, player_version, input)
1252        } else {
1253            Err(Error::UnsupportedFormatVersionError(format_version))
1254        }
1255    }
1256
1257    fn write_player_objects(
1258        &self,
1259        mut output: impl Write,
1260        format_version: SCXVersion,
1261    ) -> Result<()> {
1262        for objects in &self.player_objects {
1263            output.write_i32::<LE>(objects.len() as i32)?;
1264            for object in objects {
1265                object.write_to(&mut output, format_version)?;
1266            }
1267        }
1268        Ok(())
1269    }
1270
1271    fn write_scenario_players(
1272        &self,
1273        mut output: impl Write,
1274        player_version: f32,
1275        victory_version: f32,
1276    ) -> Result<()> {
1277        output.write_i32::<LE>(self.scenario_players.len() as i32 + 1)?;
1278        for player in &self.scenario_players {
1279            player.write_to(&mut output, player_version, victory_version)?;
1280        }
1281        Ok(())
1282    }
1283
1284    pub fn write_to(&self, mut output: impl Write, version: &VersionBundle) -> Result<()> {
1285        let player_version = match version.format.to_player_version() {
1286            Some(v) => v,
1287            None => return Err(Error::UnsupportedFormatVersionError(version.format)),
1288        };
1289
1290        output.write_all(version.format.as_bytes())?;
1291        self.header
1292            .write_to(&mut output, version.format, version.header)?;
1293
1294        let mut output = DeflateEncoder::new(output, Compression::default());
1295        output.write_i32::<LE>(self.next_object_id)?;
1296
1297        let num_triggers = self
1298            .triggers
1299            .as_ref()
1300            .map(|trigger_system| trigger_system.num_triggers())
1301            .unwrap_or(0);
1302        self.tribe_scen
1303            .write_to(&mut output, version.data, num_triggers)?;
1304        self.map.write_to(&mut output, version.map)?;
1305
1306        output.write_i32::<LE>(self.player_objects.len() as i32)?;
1307        for player in &self.world_players {
1308            player.write_to(&mut output, player_version)?;
1309        }
1310
1311        if version.format >= SCXVersion(*b"1.36") {
1312            self.write_scenario_players(&mut output, player_version, version.victory)?;
1313            self.write_player_objects(&mut output, version.format)?;
1314        } else {
1315            self.write_player_objects(&mut output, version.format)?;
1316            self.write_scenario_players(&mut output, player_version, version.victory)?;
1317        }
1318
1319        if version.format > SCXVersion(*b"1.13") {
1320            let def = TriggerSystem::default();
1321            let triggers = match self.triggers {
1322                Some(ref tr) => tr,
1323                None => &def,
1324            };
1325            triggers.write_to(&mut output, version.triggers.unwrap_or(1.6))?;
1326        }
1327
1328        if version.format > SCXVersion(*b"1.17") && version.format < SCXVersion(*b"2.00") {
1329            let def = AIInfo::default();
1330            let ai_info = match self.ai_info {
1331                Some(ref ai) => ai,
1332                None => &def,
1333            };
1334            ai_info.write_to(&mut output)?;
1335        }
1336
1337        output.finish()?;
1338
1339        Ok(())
1340    }
1341
1342    /// Get the name of the UserPatch mod that was used to create this scenario, if applicable.
1343    ///
1344    /// Returns None if no mod was used.
1345    pub fn mod_name(&self) -> Option<&str> {
1346        self.tribe_scen.base.player_names[9]
1347            .as_ref()
1348            .map(|string| string.as_str())
1349    }
1350
1351    /// Hash the scenario, for comparison with other instances.
1352    ///
1353    /// This is only available in tests and the implementation is horrifying :)
1354    #[cfg(test)]
1355    pub fn hash(&self) -> u64 {
1356        use std::{
1357            collections::hash_map::DefaultHasher,
1358            hash::{Hash, Hasher},
1359        };
1360        let mut hasher = DefaultHasher::new();
1361        format!("{:#?}", self).hash(&mut hasher);
1362        hasher.finish()
1363    }
1364}
1365
1366fn write_opt_string_key(mut output: impl Write, opt_key: &Option<StringKey>) -> Result<()> {
1367    output.write_u32::<LE>(if let Some(key) = opt_key {
1368        key.try_into()
1369            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?
1370    } else {
1371        0xFFFF_FFFF
1372    })?;
1373    Ok(())
1374}
1375
1376#[cfg(test)]
1377mod tests {
1378    use super::SCXFormat;
1379    use crate::{Result, VersionBundle};
1380    use std::fs::File;
1381    use std::io::{Cursor, ErrorKind, Read};
1382
1383    fn save_and_load(format: &SCXFormat, as_version: VersionBundle) -> Result<SCXFormat> {
1384        let mut out = vec![];
1385        format.write_to(&mut out, &as_version)?;
1386
1387        let mut f = Cursor::new(out);
1388        let scx = SCXFormat::load_scenario(&mut f)?;
1389        assert_consumed(f);
1390        Ok(scx)
1391    }
1392
1393    fn assert_consumed(mut input: impl Read) {
1394        let byte = &mut [0];
1395        match input.read_exact(byte) {
1396            Err(err) if err.kind() == ErrorKind::UnexpectedEof => (),
1397            Err(err) => panic!("{}", err),
1398            Ok(_) => {
1399                let mut trailing_data = vec![byte[0]];
1400                input.read_to_end(&mut trailing_data).unwrap();
1401                panic!("data left in buffer ({}): {:?}", trailing_data.len(), {
1402                    trailing_data.truncate(32);
1403                    trailing_data
1404                });
1405            }
1406        }
1407    }
1408
1409    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=42
1410    #[test]
1411    fn oldest_aoe1_scn_on_aoeheaven() {
1412        let mut f = File::open("test/scenarios/ The Destruction of Rome.scn").unwrap();
1413        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1414        assert_consumed(f);
1415        let mut out = vec![];
1416        format
1417            .write_to(&mut out, &format.version())
1418            .expect("failed to write");
1419    }
1420
1421    #[test]
1422    fn aoe1_beta_scn_reserialize() {
1423        let mut f = File::open("test/scenarios/Dawn of a New Age.scn").unwrap();
1424        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1425        assert_consumed(f);
1426        let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
1427
1428        assert_eq!(
1429            format.hash(),
1430            format2.hash(),
1431            "should produce exactly the same scenario"
1432        );
1433    }
1434
1435    #[test]
1436    fn aoe1_beta_scn_to_aoc() {
1437        let mut f = File::open("test/scenarios/Dawn of a New Age.scn").unwrap();
1438        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1439        assert_consumed(f);
1440        let format2 = save_and_load(&format, VersionBundle::aoc()).expect("save-and-load failed");
1441
1442        assert_eq!(
1443            format2.version(),
1444            VersionBundle::aoc(),
1445            "should have converted to AoC versions"
1446        );
1447    }
1448
1449    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=1678
1450    #[test]
1451    fn aoe1_trial_scn() {
1452        let mut f = File::open("test/scenarios/Bronze Age Art of War.scn").unwrap();
1453        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1454        assert_consumed(f);
1455        let mut out = vec![];
1456        format
1457            .write_to(&mut out, &format.version())
1458            .expect("failed to write");
1459    }
1460
1461    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=2409
1462    #[test]
1463    fn aoe1_ppc_trial_scn() {
1464        let mut f = File::open("test/scenarios/CEASAR.scn").unwrap();
1465        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1466        assert_consumed(f);
1467        let mut out = vec![];
1468        format
1469            .write_to(&mut out, &format.version())
1470            .expect("failed to write");
1471    }
1472
1473    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=1651
1474    #[test]
1475    fn aoe1_scn() {
1476        let mut f = File::open("test/scenarios/A New Emporer.scn").unwrap();
1477        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1478        assert_consumed(f);
1479        let mut out = vec![];
1480        format
1481            .write_to(&mut out, &format.version())
1482            .expect("failed to write");
1483    }
1484
1485    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=880
1486    #[test]
1487    fn aoe1_ror_scx() {
1488        let mut f = File::open("test/scenarios/Jeremiah Johnson (Update).scx").unwrap();
1489        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1490        assert_consumed(f);
1491        let mut out = vec![];
1492        format
1493            .write_to(&mut out, &format.version())
1494            .expect("failed to write");
1495    }
1496
1497    #[test]
1498    fn aoe1_ror_to_aoc() -> Result<()> {
1499        let mut f = File::open("test/scenarios/El advenimiento de los hunos_.scx")?;
1500        let format = SCXFormat::load_scenario(&mut f)?;
1501        assert_consumed(f);
1502        let format2 = save_and_load(&format, VersionBundle::aoc())?;
1503
1504        assert_eq!(
1505            format2.version(),
1506            VersionBundle::aoc(),
1507            "should have converted to AoC versions"
1508        );
1509
1510        Ok(())
1511    }
1512
1513    /// Source: http://aok.heavengames.com/blacksmith/showfile.php?fileid=1271
1514    #[test]
1515    fn oldest_aok_scn_on_aokheaven() {
1516        let mut f = File::open("test/scenarios/CAMELOT.SCN").unwrap();
1517        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1518        assert_consumed(f);
1519        let mut out = vec![];
1520        format
1521            .write_to(&mut out, &format.version())
1522            .expect("failed to write");
1523    }
1524
1525    #[test]
1526    fn aoc_scx() {
1527        let mut f = File::open("test/scenarios/Age of Heroes b1-3-5.scx").unwrap();
1528        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1529        assert_consumed(f);
1530        let mut out = vec![];
1531        format
1532            .write_to(&mut out, &format.version())
1533            .expect("failed to write");
1534    }
1535
1536    #[test]
1537    fn hd_aoe2scenario() {
1538        let mut f = File::open("test/scenarios/Year_of_the_Pig.aoe2scenario").unwrap();
1539        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1540        assert_consumed(f);
1541        let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
1542
1543        assert_eq!(
1544            format.hash(),
1545            format2.hash(),
1546            "should produce exactly the same scenario"
1547        );
1548    }
1549
1550    #[test]
1551    fn hd_scx2() {
1552        let mut f = File::open("test/scenarios/real_world_amazon.scx").unwrap();
1553        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1554        assert_consumed(f);
1555        let mut out = vec![];
1556        format
1557            .write_to(&mut out, &format.version())
1558            .expect("failed to write");
1559    }
1560
1561    /// A Definitive Edition scenario.
1562    ///
1563    /// (Ignored because it doesn't work yet.)
1564    /// Source: http://aoe.heavengames.com/dl-php/showfile.php?fileid=2708
1565    #[test]
1566    #[ignore]
1567    fn aoe_de_scn() {
1568        let mut f = File::open("test/scenarios/Corlis.aoescn").unwrap();
1569        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1570        assert_consumed(f);
1571        let mut out = vec![];
1572        format
1573            .write_to(&mut out, &format.version())
1574            .expect("failed to write");
1575    }
1576
1577    /// A Definitive Edition 2 scenario, SCX format version 1.36.
1578    ///
1579    /// Source: https://www.ageofempires.com/mods/details/2015/
1580    #[test]
1581    fn aoe_de2_1_36() {
1582        let mut f = File::open("test/scenarios/Hotkey Trainer Buildings.aoe2scenario").unwrap();
1583        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1584        assert_consumed(f);
1585        let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
1586        assert_eq!(
1587            format.hash(),
1588            format2.hash(),
1589            "should produce exactly the same scenario"
1590        );
1591    }
1592
1593    /// A Definitive Edition 2 scenario, based on the included AIImprovementsBucket10Test file,
1594    /// saved as a 1.37 format version with some layered terrain in the corners.
1595    #[test]
1596    fn aoe_de2_1_37() {
1597        let mut f = File::open("test/scenarios/layertest.aoe2scenario").unwrap();
1598        let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
1599        assert_consumed(f);
1600        let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
1601        assert_eq!(
1602            format.hash(),
1603            format2.hash(),
1604            "should produce exactly the same scenario"
1605        );
1606    }
1607}