#![allow(clippy::cognitive_complexity)]
use crate::ai::AIInfo;
use crate::bitmap::Bitmap;
use crate::header::SCXHeader;
use crate::map::Map;
use crate::player::*;
use crate::triggers::TriggerSystem;
use crate::types::*;
use crate::victory::*;
use crate::{Error, Result, VersionBundle};
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
use genie_support::{
f32_eq, read_opt_u32, read_str, write_opt_str, write_str, StringKey, UnitTypeID,
};
use std::convert::{TryFrom, TryInto};
use std::io::{self, Read, Write};
#[derive(Debug, Clone, Default)]
pub struct ScenarioObject {
pub position: (f32, f32, f32),
pub id: i32,
pub object_type: UnitTypeID,
pub state: u8,
pub angle: f32,
pub frame: i16,
pub garrisoned_in: Option<i32>,
}
impl ScenarioObject {
pub fn read_from(mut input: impl Read, version: SCXVersion) -> Result<Self> {
let position = (
input.read_f32::<LE>()?,
input.read_f32::<LE>()?,
input.read_f32::<LE>()?,
);
let id = input.read_i32::<LE>()?;
let object_type = input.read_u16::<LE>()?.into();
let state = input.read_u8()?;
let angle = input.read_f32::<LE>()?;
let frame = if version < SCXVersion(*b"1.15") {
-1
} else {
input.read_i16::<LE>()?
};
let garrisoned_in = if version < SCXVersion(*b"1.13") {
None
} else {
Some(input.read_i32::<LE>()?)
}
.and_then(|id| match id {
-1 => None,
id => Some(id),
})
.and_then(|id| match id {
0 if version > SCXVersion(*b"1.12") => None,
id => Some(id),
});
Ok(Self {
position,
id,
object_type,
state,
angle,
frame,
garrisoned_in,
})
}
pub fn write_to(&self, mut output: impl Write, version: SCXVersion) -> Result<()> {
output.write_f32::<LE>(self.position.0)?;
output.write_f32::<LE>(self.position.1)?;
output.write_f32::<LE>(self.position.2)?;
output.write_i32::<LE>(self.id)?;
output.write_u16::<LE>(self.object_type.into())?;
output.write_u8(self.state)?;
output.write_f32::<LE>(self.angle)?;
if version > SCXVersion(*b"1.14") {
output.write_i16::<LE>(self.frame)?;
}
if version > SCXVersion(*b"1.12") {
match self.garrisoned_in {
Some(id) => output.write_i32::<LE>(id)?,
None => output.write_i32::<LE>(-1)?,
}
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct RGEScen {
pub(crate) version: f32,
player_names: Vec<Option<String>>,
player_string_table: Vec<Option<StringKey>>,
player_base_properties: Vec<PlayerBaseProperties>,
victory_conquest: bool,
pub(crate) name: String,
description_string_table: Option<StringKey>,
hints_string_table: Option<StringKey>,
win_message_string_table: Option<StringKey>,
loss_message_string_table: Option<StringKey>,
history_string_table: Option<StringKey>,
scout_string_table: Option<StringKey>,
description: Option<String>,
hints: Option<String>,
win_message: Option<String>,
loss_message: Option<String>,
history: Option<String>,
scout: Option<String>,
pregame_cinematic: Option<String>,
victory_cinematic: Option<String>,
loss_cinematic: Option<String>,
mission_bmp: Option<String>,
player_build_lists: Vec<Option<String>>,
player_city_plans: Vec<Option<String>>,
player_ai_rules: Vec<Option<String>>,
player_files: Vec<PlayerFiles>,
ai_rules_types: Vec<i8>,
}
impl RGEScen {
pub fn read_from(mut input: impl Read) -> Result<Self> {
let version = input.read_f32::<LE>()?;
log::debug!("RGEScen version {}", version);
let mut player_names = vec![None; 16];
if version > 1.13 {
for name in player_names.iter_mut() {
*name = read_str(&mut input, 256)?;
}
}
let mut player_string_table = vec![None; 16];
if version > 1.16 {
for string_id in player_string_table.iter_mut() {
*string_id = read_opt_u32(&mut input)?;
}
}
let mut player_base_properties = vec![PlayerBaseProperties::default(); 16];
if version > 1.13 {
for properties in player_base_properties.iter_mut() {
properties.active = input.read_i32::<LE>()?;
properties.player_type = input.read_i32::<LE>()?;
properties.civilization = input.read_i32::<LE>()?;
properties.posture = input.read_i32::<LE>()?;
}
}
let victory_conquest = if version >= 1.07 {
input.read_u8()? != 0
} else {
true
};
dbg!(victory_conquest);
{
let _timeline_count = input.read_i16::<LE>()?;
let _timeline_available = input.read_i16::<LE>()?;
let _old_time = input.read_f32::<LE>()?;
dbg!(_timeline_count, _timeline_available, _old_time);
assert_eq!(_timeline_count, 0, "Unexpected RGE_Timeline");
}
if version >= 1.28 {
let _civ_lock = &mut [0; 16];
input.read_u32_into::<LE>(_civ_lock)?;
}
let name_length = input.read_i16::<LE>()? as usize;
let name = read_str(&mut input, name_length)?.unwrap_or_default();
let (
description_string_table,
hints_string_table,
win_message_string_table,
loss_message_string_table,
history_string_table,
) = if version >= 1.16 {
(
read_opt_u32(&mut input)?,
read_opt_u32(&mut input)?,
read_opt_u32(&mut input)?,
read_opt_u32(&mut input)?,
read_opt_u32(&mut input)?,
)
} else {
Default::default()
};
let scout_string_table = if version >= 1.22 {
read_opt_u32(&mut input)?
} else {
Default::default()
};
let description_length = input.read_i16::<LE>()? as usize;
let description = read_str(&mut input, description_length)?;
let (hints, win_message, loss_message, history) = if version >= 1.11 {
let hints_length = input.read_i16::<LE>()? as usize;
let hints = read_str(&mut input, hints_length)?;
let win_message_length = input.read_i16::<LE>()? as usize;
let win_message = read_str(&mut input, win_message_length)?;
let loss_message_length = input.read_i16::<LE>()? as usize;
let loss_message = read_str(&mut input, loss_message_length)?;
let history_length = input.read_i16::<LE>()? as usize;
let history = read_str(&mut input, history_length)?;
(hints, win_message, loss_message, history)
} else {
(None, None, None, None)
};
let scout = if version >= 1.22 {
let scout_length = input.read_i16::<LE>()? as usize;
read_str(&mut input, scout_length)?
} else {
None
};
if version < 1.03 {
}
let len = input.read_i16::<LE>()? as usize;
let pregame_cinematic = read_str(&mut input, len)?;
let len = input.read_i16::<LE>()? as usize;
let victory_cinematic = read_str(&mut input, len)?;
let len = input.read_i16::<LE>()? as usize;
let loss_cinematic = read_str(&mut input, len)?;
let mission_bmp = if version >= 1.09 {
let len = input.read_i16::<LE>()? as usize;
read_str(&mut input, len)?
} else {
None
};
let _mission_picture = if version >= 1.10 {
Bitmap::read_from(&mut input)?
} else {
None
};
let mut player_build_lists = vec![None; 16];
for build_list in player_build_lists.iter_mut() {
let len = input.read_u16::<LE>()? as usize;
*build_list = read_str(&mut input, len)?;
}
let mut player_city_plans = vec![None; 16];
for city_plan in player_city_plans.iter_mut() {
let len = input.read_u16::<LE>()? as usize;
*city_plan = read_str(&mut input, len)?;
}
let mut player_ai_rules = vec![None; 16];
if version >= 1.08 {
for ai_rules in player_ai_rules.iter_mut() {
let len = input.read_u16::<LE>()? as usize;
*ai_rules = read_str(&mut input, len)?;
}
}
let mut player_files = vec![PlayerFiles::default(); 16];
for files in player_files.iter_mut() {
let build_list_length = input.read_i32::<LE>()? as usize;
let city_plan_length = input.read_i32::<LE>()? as usize;
let ai_rules_length = if version >= 1.08 {
input.read_i32::<LE>()? as usize
} else {
0
};
files.build_list = read_str(&mut input, build_list_length)?;
files.city_plan = read_str(&mut input, city_plan_length)?;
files.ai_rules = read_str(&mut input, ai_rules_length)?;
}
let mut ai_rules_types = vec![0; 16];
if version >= 1.20 {
for rule_type in ai_rules_types.iter_mut() {
*rule_type = input.read_i8()?;
}
}
if version >= 1.02 {
let sep = input.read_i32::<LE>()?;
debug_assert_eq!(sep, -99);
}
Ok(RGEScen {
version,
player_names,
player_string_table,
player_base_properties,
victory_conquest,
name,
description_string_table,
hints_string_table,
win_message_string_table,
loss_message_string_table,
history_string_table,
scout_string_table,
description,
hints,
win_message,
loss_message,
history,
scout,
pregame_cinematic,
victory_cinematic,
loss_cinematic,
mission_bmp,
player_build_lists,
player_city_plans,
player_ai_rules,
player_files,
ai_rules_types,
})
}
pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> {
output.write_f32::<LE>(version)?;
if version > 1.13 {
assert_eq!(self.player_names.len(), 16);
for name in &self.player_names {
let mut padded_bytes = Vec::with_capacity(256);
if let Some(ref name) = name {
let name_bytes = name.as_bytes();
padded_bytes.write_all(name_bytes)?;
}
padded_bytes.extend(vec![0; 256 - padded_bytes.len()]);
output.write_all(&padded_bytes)?;
}
}
if version > 1.16 {
assert_eq!(self.player_string_table.len(), 16);
for id in &self.player_string_table {
write_opt_string_key(&mut output, id)?;
}
}
if version > 1.13 {
assert_eq!(self.player_base_properties.len(), 16);
for props in &self.player_base_properties {
output.write_i32::<LE>(props.active)?;
output.write_i32::<LE>(props.player_type)?;
output.write_i32::<LE>(props.civilization)?;
output.write_i32::<LE>(props.posture)?;
}
}
if version >= 1.07 {
output.write_u8(if self.victory_conquest { 1 } else { 0 })?;
}
output.write_i16::<LE>(0)?;
output.write_i16::<LE>(0)?;
output.write_f32::<LE>(-1.0)?;
if version >= 1.28 {
for _ in 0..16 {
output.write_u32::<LE>(0)?;
}
}
write_str(&mut output, &self.name)?;
if version >= 1.16 {
write_opt_string_key(&mut output, &self.description_string_table)?;
write_opt_string_key(&mut output, &self.hints_string_table)?;
write_opt_string_key(&mut output, &self.win_message_string_table)?;
write_opt_string_key(&mut output, &self.loss_message_string_table)?;
write_opt_string_key(&mut output, &self.history_string_table)?;
}
if version >= 1.22 {
write_opt_string_key(&mut output, &self.scout_string_table)?;
}
write_opt_str(&mut output, &self.description)?;
if version >= 1.11 {
write_opt_str(&mut output, &self.hints)?;
write_opt_str(&mut output, &self.win_message)?;
write_opt_str(&mut output, &self.loss_message)?;
write_opt_str(&mut output, &self.history)?;
}
if version >= 1.22 {
write_opt_str(&mut output, &self.scout)?;
}
write_opt_str(&mut output, &self.pregame_cinematic)?;
write_opt_str(&mut output, &self.victory_cinematic)?;
write_opt_str(&mut output, &self.loss_cinematic)?;
if version >= 1.09 {
write_opt_str(&mut output, &None)?;
}
if version >= 1.10 {
output.write_u32::<LE>(0)?;
output.write_u32::<LE>(0)?;
output.write_u32::<LE>(0)?;
output.write_u16::<LE>(1)?;
}
assert_eq!(self.player_build_lists.len(), 16);
for build_list in &self.player_build_lists {
write_opt_str(&mut output, build_list)?;
}
assert_eq!(self.player_city_plans.len(), 16);
for city_plan in &self.player_city_plans {
write_opt_str(&mut output, city_plan)?;
}
if version >= 1.08 {
assert_eq!(self.player_ai_rules.len(), 16);
for ai_rules in &self.player_ai_rules {
write_opt_str(&mut output, ai_rules)?;
}
}
assert_eq!(self.player_files.len(), 16);
for files in &self.player_files {
if let Some(build_list) = &files.build_list {
output.write_u32::<LE>(build_list.len() as u32)?;
} else {
output.write_u32::<LE>(0)?;
}
if let Some(city_plan) = &files.city_plan {
output.write_u32::<LE>(city_plan.len() as u32)?;
} else {
output.write_u32::<LE>(0)?;
}
if version >= 1.08 {
if let Some(ai_rules) = &files.ai_rules {
output.write_u32::<LE>(ai_rules.len() as u32)?;
} else {
output.write_u32::<LE>(0)?;
}
}
if let Some(build_list) = &files.build_list {
output.write_all(build_list.as_bytes())?;
}
if let Some(city_plan) = &files.city_plan {
output.write_all(city_plan.as_bytes())?;
}
if version >= 1.08 {
if let Some(ai_rules) = &files.ai_rules {
output.write_all(ai_rules.as_bytes())?;
}
}
}
if version >= 1.20 {
assert_eq!(self.ai_rules_types.len(), 16);
for ai_rules_type in &self.ai_rules_types {
output.write_i8(*ai_rules_type)?;
}
}
output.write_i32::<LE>(-99)?;
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct TribeScen {
pub(crate) base: RGEScen,
player_start_resources: Vec<PlayerStartResources>,
victory: VictoryInfo,
victory_all_flag: bool,
mp_victory_type: i32,
victory_score: i32,
victory_time: i32,
diplomacy: Vec<Vec<DiplomaticStance>>,
legacy_victory_info: Vec<Vec<LegacyVictoryInfo>>,
allied_victory: Vec<i32>,
teams_locked: bool,
can_change_teams: bool,
random_start_locations: bool,
max_teams: u8,
num_disabled_techs: Vec<i32>,
disabled_techs: Vec<Vec<i32>>,
num_disabled_units: Vec<i32>,
disabled_units: Vec<Vec<i32>>,
num_disabled_buildings: Vec<i32>,
disabled_buildings: Vec<Vec<i32>>,
combat_mode: i32,
naval_mode: i32,
all_techs: bool,
player_start_ages: Vec<StartingAge>,
view: (i32, i32),
map_type: Option<i32>,
base_priorities: Vec<i8>,
water_definition: Option<String>,
color_mood: Option<String>,
collide_and_correct: bool,
villager_force_drop: bool,
}
impl TribeScen {
#[deprecated = "Use TribeScen::read_from instead"]
#[doc(hidden)]
pub fn from(input: impl Read) -> Result<Self> {
Self::read_from(input)
}
pub fn read_from(mut input: impl Read) -> Result<Self> {
let mut base = RGEScen::read_from(&mut input)?;
let version = base.version;
let mut player_start_resources = vec![PlayerStartResources::default(); 16];
if version <= 1.13 {
for name in base.player_names.iter_mut() {
*name = read_str(&mut input, 256)?;
}
for i in 0..16 {
let properties = &mut base.player_base_properties[i];
properties.active = input.read_i32::<LE>()?;
let resources = PlayerStartResources::read_from(&mut input, version)?;
properties.player_type = input.read_i32::<LE>()?;
properties.civilization = input.read_i32::<LE>()?;
properties.posture = input.read_i32::<LE>()?;
player_start_resources[i] = resources;
}
} else {
for resources in player_start_resources.iter_mut() {
*resources = PlayerStartResources::read_from(&mut input, version)?;
}
}
if version >= 1.02 {
let sep = input.read_i32::<LE>()?;
debug_assert_eq!(sep, -99);
}
let victory = VictoryInfo::read_from(&mut input)?;
let victory_all_flag = input.read_i32::<LE>()? != 0;
let mp_victory_type = if version >= 1.13 {
input.read_i32::<LE>()?
} else {
4
};
let victory_score = if version >= 1.13 {
input.read_i32::<LE>()?
} else {
900
};
let victory_time = if version >= 1.13 {
input.read_i32::<LE>()?
} else {
9000
};
log::debug!(
"Victory values: {} {} {}",
mp_victory_type,
victory_score,
victory_time
);
let mut diplomacy = vec![vec![DiplomaticStance::Neutral; 16]; 16];
for player_diplomacy in diplomacy.iter_mut() {
for stance in player_diplomacy.iter_mut() {
*stance = DiplomaticStance::try_from(input.read_i32::<LE>()?)?;
}
}
let mut legacy_victory_info = vec![vec![LegacyVictoryInfo::default(); 12]; 16];
for list in legacy_victory_info.iter_mut() {
for victory_info in list.iter_mut() {
*victory_info = LegacyVictoryInfo::read_from(&mut input)?;
}
}
if version >= 1.02 {
let sep = input.read_i32::<LE>()?;
debug_assert_eq!(sep, -99);
}
let mut allied_victory = vec![0i32; 16];
for setting in allied_victory.iter_mut() {
*setting = input.read_i32::<LE>()?;
}
let (teams_locked, can_change_teams, random_start_locations, max_teams) = if version >= 1.24
{
(
input.read_i8()? != 0,
input.read_i8()? != 0,
input.read_i8()? != 0,
input.read_u8()?,
)
} else if f32_eq!(version, 1.23) {
(input.read_i32::<LE>()? != 0, true, true, 4)
} else {
(false, true, true, 4)
};
let mut num_disabled_techs = vec![0; 16];
let mut disabled_techs = vec![vec![]; 16];
let mut num_disabled_units = vec![0; 16];
let mut disabled_units = vec![vec![]; 16];
let mut num_disabled_buildings = vec![0; 16];
let mut disabled_buildings = vec![vec![]; 16];
if version >= 1.28 {
input.read_i32_into::<LE>(&mut num_disabled_techs)?;
for (player_disabled_techs, &num) in
disabled_techs.iter_mut().zip(num_disabled_techs.iter())
{
*player_disabled_techs = vec![0; num as usize];
input.read_i32_into::<LE>(player_disabled_techs)?;
}
input.read_i32_into::<LE>(&mut num_disabled_units)?;
for (player_disabled_units, &num) in
disabled_units.iter_mut().zip(num_disabled_units.iter())
{
*player_disabled_units = vec![0; num as usize];
input.read_i32_into::<LE>(player_disabled_units)?;
}
input.read_i32_into::<LE>(&mut num_disabled_buildings)?;
for (player_disabled_buildings, &num) in disabled_buildings
.iter_mut()
.zip(num_disabled_buildings.iter())
{
*player_disabled_buildings = vec![0; num as usize];
input.read_i32_into::<LE>(player_disabled_buildings)?;
}
} else if version >= 1.18 {
input.read_i32_into::<LE>(&mut num_disabled_techs)?;
for player_disabled_techs in disabled_techs.iter_mut() {
*player_disabled_techs = vec![0; 30];
input.read_i32_into::<LE>(player_disabled_techs)?;
}
input.read_i32_into::<LE>(&mut num_disabled_units)?;
for player_disabled_units in disabled_units.iter_mut() {
*player_disabled_units = vec![0; 30];
input.read_i32_into::<LE>(player_disabled_units)?;
}
input.read_i32_into::<LE>(&mut num_disabled_buildings)?;
let max_disabled_buildings = if version >= 1.25 { 30 } else { 20 };
for player_disabled_buildings in disabled_buildings.iter_mut() {
*player_disabled_buildings = vec![0; max_disabled_buildings];
input.read_i32_into::<LE>(player_disabled_buildings)?;
}
} else if version > 1.03 {
for i in 0..16 {
let player_disabled_techs = &mut disabled_techs[i];
*player_disabled_techs = vec![0; 20];
input.read_i32_into::<LE>(player_disabled_techs)?;
num_disabled_techs[i] = player_disabled_techs
.iter()
.position(|val| *val <= 0)
.map(|index| (index as i32) + 1)
.unwrap_or(0);
}
} else {
}
let combat_mode = if version > 1.04 {
input.read_i32::<LE>()?
} else {
0
};
let (naval_mode, all_techs) = if version >= 1.12 {
(input.read_i32::<LE>()?, input.read_i32::<LE>()? != 0)
} else {
(0, false)
};
let mut player_start_ages = vec![StartingAge::Default; 16];
if version > 1.05 {
for start_age in player_start_ages.iter_mut() {
*start_age = StartingAge::try_from(input.read_i32::<LE>()?, version)?;
}
}
log::debug!("starting ages: {:?}", player_start_ages);
if version >= 1.02 {
let sep = input.read_i32::<LE>()?;
debug_assert_eq!(sep, -99);
}
let view = if version >= 1.19 {
(input.read_i32::<LE>()?, input.read_i32::<LE>()?)
} else {
(-1, -1)
};
let map_type = if version >= 1.21 {
match input.read_i32::<LE>()? {
-2 | -1 => None,
id => Some(
id.try_into()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?,
),
}
} else {
None
};
let mut base_priorities = vec![0; 16];
if version >= 1.24 {
input.read_i8_into(&mut base_priorities)?;
}
let mut water_definition = None;
let mut color_mood = None;
let mut collide_and_correct = false;
let mut villager_force_drop = false;
if version >= 1.35 {
let _trigger_count = input.read_u32::<LE>()?;
}
if version >= 1.30 {
let _str_signature = input.read_u16::<LE>()?;
water_definition = {
let len = input.read_u16::<LE>()?;
read_str(&mut input, len as usize)?
};
}
if version >= 1.32 {
let _str_signature = input.read_u16::<LE>()?;
color_mood = {
let len = input.read_u16::<LE>()?;
read_str(&mut input, len as usize)?
};
}
if version >= 1.36 {
collide_and_correct = input.read_u8()? != 0;
}
if version >= 1.37 {
villager_force_drop = input.read_u8()? != 0;
}
Ok(TribeScen {
base,
player_start_resources,
victory,
victory_all_flag,
mp_victory_type,
victory_score,
victory_time,
diplomacy,
legacy_victory_info,
allied_victory,
teams_locked,
can_change_teams,
random_start_locations,
max_teams,
num_disabled_techs,
disabled_techs,
num_disabled_units,
disabled_units,
num_disabled_buildings,
disabled_buildings,
combat_mode,
naval_mode,
all_techs,
player_start_ages,
view,
map_type,
base_priorities,
water_definition,
color_mood,
collide_and_correct,
villager_force_drop,
})
}
pub fn write_to(&self, mut output: impl Write, version: f32, num_triggers: u32) -> Result<()> {
self.base.write_to(&mut output, version)?;
if version <= 1.13 {
assert_eq!(self.base.player_names.len(), 16);
for name in &self.base.player_names {
let mut padded_bytes = Vec::with_capacity(256);
if let Some(ref name) = name {
let name_bytes = name.as_bytes();
padded_bytes.write_all(name_bytes)?;
}
padded_bytes.extend(vec![0; 256 - padded_bytes.len()]);
output.write_all(&padded_bytes)?;
}
assert_eq!(self.base.player_base_properties.len(), 16);
assert_eq!(self.player_start_resources.len(), 16);
for i in 0..16 {
let properties = &self.base.player_base_properties[i];
let resources = &self.player_start_resources[i];
output.write_i32::<LE>(properties.active)?;
resources.write_to(&mut output, version)?;
output.write_i32::<LE>(properties.player_type)?;
output.write_i32::<LE>(properties.civilization)?;
output.write_i32::<LE>(properties.posture)?;
}
} else {
assert_eq!(self.player_start_resources.len(), 16);
for start_resources in &self.player_start_resources {
start_resources.write_to(&mut output, version)?;
}
}
if version >= 1.02 {
output.write_i32::<LE>(-99)?;
}
self.victory.write_to(&mut output)?;
output.write_i32::<LE>(if self.victory_all_flag { 1 } else { 0 })?;
if version >= 1.13 {
output.write_i32::<LE>(self.mp_victory_type)?;
output.write_i32::<LE>(self.victory_score)?;
output.write_i32::<LE>(self.victory_time)?;
}
assert_eq!(self.diplomacy.len(), 16);
for player_diplomacy in &self.diplomacy {
assert_eq!(player_diplomacy.len(), 16);
for stance in player_diplomacy {
output.write_i32::<LE>((*stance).into())?;
}
}
assert_eq!(self.legacy_victory_info.len(), 16);
for list in &self.legacy_victory_info {
for entry in list {
entry.write_to(&mut output)?;
}
}
if version >= 1.02 {
output.write_i32::<LE>(-99)?;
}
for value in &self.allied_victory {
output.write_i32::<LE>(*value)?;
}
if version >= 1.24 {
output.write_i8(if self.teams_locked { 1 } else { 0 })?;
output.write_i8(if self.can_change_teams { 1 } else { 0 })?;
output.write_i8(if self.random_start_locations { 1 } else { 0 })?;
output.write_u8(self.max_teams)?;
} else if f32_eq!(version, 1.23) {
output.write_i32::<LE>(if self.teams_locked { 1 } else { 0 })?;
}
if version >= 1.28 {
for num in &self.num_disabled_techs {
output.write_i32::<LE>(*num)?;
}
for (player_disabled_techs, &num) in self
.disabled_techs
.iter()
.zip(self.num_disabled_techs.iter())
{
for i in 0..num as usize {
output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
}
}
for num in &self.num_disabled_units {
output.write_i32::<LE>(*num)?;
}
for (player_disabled_units, &num) in self
.disabled_units
.iter()
.zip(self.num_disabled_units.iter())
{
for i in 0..num as usize {
output.write_i32::<LE>(*player_disabled_units.get(i).unwrap_or(&-1))?;
}
}
for num in &self.num_disabled_buildings {
output.write_i32::<LE>(*num)?;
}
for (player_disabled_buildings, &num) in self
.disabled_buildings
.iter()
.zip(self.num_disabled_buildings.iter())
{
for i in 0..num as usize {
output.write_i32::<LE>(*player_disabled_buildings.get(i).unwrap_or(&-1))?;
}
}
} else if version >= 1.18 {
let max_disabled_buildings = if version >= 1.25 { 30 } else { 20 };
let most = *self.num_disabled_buildings.iter().max().unwrap_or(&0);
if most > max_disabled_buildings {
return Err(Error::TooManyDisabledBuildingsError(
most,
max_disabled_buildings,
));
}
for num in &self.num_disabled_techs {
output.write_i32::<LE>(*num)?;
}
for player_disabled_techs in &self.disabled_techs {
for i in 0..30 {
output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
}
}
for num in &self.num_disabled_units {
output.write_i32::<LE>(*num)?;
}
for player_disabled_units in &self.disabled_units {
for i in 0..30 {
output.write_i32::<LE>(*player_disabled_units.get(i).unwrap_or(&-1))?;
}
}
for num in &self.num_disabled_buildings {
output.write_i32::<LE>(*num)?;
}
for player_disabled_buildings in &self.disabled_buildings {
for i in 0..max_disabled_buildings as usize {
output.write_i32::<LE>(*player_disabled_buildings.get(i).unwrap_or(&-1))?;
}
}
} else if version > 1.03 {
let most = *self.num_disabled_techs.iter().max().unwrap_or(&0);
if most > 20 {
return Err(Error::TooManyDisabledTechsError(most));
}
if self.num_disabled_units.iter().any(|&n| n > 0) {
return Err(Error::CannotDisableUnitsError);
}
if self.num_disabled_buildings.iter().any(|&n| n > 0) {
return Err(Error::CannotDisableBuildingsError);
}
for player_disabled_techs in &self.disabled_techs {
for i in 0..20 {
output.write_i32::<LE>(*player_disabled_techs.get(i).unwrap_or(&-1))?;
}
}
} else {
if self.num_disabled_techs.iter().any(|&n| n > 0) {
return Err(Error::CannotDisableTechsError);
}
if self.num_disabled_units.iter().any(|&n| n > 0) {
return Err(Error::CannotDisableUnitsError);
}
if self.num_disabled_buildings.iter().any(|&n| n > 0) {
return Err(Error::CannotDisableBuildingsError);
}
}
if version > 1.04 {
output.write_i32::<LE>(0)?;
}
if version >= 1.12 {
output.write_i32::<LE>(0)?;
output.write_i32::<LE>(if self.all_techs { 1 } else { 0 })?;
}
if version > 1.05 {
for start_age in &self.player_start_ages {
output.write_i32::<LE>(start_age.to_i32(version))?;
}
}
if version >= 1.02 {
output.write_i32::<LE>(-99)?;
}
if version >= 1.19 {
output.write_i32::<LE>(self.view.0)?;
output.write_i32::<LE>(self.view.1)?;
}
if version >= 1.21 {
output.write_i32::<LE>(self.map_type.unwrap_or(-1))?;
}
if version >= 1.24 {
assert_eq!(self.base_priorities.len(), 16);
for priority in &self.base_priorities {
output.write_i8(*priority)?;
}
}
if version >= 1.28 {
output.write_u32::<LE>(num_triggers)?;
output.write_u16::<LE>(0)?;
write_opt_str(&mut output, &self.water_definition)?;
}
if version >= 1.36 {
output.write_u8(0)?;
output.write_u8(0)?;
write_opt_str(&mut output, &self.color_mood)?;
output.write_u8(if self.collide_and_correct { 1 } else { 0 })?;
}
if version >= 1.37 {
output.write_u8(if self.villager_force_drop { 1 } else { 0 })?;
}
Ok(())
}
pub fn version(&self) -> f32 {
self.base.version
}
pub fn description(&self) -> Option<&str> {
self.base.description.as_ref().map(|s| &**s)
}
}
#[derive(Debug, Clone)]
pub struct SCXFormat {
pub(crate) version: SCXVersion,
pub(crate) header: SCXHeader,
pub(crate) next_object_id: i32,
pub(crate) tribe_scen: TribeScen,
pub(crate) map: Map,
world_players: Vec<WorldPlayerData>,
pub(crate) player_objects: Vec<Vec<ScenarioObject>>,
scenario_players: Vec<ScenarioPlayerData>,
pub(crate) triggers: Option<TriggerSystem>,
ai_info: Option<AIInfo>,
}
impl SCXFormat {
pub fn version(&self) -> VersionBundle {
VersionBundle {
format: self.version,
header: self.header.version,
data: self.tribe_scen.version(),
triggers: self.triggers.as_ref().map(|triggers| triggers.version()),
map: self.map.version(),
..VersionBundle::aoc()
}
}
fn load_inner(version: SCXVersion, player_version: f32, mut input: impl Read) -> Result<Self> {
let header = SCXHeader::read_from(&mut input, version)?;
let mut input = DeflateDecoder::new(&mut input);
let next_object_id = input.read_i32::<LE>()?;
let tribe_scen = TribeScen::read_from(&mut input)?;
let map = Map::read_from(&mut input)?;
let num_players = input.read_u32::<LE>()?;
log::debug!("number of players: {}", num_players);
let mut world_players = Vec::with_capacity(num_players as usize);
for _ in 1..num_players {
world_players.push(WorldPlayerData::read_from(&mut input, player_version)?);
}
fn read_scenario_players(
mut input: impl Read,
player_version: f32,
) -> Result<Vec<ScenarioPlayerData>> {
let num = input.read_u32::<LE>()?;
let mut players = Vec::with_capacity(num as usize);
log::debug!("number of scenario players: {}", num);
for _ in 1..num {
players.push(ScenarioPlayerData::read_from(&mut input, player_version)?);
}
Ok(players)
}
fn read_player_objects(
mut input: impl Read,
num_players: u32,
version: SCXVersion,
) -> Result<Vec<Vec<ScenarioObject>>> {
let mut player_objects = Vec::with_capacity(num_players as usize);
for _ in 0..num_players {
let num_objects = input.read_u32::<LE>()?;
let mut objects = Vec::with_capacity(num_objects as usize);
log::debug!("number of objects: {}", num_objects);
for _ in 0..num_objects {
objects.push(ScenarioObject::read_from(&mut input, version)?);
}
player_objects.push(objects);
}
Ok(player_objects)
}
let (scenario_players, player_objects) = if version >= SCXVersion(*b"1.36") {
let players = read_scenario_players(&mut input, player_version)?;
let objects = read_player_objects(&mut input, num_players, version)?;
(players, objects)
} else {
let objects = read_player_objects(&mut input, num_players, version)?;
let players = read_scenario_players(&mut input, player_version)?;
(players, objects)
};
let triggers = if version < SCXVersion(*b"1.14") {
None
} else {
Some(TriggerSystem::read_from(&mut input)?)
};
let ai_info = if version > SCXVersion(*b"1.17") && version < SCXVersion(*b"2.00") {
AIInfo::read_from(&mut input)?
} else {
None
};
Ok(SCXFormat {
version,
header,
next_object_id,
tribe_scen,
map,
world_players,
player_objects,
scenario_players,
triggers,
ai_info,
})
}
pub fn load_scenario(mut input: impl Read) -> Result<Self> {
let mut format_version = [0; 4];
input.read_exact(&mut format_version)?;
let format_version = SCXVersion(format_version);
if let Some(player_version) = format_version.to_player_version() {
Self::load_inner(format_version, player_version, input)
} else {
Err(Error::UnsupportedFormatVersionError(format_version))
}
}
fn write_player_objects(
&self,
mut output: impl Write,
format_version: SCXVersion,
) -> Result<()> {
for objects in &self.player_objects {
output.write_i32::<LE>(objects.len() as i32)?;
for object in objects {
object.write_to(&mut output, format_version)?;
}
}
Ok(())
}
fn write_scenario_players(
&self,
mut output: impl Write,
player_version: f32,
victory_version: f32,
) -> Result<()> {
output.write_i32::<LE>(self.scenario_players.len() as i32 + 1)?;
for player in &self.scenario_players {
player.write_to(&mut output, player_version, victory_version)?;
}
Ok(())
}
pub fn write_to(&self, mut output: impl Write, version: &VersionBundle) -> Result<()> {
let player_version = match version.format.to_player_version() {
Some(v) => v,
None => return Err(Error::UnsupportedFormatVersionError(version.format)),
};
output.write_all(version.format.as_bytes())?;
self.header
.write_to(&mut output, version.format, version.header)?;
let mut output = DeflateEncoder::new(output, Compression::default());
output.write_i32::<LE>(self.next_object_id)?;
let num_triggers = self
.triggers
.as_ref()
.map(|trigger_system| trigger_system.num_triggers())
.unwrap_or(0);
self.tribe_scen
.write_to(&mut output, version.data, num_triggers)?;
self.map.write_to(&mut output, version.map)?;
output.write_i32::<LE>(self.player_objects.len() as i32)?;
for player in &self.world_players {
player.write_to(&mut output, player_version)?;
}
if version.format >= SCXVersion(*b"1.36") {
self.write_scenario_players(&mut output, player_version, version.victory)?;
self.write_player_objects(&mut output, version.format)?;
} else {
self.write_player_objects(&mut output, version.format)?;
self.write_scenario_players(&mut output, player_version, version.victory)?;
}
if version.format > SCXVersion(*b"1.13") {
let def = TriggerSystem::default();
let triggers = match self.triggers {
Some(ref tr) => tr,
None => &def,
};
triggers.write_to(&mut output, version.triggers.unwrap_or(1.6))?;
}
if version.format > SCXVersion(*b"1.17") && version.format < SCXVersion(*b"2.00") {
let def = AIInfo::default();
let ai_info = match self.ai_info {
Some(ref ai) => ai,
None => &def,
};
ai_info.write_to(&mut output)?;
}
output.finish()?;
Ok(())
}
pub fn mod_name(&self) -> Option<&str> {
self.tribe_scen.base.player_names[9]
.as_ref()
.map(|string| string.as_str())
}
#[cfg(test)]
pub fn hash(&self) -> u64 {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let mut hasher = DefaultHasher::new();
format!("{:#?}", self).hash(&mut hasher);
hasher.finish()
}
}
fn write_opt_string_key(mut output: impl Write, opt_key: &Option<StringKey>) -> Result<()> {
output.write_u32::<LE>(if let Some(key) = opt_key {
key.try_into()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?
} else {
0xFFFF_FFFF
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::SCXFormat;
use crate::{Result, VersionBundle};
use std::fs::File;
use std::io::{Cursor, ErrorKind, Read};
fn save_and_load(format: &SCXFormat, as_version: VersionBundle) -> Result<SCXFormat> {
let mut out = vec![];
format.write_to(&mut out, &as_version)?;
let mut f = Cursor::new(out);
let scx = SCXFormat::load_scenario(&mut f)?;
assert_consumed(f);
Ok(scx)
}
fn assert_consumed(mut input: impl Read) {
let byte = &mut [0];
match input.read_exact(byte) {
Err(err) if err.kind() == ErrorKind::UnexpectedEof => (),
Err(err) => panic!("{}", err),
Ok(_) => {
let mut trailing_data = vec![byte[0]];
input.read_to_end(&mut trailing_data).unwrap();
panic!("data left in buffer ({}): {:?}", trailing_data.len(), {
trailing_data.truncate(32);
trailing_data
});
}
}
}
#[test]
fn oldest_aoe1_scn_on_aoeheaven() {
let mut f = File::open("test/scenarios/ The Destruction of Rome.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe1_beta_scn_reserialize() {
let mut f = File::open("test/scenarios/Dawn of a New Age.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
assert_eq!(
format.hash(),
format2.hash(),
"should produce exactly the same scenario"
);
}
#[test]
fn aoe1_beta_scn_to_aoc() {
let mut f = File::open("test/scenarios/Dawn of a New Age.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let format2 = save_and_load(&format, VersionBundle::aoc()).expect("save-and-load failed");
assert_eq!(
format2.version(),
VersionBundle::aoc(),
"should have converted to AoC versions"
);
}
#[test]
fn aoe1_trial_scn() {
let mut f = File::open("test/scenarios/Bronze Age Art of War.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe1_ppc_trial_scn() {
let mut f = File::open("test/scenarios/CEASAR.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe1_scn() {
let mut f = File::open("test/scenarios/A New Emporer.scn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe1_ror_scx() {
let mut f = File::open("test/scenarios/Jeremiah Johnson (Update).scx").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe1_ror_to_aoc() -> Result<()> {
let mut f = File::open("test/scenarios/El advenimiento de los hunos_.scx")?;
let format = SCXFormat::load_scenario(&mut f)?;
assert_consumed(f);
let format2 = save_and_load(&format, VersionBundle::aoc())?;
assert_eq!(
format2.version(),
VersionBundle::aoc(),
"should have converted to AoC versions"
);
Ok(())
}
#[test]
fn oldest_aok_scn_on_aokheaven() {
let mut f = File::open("test/scenarios/CAMELOT.SCN").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoc_scx() {
let mut f = File::open("test/scenarios/Age of Heroes b1-3-5.scx").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn hd_aoe2scenario() {
let mut f = File::open("test/scenarios/Year_of_the_Pig.aoe2scenario").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
assert_eq!(
format.hash(),
format2.hash(),
"should produce exactly the same scenario"
);
}
#[test]
fn hd_scx2() {
let mut f = File::open("test/scenarios/real_world_amazon.scx").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
#[ignore]
fn aoe_de_scn() {
let mut f = File::open("test/scenarios/Corlis.aoescn").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let mut out = vec![];
format
.write_to(&mut out, &format.version())
.expect("failed to write");
}
#[test]
fn aoe_de2_1_36() {
let mut f = File::open("test/scenarios/Hotkey Trainer Buildings.aoe2scenario").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
assert_eq!(
format.hash(),
format2.hash(),
"should produce exactly the same scenario"
);
}
#[test]
fn aoe_de2_1_37() {
let mut f = File::open("test/scenarios/layertest.aoe2scenario").unwrap();
let format = SCXFormat::load_scenario(&mut f).expect("failed to read");
assert_consumed(f);
let format2 = save_and_load(&format, format.version()).expect("save-and-load failed");
assert_eq!(
format.hash(),
format2.hash(),
"should produce exactly the same scenario"
);
}
}