diff --git a/.gitignore b/.gitignore index 5cb9f6a..8d25468 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target **/*.rs.bk Cargo.lock -.vscode \ No newline at end of file +.vscode +crates/genie-rec/examples/nom.rs diff --git a/Cargo.toml b/Cargo.toml index 7f2980a..26a2bc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,45 @@ [package] name = "genie" version = "0.5.0" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" description = "Libraries for reading/writing Age of Empires II data files" +documentation = "https://docs.rs/genie/" homepage = "https://github.com/SiegeEngineers/genie-rs" +readme = "./README.md" + +[workspace] +members = [ + "crates/genie-cpx", + "crates/genie-dat", + "crates/genie-drs", + "crates/genie-hki", + "crates/genie-lang", + "crates/genie-rec", + "crates/genie-scx", + "crates/genie-support", + "crates/jascpal", +] + +[workspace.package] +authors = ["Renée Kooi "] +edition = "2021" +rust-version = "1.64.0" +license = "GPL-3.0" repository = "https://github.com/SiegeEngineers/genie-rs" -readme = "README.md" + +[workspace.dependencies] +structopt = "0.3.26" +anyhow = "1.0.65" +simplelog = "0.12.0" +thiserror = "1.0.36" +byteorder = "1.4.3" +flate2 = { version = "1.0.24", features = [ + "rust_backend", +], default-features = false } +encoding_rs = "0.8.31" +encoding_rs_io = "0.1.7" +rgb = "0.8.34" +num_enum = "0.5.7" +arrayvec = "0.7.2" [dependencies] genie-cpx = { version = "0.5.0", path = "crates/genie-cpx" } @@ -20,19 +52,6 @@ genie-scx = { version = "4.0.0", path = "crates/genie-scx" } jascpal = { version = "0.1.1", path = "crates/jascpal" } [dev-dependencies] -structopt = "0.3.20" -anyhow = "1.0.33" -simplelog = "0.10.0" - -[workspace] -members = [ - "crates/genie-cpx", - "crates/genie-dat", - "crates/genie-drs", - "crates/genie-hki", - "crates/genie-lang", - "crates/genie-rec", - "crates/genie-scx", - "crates/genie-support", - "crates/jascpal" -] +structopt.workspace = true +anyhow.workspace = true +simplelog.workspace = true diff --git a/README.md b/README.md index 9c336a2..95b241d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # genie-rs -Rust libraries for reading/writing various Age of Empires I/II files. +[![docs.rs](https://img.shields.io/badge/docs.rs-genie-blue?style=flat-square)](https://docs.rs/genie/) +[![crates.io](https://img.shields.io/crates/v/genie.svg?style=flat-square)](https://crates.io/crates/genie) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) -See [docs.rs](https://docs.rs/genie) for documentation. +Rust libraries for reading/writing various Age of Empires I/II files. ## Example Programs diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..2ee0a40 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,101 @@ +# This is a configuration file for the bacon tool +# More info at https://github.com/Canop/bacon + +default_job = "check" + +[jobs] + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false +watch = ["tests", "benches", "examples"] + +[jobs.clippy] +command = ["cargo", "clippy", "--color", "always"] +need_stdout = false + +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets", "--color", "always"] +need_stdout = false +watch = ["tests", "benches", "examples"] + +[jobs.test] +command = ["cargo", "test", "--workspace", "--color", "always"] +need_stdout = true +watch = ["tests"] + +# Uses nextest +[jobs.ntest] +command = [ + "cargo", + "nextest", + "run", + "--workspace", + "--color", + "always", + "--build-jobs", + "14", + "--fail-fast", + "--test-threads", + "14", +] +need_stdout = true +watch = ["tests"] + +# Only test a single package +# Uses nextest +[jobs.mtest] +command = [ + "cargo", + "nextest", + "run", + "--color", + "always", + "--build-jobs", + "14", + "--fail-fast", + "--test-threads", + "14", + "-p", + "genie-rec", +] +need_stdout = true +watch = ["tests"] + + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# if the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. You can run an example the same +# way. Don't forget the `--color always` part or the errors won't be +# properly parsed. +[jobs.run] +command = ["cargo", "run", "--color", "always"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal prefs.toml file instead. +[keybindings] +a = "job:check-all" +i = "job:initial" +c = "job:clippy" +d = "job:doc-open" +t = "job:ntest" +r = "job:run" +e = "job:mtest" diff --git a/crates/genie-cpx/Cargo.toml b/crates/genie-cpx/Cargo.toml index 2365274..173e4aa 100644 --- a/crates/genie-cpx/Cargo.toml +++ b/crates/genie-cpx/Cargo.toml @@ -1,20 +1,23 @@ [package] name = "genie-cpx" version = "0.5.0" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read and write Age of Empires I/II campaign files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-cpx" +documentation = "https://docs.rs/genie-cpx" +repository.workspace = true +readme = "./README.md" exclude = ["test/campaigns"] [dependencies] -byteorder = "1.3.4" +byteorder.workspace = true chardet = "0.2.4" -encoding_rs = "0.8.24" +encoding_rs.workspace = true genie-scx = { version = "4.0.0", path = "../genie-scx" } -thiserror = "1.0.21" +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-cpx/README.md b/crates/genie-cpx/README.md index 5b9e588..98b45cf 100644 --- a/crates/genie-cpx/README.md +++ b/crates/genie-cpx/README.md @@ -1,10 +1,11 @@ # genie-cpx -Read and write Age of Empires I/II campaign files. - -## Usage +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--cpx-blue?style=flat-square&color=blue)](https://docs.rs/genie-cpx/) +[![crates.io](https://img.shields.io/crates/v/genie-cpx.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-cpx) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) -See [docs.rs](https://docs.rs/genie-cpx) for API documentation. +Read and write Age of Empires I/II campaign files. ## License diff --git a/crates/genie-cpx/src/read.rs b/crates/genie-cpx/src/read.rs index 5c36cbd..3b06eaa 100644 --- a/crates/genie-cpx/src/read.rs +++ b/crates/genie-cpx/src/read.rs @@ -34,11 +34,11 @@ fn decode_str(bytes: &[u8]) -> Result { return Ok("".to_string()); } - let (encoding_name, _confidence, _language) = detect_encoding(&bytes); + let (encoding_name, _confidence, _language) = detect_encoding(bytes); Encoding::for_label(encoding_name.as_bytes()) .ok_or(ReadCampaignError::DecodeStringError) .and_then(|encoding| { - let (decoded, _enc, failed) = encoding.decode(&bytes); + let (decoded, _enc, failed) = encoding.decode(bytes); if failed { return Err(ReadCampaignError::DecodeStringError); } diff --git a/crates/genie-dat/Cargo.toml b/crates/genie-dat/Cargo.toml index 16d24c3..7842090 100644 --- a/crates/genie-dat/Cargo.toml +++ b/crates/genie-dat/Cargo.toml @@ -1,21 +1,24 @@ [package] name = "genie-dat" version = "0.1.0" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read and write Age of Empires I/II data files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-dat" +documentation = "https://docs.rs/genie-dat" +repository.workspace = true +readme = "./README.md" [dependencies] -arrayvec = "0.7.0" -byteorder = "1.3.4" -encoding_rs = "0.8.24" -flate2 = { version = "1.0.18", features = ["rust_backend"], default-features = false } -genie-support = { version = "^1.0.0", path = "../genie-support" } +arrayvec.workspace = true +byteorder.workspace = true +encoding_rs.workspace = true +flate2.workspace = true +genie-support = { version = "^2.0.0", path = "../genie-support" } jascpal = { version = "^0.1.0", path = "../jascpal" } -thiserror = "1.0.21" +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-dat/README.md b/crates/genie-dat/README.md new file mode 100644 index 0000000..558049c --- /dev/null +++ b/crates/genie-dat/README.md @@ -0,0 +1,12 @@ +# genie-dat + +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--dat-blue?style=flat-square&color=blue)](https://docs.rs/genie-dat/) +[![crates.io](https://img.shields.io/crates/v/genie-dat.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-dat) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) + +Read and write Age of Empires I/II data files. + +## License + +[GPL-3.0](../../LICENSE.md) diff --git a/crates/genie-dat/src/civ.rs b/crates/genie-dat/src/civ.rs index 73fad58..f771981 100644 --- a/crates/genie-dat/src/civ.rs +++ b/crates/genie-dat/src/civ.rs @@ -76,7 +76,7 @@ impl Civilization { let mut bytes = [0; 20]; input.read_exact(&mut bytes)?; let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; - let (name, _encoding, _failed) = WINDOWS_1252.decode(&bytes); + let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); civ.name = CivName::from(&name).unwrap(); let num_attributes = input.read_u16::()?; civ.civ_effect = input.read_u16::()?; @@ -112,7 +112,7 @@ impl Civilization { /// Write civilization data to an output stream. pub fn write_to(&self, mut output: impl Write, version: GameVersion) -> Result<()> { let mut name = [0; 20]; - (&mut name[..self.name.len()]).copy_from_slice(self.name.as_bytes()); + (name[..self.name.len()]).copy_from_slice(self.name.as_bytes()); output.write_all(&name)?; output.write_u16::(self.attributes.len().try_into().unwrap())?; output.write_u16::(self.civ_effect)?; @@ -129,10 +129,8 @@ impl Civilization { None => 0, })?; } - for opt in &self.unit_types { - if let Some(unit_type) = opt { - unit_type.write_to(&mut output, version.as_f32())?; - } + for unit_type in self.unit_types.iter().flatten() { + unit_type.write_to(&mut output, version.as_f32())?; } Ok(()) } diff --git a/crates/genie-dat/src/lib.rs b/crates/genie-dat/src/lib.rs index 727be63..51c8ac0 100644 --- a/crates/genie-dat/src/lib.rs +++ b/crates/genie-dat/src/lib.rs @@ -62,7 +62,7 @@ impl GameVersion { } /// A data file version. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FileVersion([u8; 8]); impl From<[u8; 8]> for FileVersion { @@ -80,7 +80,7 @@ impl From<&str> for FileVersion { fn from(string: &str) -> Self { assert!(string.len() <= 8); let mut bytes = [0; 8]; - (&mut bytes[..string.len()]).copy_from_slice(string.as_bytes()); + (bytes[..string.len()]).copy_from_slice(string.as_bytes()); Self::from(bytes) } } @@ -400,10 +400,8 @@ impl DatFile { None => 0, })?; } - for maybe_sprite in &self.sprites { - if let Some(sprite) = maybe_sprite { - sprite.write_to(&mut output)?; - } + for sprite in self.sprites.iter().flatten() { + sprite.write_to(&mut output)?; } output.write_u32::(0)?; // map vtable pointer diff --git a/crates/genie-dat/src/random_map.rs b/crates/genie-dat/src/random_map.rs index de83711..69c0536 100644 --- a/crates/genie-dat/src/random_map.rs +++ b/crates/genie-dat/src/random_map.rs @@ -23,18 +23,20 @@ pub struct RandomMapInfo { impl RandomMapInfo { pub fn read_from(input: &mut R) -> Result { - let mut info = Self::default(); - info.id = input.read_i32::()?; - info.borders = ( - input.read_i32::()?, - input.read_i32::()?, - input.read_i32::()?, - input.read_i32::()?, - ); - info.border_fade = input.read_i32::()?; - info.water_border = input.read_i32::()?; - info.base_terrain = input.read_i32::()?; - info.land_percent = input.read_i32::()?; + let mut info = RandomMapInfo { + id: input.read_i32::()?, + borders: ( + input.read_i32::()?, + input.read_i32::()?, + input.read_i32::()?, + input.read_i32::()?, + ), + border_fade: input.read_i32::()?, + water_border: input.read_i32::()?, + base_terrain: input.read_i32::()?, + land_percent: input.read_i32::()?, + ..Default::default() + }; let _some_id = input.read_i32::()?; let num_lands = input.read_u32::()?; @@ -159,9 +161,11 @@ pub struct RandomMapLand { impl RandomMapLand { pub fn read_from(input: &mut R) -> Result { - let mut land = Self::default(); - land.id = input.read_i32::()?; - land.terrain_type = input.read_u8()?; + let mut land = RandomMapLand { + id: input.read_i32::()?, + terrain_type: input.read_u8()?, + ..Default::default() + }; let _padding = input.read_u16::()?; let _padding = input.read_u8()?; land.land_avoidance_tiles = input.read_i32::()?; @@ -214,14 +218,14 @@ pub struct RandomMapTerrain { impl RandomMapTerrain { pub fn read_from(input: &mut R) -> Result { - let mut terrain = Self::default(); - terrain.percent = input.read_i32::()?; - terrain.terrain_type = input.read_i32::()?; - terrain.clumps = input.read_i32::()?; - terrain.spacing = input.read_i32::()?; - terrain.base_terrain_type = input.read_i32::()?; - terrain.clumpiness_factor = input.read_i32::()?; - Ok(terrain) + Ok(RandomMapTerrain { + percent: input.read_i32::()?, + terrain_type: input.read_i32::()?, + clumps: input.read_i32::()?, + spacing: input.read_i32::()?, + base_terrain_type: input.read_i32::()?, + clumpiness_factor: input.read_i32::()?, + }) } pub fn write_to(&self, output: &mut W) -> Result<()> { @@ -253,11 +257,13 @@ pub struct RandomMapObject { impl RandomMapObject { pub fn read_from(input: &mut R) -> Result { - let mut object = Self::default(); - object.unit_type = input.read_u32::()?.try_into().unwrap(); - object.terrain_type = input.read_i32::()?; - object.group_flag = input.read_i8()?; - object.scale_flag = input.read_i8()?; + let mut object = RandomMapObject { + unit_type: input.read_u32::()?.try_into().unwrap(), + terrain_type: input.read_i32::()?, + group_flag: input.read_i8()?, + scale_flag: input.read_i8()?, + ..Default::default() + }; let _padding = input.read_u16::()?; object.group_size = input.read_i32::()?; object.group_size_variance = input.read_i32::()?; @@ -300,14 +306,14 @@ pub struct RandomMapElevation { impl RandomMapElevation { pub fn read_from(input: &mut R) -> Result { - let mut elevation = Self::default(); - elevation.percent = input.read_u32::()?.try_into().unwrap(); - elevation.height = input.read_i32::()?; - elevation.clumps = input.read_i32::()?; - elevation.spacing = input.read_i32::()?; - elevation.base_terrain_type = input.read_i32::()?; - elevation.base_elevation = input.read_i32::()?; - Ok(elevation) + Ok(RandomMapElevation { + percent: input.read_u32::()?.try_into().unwrap(), + height: input.read_i32::()?, + clumps: input.read_i32::()?, + spacing: input.read_i32::()?, + base_terrain_type: input.read_i32::()?, + base_elevation: input.read_i32::()?, + }) } pub fn write_to(&self, output: &mut W) -> Result<()> { diff --git a/crates/genie-dat/src/sound.rs b/crates/genie-dat/src/sound.rs index 9275299..48d3239 100644 --- a/crates/genie-dat/src/sound.rs +++ b/crates/genie-dat/src/sound.rs @@ -97,9 +97,11 @@ impl SoundItem { impl Sound { /// Read this sound from an input stream. pub fn read_from(input: &mut R, version: FileVersion) -> Result { - let mut sound = Sound::default(); - sound.id = input.read_u16::()?.into(); - sound.play_delay = input.read_i16::()?; + let mut sound = Sound { + id: input.read_u16::()?.into(), + play_delay: input.read_i16::()?, + ..Default::default() + }; let num_items = input.read_u16::()?; sound.cache_time = input.read_i32::()?; if version.is_de2() { diff --git a/crates/genie-dat/src/sprite.rs b/crates/genie-dat/src/sprite.rs index 11a83bd..c1c73e3 100644 --- a/crates/genie-dat/src/sprite.rs +++ b/crates/genie-dat/src/sprite.rs @@ -48,6 +48,7 @@ pub struct SpriteDelta { pub struct SoundProp { pub sound_delay: i16, pub sound_id: SoundID, + #[allow(dead_code)] wwise_sound_id: Option, } @@ -113,8 +114,10 @@ pub struct Sprite { impl SpriteDelta { pub fn read_from(mut input: impl Read) -> Result { - let mut delta = SpriteDelta::default(); - delta.sprite_id = read_opt_u16(&mut input)?; + let mut delta = SpriteDelta { + sprite_id: read_opt_u16(&mut input)?, + ..Default::default() + }; let _padding = input.read_i16::()?; let _parent_sprite_pointer = input.read_i32::()?; delta.offset_x = input.read_i16::()?; @@ -259,11 +262,11 @@ impl Sprite { output.write_all(&name)?; output.write_all(&filename)?; output.write_i32::(self.slp_id.map(|v| v.try_into().unwrap()).unwrap_or(-1))?; - output.write_u8(if self.is_loaded { 1 } else { 0 })?; + output.write_u8(u8::from(self.is_loaded))?; output.write_u8(self.force_player_color.unwrap_or(0xFF))?; output.write_u8(self.layer)?; output.write_u16::(self.color_table)?; - output.write_u8(if self.transparent_selection { 1 } else { 0 })?; + output.write_u8(u8::from(self.transparent_selection))?; output.write_i16::(self.bounding_box.0)?; output.write_i16::(self.bounding_box.1)?; output.write_i16::(self.bounding_box.2)?; @@ -271,7 +274,7 @@ impl Sprite { output.write_u16::(self.deltas.len().try_into().unwrap())?; output.write_i16::(self.sound_id.map(|v| v.try_into().unwrap()).unwrap_or(-1))?; - output.write_u8(if self.attack_sounds.is_empty() { 0 } else { 1 })?; + output.write_u8(u8::from(!self.attack_sounds.is_empty()))?; output.write_u16::(self.num_frames)?; output.write_u16::(self.num_angles)?; output.write_f32::(self.base_speed)?; diff --git a/crates/genie-dat/src/task.rs b/crates/genie-dat/src/task.rs index 2259ead..f900e33 100644 --- a/crates/genie-dat/src/task.rs +++ b/crates/genie-dat/src/task.rs @@ -69,42 +69,41 @@ impl TaskList { impl Task { pub fn read_from(mut input: impl Read) -> Result { - let mut task = Self::default(); - task.id = input.read_u16::()?; - task.is_default = input.read_u8()? != 0; - task.action_type = input.read_u16::()?; - task.object_class = input.read_i16::()?; - task.object_id = read_opt_u16(&mut input)?; - task.terrain_id = input.read_i16::()?; - task.attribute_types = ( - input.read_i16::()?, - input.read_i16::()?, - input.read_i16::()?, - input.read_i16::()?, - ); - task.work_values = (input.read_f32::()?, input.read_f32::()?); - task.work_range = input.read_f32::()?; - task.auto_search_targets = input.read_u8()? != 0; - task.search_wait_time = input.read_f32::()?; - task.enable_targeting = input.read_u8()? != 0; - task.combat_level = input.read_u8()?; - task.work_flags = (input.read_u16::()?, input.read_u16::()?); - task.owner_type = input.read_u8()?; - task.holding_attribute = input.read_u8()?; - task.state_building = input.read_u8()?; - task.move_sprite = read_opt_u16(&mut input)?; - task.work_sprite = read_opt_u16(&mut input)?; - task.work_sprite2 = read_opt_u16(&mut input)?; - task.carry_sprite = read_opt_u16(&mut input)?; - task.work_sound = read_opt_u16(&mut input)?; - task.work_sound2 = read_opt_u16(&mut input)?; - - Ok(task) + Ok(Task { + id: input.read_u16::()?, + is_default: input.read_u8()? != 0, + action_type: input.read_u16::()?, + object_class: input.read_i16::()?, + object_id: read_opt_u16(&mut input)?, + terrain_id: input.read_i16::()?, + attribute_types: ( + input.read_i16::()?, + input.read_i16::()?, + input.read_i16::()?, + input.read_i16::()?, + ), + work_values: (input.read_f32::()?, input.read_f32::()?), + work_range: input.read_f32::()?, + auto_search_targets: input.read_u8()? != 0, + search_wait_time: input.read_f32::()?, + enable_targeting: input.read_u8()? != 0, + combat_level: input.read_u8()?, + work_flags: (input.read_u16::()?, input.read_u16::()?), + owner_type: input.read_u8()?, + holding_attribute: input.read_u8()?, + state_building: input.read_u8()?, + move_sprite: read_opt_u16(&mut input)?, + work_sprite: read_opt_u16(&mut input)?, + work_sprite2: read_opt_u16(&mut input)?, + carry_sprite: read_opt_u16(&mut input)?, + work_sound: read_opt_u16(&mut input)?, + work_sound2: read_opt_u16(&mut input)?, + }) } pub fn write_to(&self, output: &mut W) -> Result<()> { output.write_u16::(self.id)?; - output.write_u8(if self.is_default { 1 } else { 0 })?; + output.write_u8(u8::from(self.is_default))?; output.write_u16::(self.action_type)?; output.write_i16::(self.object_class)?; output.write_i16::( @@ -120,9 +119,9 @@ impl Task { output.write_f32::(self.work_values.0)?; output.write_f32::(self.work_values.1)?; output.write_f32::(self.work_range)?; - output.write_u8(if self.auto_search_targets { 1 } else { 0 })?; + output.write_u8(u8::from(self.auto_search_targets))?; output.write_f32::(self.search_wait_time)?; - output.write_u8(if self.enable_targeting { 1 } else { 0 })?; + output.write_u8(u8::from(self.enable_targeting))?; output.write_u8(self.combat_level)?; output.write_u16::(self.work_flags.0)?; output.write_u16::(self.work_flags.1)?; diff --git a/crates/genie-dat/src/tech.rs b/crates/genie-dat/src/tech.rs index 6dfe8ae..01900be 100644 --- a/crates/genie-dat/src/tech.rs +++ b/crates/genie-dat/src/tech.rs @@ -98,7 +98,7 @@ impl TechEffect { let mut bytes = [0; 31]; input.read_exact(&mut bytes)?; let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; - let (name, _encoding, _failed) = WINDOWS_1252.decode(&bytes); + let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); effect.name = TechEffectName::from(&name).unwrap(); let num_commands = input.read_u16::()?; @@ -111,7 +111,7 @@ impl TechEffect { pub fn write_to(&self, output: &mut W) -> Result<()> { let mut buffer = [0; 31]; - (&mut buffer[..self.name.len()]).copy_from_slice(self.name.as_bytes()); + buffer[..self.name.len()].copy_from_slice(self.name.as_bytes()); output.write_all(&buffer)?; output.write_u16::(self.commands.len() as u16)?; @@ -134,7 +134,7 @@ impl TechEffectRef { pub fn write_to(self, output: &mut W) -> Result<()> { output.write_u16::(self.effect_type)?; output.write_u16::(self.amount)?; - output.write_u8(if self.enabled { 1 } else { 0 })?; + output.write_u8(u8::from(self.enabled))?; Ok(()) } } @@ -178,7 +178,7 @@ impl Tech { let mut bytes = vec![0; name_len as usize]; input.read_exact(&mut bytes)?; let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; - let (name, _encoding, _failed) = WINDOWS_1252.decode(&bytes); + let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); name.to_string() }; Ok(tech) diff --git a/crates/genie-dat/src/tech_tree.rs b/crates/genie-dat/src/tech_tree.rs index bcba66e..cb03961 100644 --- a/crates/genie-dat/src/tech_tree.rs +++ b/crates/genie-dat/src/tech_tree.rs @@ -194,9 +194,9 @@ impl TryFrom for TechTreeDependencyType { } } -impl Into for TechTreeDependencyType { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(tech_tree_dependency_type: TechTreeDependencyType) -> i32 { + tech_tree_dependency_type as i32 } } @@ -252,6 +252,7 @@ pub struct TechTreeUnit { status: TechTreeStatus, node_type: TechTreeType, depends_tech_id: Option, + // TODO: was Option before (merge conflict) building: UnitTypeID, requires_tech_id: Option, dependent_units: Vec, @@ -422,14 +423,16 @@ where impl TechTreeAge { pub fn read_from(input: &mut R) -> Result { - let mut age = Self::default(); - age.age_id = input.read_i32::()?; - age.status = input.read_u8()?.try_into().map_err(invalid_data)?; - age.dependent_buildings = read_dependents(input)?; - age.dependent_units = read_dependents(input)?; - age.dependent_techs = read_dependents(input)?; - age.prerequisites = TechTreeDependencies::read_from(input)?; - age.building_levels = input.read_u8()?; + let mut age = TechTreeAge { + age_id: input.read_i32::()?, + status: input.read_u8()?.try_into().map_err(invalid_data)?, + dependent_buildings: read_dependents(input)?, + dependent_units: read_dependents(input)?, + dependent_techs: read_dependents(input)?, + prerequisites: TechTreeDependencies::read_from(input)?, + building_levels: input.read_u8()?, + ..Default::default() + }; assert!(age.building_levels <= 10); for building in age.buildings_per_zone.iter_mut() { *building = input.read_u8()?; @@ -470,14 +473,16 @@ impl TechTreeAge { impl TechTreeBuilding { pub fn read_from(mut input: impl Read) -> Result { - let mut building = Self::default(); - building.building_id = input.read_i32::()?.try_into().map_err(invalid_data)?; - building.status = input.read_u8()?.try_into().map_err(invalid_data)?; - building.dependent_buildings = read_dependents(&mut input)?; - building.dependent_units = read_dependents(&mut input)?; - building.dependent_techs = read_dependents(&mut input)?; - building.prerequisites = TechTreeDependencies::read_from(&mut input)?; - building.level_no = input.read_u8()?; + let mut building = TechTreeBuilding { + building_id: input.read_i32::()?.try_into().map_err(invalid_data)?, + status: input.read_u8()?.try_into().map_err(invalid_data)?, + dependent_buildings: read_dependents(&mut input)?, + dependent_units: read_dependents(&mut input)?, + dependent_techs: read_dependents(&mut input)?, + prerequisites: TechTreeDependencies::read_from(&mut input)?, + level_no: input.read_u8()?, + ..Default::default() + }; for children in building.total_children_by_age.iter_mut() { *children = input.read_u8()?; } @@ -520,18 +525,18 @@ impl TechTreeBuilding { impl TechTreeUnit { pub fn read_from(mut input: impl Read) -> Result { - let mut unit = Self::default(); - unit.unit_id = input.read_i32::()?.try_into().map_err(invalid_data)?; - unit.status = input.read_u8()?.try_into().map_err(invalid_data)?; - unit.building = input.read_i32::()?.try_into().map_err(invalid_data)?; - unit.prerequisites = TechTreeDependencies::read_from(&mut input)?; - unit.group_id = input.read_i32::()?; - unit.dependent_units = read_dependents(&mut input)?; - unit.level_no = input.read_i32::()?; - unit.requires_tech_id = read_opt_u32(&mut input)?; - unit.node_type = input.read_i32::()?.try_into().map_err(invalid_data)?; - unit.depends_tech_id = read_opt_u32(&mut input)?; - Ok(unit) + Ok(TechTreeUnit { + unit_id: input.read_i32::()?.try_into().map_err(invalid_data)?, + status: input.read_u8()?.try_into().map_err(invalid_data)?, + building: input.read_i32::()?.try_into().map_err(invalid_data)?, + prerequisites: TechTreeDependencies::read_from(&mut input)?, + group_id: input.read_i32::()?, + dependent_units: read_dependents(&mut input)?, + level_no: input.read_i32::()?, + requires_tech_id: read_opt_u32(&mut input)?, + node_type: input.read_i32::()?.try_into().map_err(invalid_data)?, + depends_tech_id: read_opt_u32(&mut input)?, + }) } pub fn write_to(&self, mut output: impl Write) -> Result<()> { @@ -562,18 +567,18 @@ impl TechTreeUnit { impl TechTreeTech { pub fn read_from(input: &mut R) -> Result { - let mut tech = Self::default(); - tech.tech_id = input.read_i32::()?.try_into().map_err(invalid_data)?; - tech.status = input.read_u8()?.try_into().map_err(invalid_data)?; - tech.building = input.read_i32::()?.try_into().map_err(invalid_data)?; - tech.dependent_buildings = read_dependents(input)?; - tech.dependent_units = read_dependents(input)?; - tech.dependent_techs = read_dependents(input)?; - tech.prerequisites = TechTreeDependencies::read_from(input)?; - tech.group_id = input.read_i32::()?; - tech.level_no = input.read_i32::()?; - tech.node_type = input.read_i32::()?.try_into().map_err(invalid_data)?; - Ok(tech) + Ok(TechTreeTech { + tech_id: input.read_i32::()?.try_into().map_err(invalid_data)?, + status: input.read_u8()?.try_into().map_err(invalid_data)?, + building: input.read_i32::()?.try_into().map_err(invalid_data)?, + dependent_buildings: read_dependents(input)?, + dependent_units: read_dependents(input)?, + dependent_techs: read_dependents(input)?, + prerequisites: TechTreeDependencies::read_from(input)?, + group_id: input.read_i32::()?, + level_no: input.read_i32::()?, + node_type: input.read_i32::()?.try_into().map_err(invalid_data)?, + }) } pub fn write_to(&self, mut output: impl Write) -> Result<()> { diff --git a/crates/genie-dat/src/terrain.rs b/crates/genie-dat/src/terrain.rs index 9df8af4..69f6813 100644 --- a/crates/genie-dat/src/terrain.rs +++ b/crates/genie-dat/src/terrain.rs @@ -154,10 +154,12 @@ pub struct TerrainBorder { impl TerrainPassGraphic { pub fn read_from(mut input: impl Read, version: FileVersion) -> Result { - let mut pass = TerrainPassGraphic::default(); - pass.exit_tile_sprite = read_opt_u32(&mut input)?; - pass.enter_tile_sprite = read_opt_u32(&mut input)?; - pass.walk_tile_sprite = read_opt_u32(&mut input)?; + let mut pass = TerrainPassGraphic { + exit_tile_sprite: read_opt_u32(&mut input)?, + enter_tile_sprite: read_opt_u32(&mut input)?, + walk_tile_sprite: read_opt_u32(&mut input)?, + ..Default::default() + }; if version.is_swgb() { pass.walk_rate = Some(input.read_f32::()?); } else { @@ -246,23 +248,23 @@ impl TileSize { impl TerrainAnimation { pub fn read_from(input: &mut R) -> Result { - let mut anim = TerrainAnimation::default(); - anim.enabled = input.read_u8()? != 0; - anim.num_frames = input.read_i16::()?; - anim.num_pause_frames = input.read_i16::()?; - anim.frame_interval = input.read_f32::()?; - anim.replay_delay = input.read_f32::()?; - anim.frame = input.read_i16::()?; - anim.draw_frame = input.read_i16::()?; - anim.animate_last = input.read_f32::()?; - anim.frame_changed = input.read_u8()? != 0; - anim.drawn = input.read_u8()? != 0; - Ok(anim) + Ok(TerrainAnimation { + enabled: input.read_u8()? != 0, + num_frames: input.read_i16::()?, + num_pause_frames: input.read_i16::()?, + frame_interval: input.read_f32::()?, + replay_delay: input.read_f32::()?, + frame: input.read_i16::()?, + draw_frame: input.read_i16::()?, + animate_last: input.read_f32::()?, + frame_changed: input.read_u8()? != 0, + drawn: input.read_u8()? != 0, + }) } /// Serialize this object to a binary output stream. pub fn write_to(&self, output: &mut W) -> Result<()> { - output.write_u8(if self.enabled { 1 } else { 0 })?; + output.write_u8(u8::from(self.enabled))?; output.write_i16::(self.num_frames)?; output.write_i16::(self.num_pause_frames)?; output.write_f32::(self.frame_interval)?; @@ -270,8 +272,8 @@ impl TerrainAnimation { output.write_i16::(self.frame)?; output.write_i16::(self.draw_frame)?; output.write_f32::(self.animate_last)?; - output.write_u8(if self.frame_changed { 1 } else { 0 })?; - output.write_u8(if self.drawn { 1 } else { 0 })?; + output.write_u8(u8::from(self.frame_changed))?; + output.write_u8(u8::from(self.drawn))?; Ok(()) } } @@ -309,9 +311,11 @@ impl Terrain { version: FileVersion, num_terrains: u16, ) -> Result { - let mut terrain = Terrain::default(); - terrain.enabled = input.read_u8()? != 0; - terrain.random = input.read_u8()?; + let mut terrain = Terrain { + enabled: input.read_u8()? != 0, + random: input.read_u8()?, + ..Default::default() + }; read_terrain_name(&mut input, &mut terrain.name)?; read_terrain_name(&mut input, &mut terrain.slp_name)?; // println!("{}", terrain.name); @@ -380,7 +384,7 @@ impl Terrain { num_terrains: u16, ) -> Result<()> { assert_eq!(self.borders.len(), num_terrains as usize); - output.write_u8(if self.enabled { 1 } else { 0 })?; + output.write_u8(u8::from(self.enabled))?; output.write_u8(self.random)?; write_terrain_name(output, &self.name)?; write_terrain_name(output, &self.slp_name)?; @@ -442,9 +446,11 @@ impl Terrain { impl TerrainBorder { pub fn read_from(mut input: impl Read) -> Result { - let mut border = TerrainBorder::default(); - border.enabled = input.read_u8()? != 0; - border.random = input.read_u8()?; + let mut border = TerrainBorder { + enabled: input.read_u8()? != 0, + random: input.read_u8()?, + ..Default::default() + }; read_terrain_name(&mut input, &mut border.name)?; read_terrain_name(&mut input, &mut border.slp_name)?; border.slp_id = read_opt_u32(&mut input)?; @@ -471,7 +477,7 @@ impl TerrainBorder { /// Serialize this object to a binary output stream. pub fn write_to(&self, output: &mut W) -> Result<()> { - output.write_u8(if self.enabled { 1 } else { 0 })?; + output.write_u8(u8::from(self.enabled))?; output.write_u8(self.random)?; write_terrain_name(output, &self.name)?; write_terrain_name(output, &self.slp_name)?; @@ -509,7 +515,7 @@ fn read_terrain_name(input: &mut R, output: &mut TerrainName) -> Result fn write_terrain_name(output: &mut W, name: &TerrainName) -> Result<()> { let bytes = &mut [0; 13]; - (&mut bytes[..name.len()]).copy_from_slice(name.as_bytes()); + bytes[..name.len()].copy_from_slice(name.as_bytes()); output.write_all(bytes)?; Ok(()) } diff --git a/crates/genie-dat/src/unit_type.rs b/crates/genie-dat/src/unit_type.rs index d068b9a..9c8ebbc 100644 --- a/crates/genie-dat/src/unit_type.rs +++ b/crates/genie-dat/src/unit_type.rs @@ -577,28 +577,28 @@ impl StaticUnitTypeAttributes { .unwrap_or(-1), )?; output.write_u8(self.sort_number)?; - output.write_u8(if self.can_be_built_on { 1 } else { 0 })?; + output.write_u8(u8::from(self.can_be_built_on))?; output.write_i16::( self.button_picture .map(|id| id.try_into().unwrap()) .unwrap_or(-1), )?; - output.write_u8(if self.hide_in_scenario_editor { 1 } else { 0 })?; + output.write_u8(u8::from(self.hide_in_scenario_editor))?; output.write_i16::( self.portrait_picture .map(|id| id.try_into().unwrap()) .unwrap_or(-1), )?; - output.write_u8(if self.enabled { 1 } else { 0 })?; - output.write_u8(if self.disabled { 1 } else { 0 })?; + output.write_u8(u8::from(self.enabled))?; + output.write_u8(u8::from(self.disabled))?; output.write_i16::(self.tile_req.0)?; output.write_i16::(self.tile_req.1)?; output.write_i16::(self.center_tile_req.0)?; output.write_i16::(self.center_tile_req.1)?; output.write_f32::(self.construction_radius.0)?; output.write_f32::(self.construction_radius.1)?; - output.write_u8(if self.elevation_flag { 1 } else { 0 })?; - output.write_u8(if self.fog_flag { 1 } else { 0 })?; + output.write_u8(u8::from(self.elevation_flag))?; + output.write_u8(u8::from(self.fog_flag))?; output.write_u16::(self.terrain_restriction_id)?; output.write_u8(self.movement_type)?; output.write_u16::(self.attribute_max_amount)?; @@ -613,9 +613,9 @@ impl StaticUnitTypeAttributes { output.write_u32::((&self.help_string_id).try_into().unwrap())?; output.write_u32::(self.help_page_id)?; output.write_u32::(self.hotkey_id)?; - output.write_u8(if self.recyclable { 1 } else { 0 })?; - output.write_u8(if self.track_as_resource { 1 } else { 0 })?; - output.write_u8(if self.create_doppleganger { 1 } else { 0 })?; + output.write_u8(u8::from(self.recyclable))?; + output.write_u8(u8::from(self.track_as_resource))?; + output.write_u8(u8::from(self.create_doppleganger))?; output.write_u8(self.resource_group)?; output.write_u8(self.occlusion_mask)?; output.write_u8(self.obstruction_type)?; @@ -693,21 +693,21 @@ pub struct MovingUnitTypeAttributes { impl MovingUnitTypeAttributes { pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut attrs = Self::default(); - attrs.move_sprite = read_opt_u16(&mut input)?; - attrs.run_sprite = read_opt_u16(&mut input)?; - attrs.turn_speed = input.read_f32::()?; - attrs.size_class = input.read_u8()?; - attrs.trailing_unit = read_opt_u16(&mut input)?; - attrs.trailing_options = input.read_u8()?; - attrs.trailing_spacing = input.read_f32::()?; - attrs.move_algorithm = input.read_u8()?; - attrs.turn_radius = input.read_f32::()?; - attrs.turn_radius_speed = input.read_f32::()?; - attrs.maximum_yaw_per_second_moving = input.read_f32::()?; - attrs.stationary_yaw_revolution_time = input.read_f32::()?; - attrs.maximum_yaw_per_second_stationary = input.read_f32::()?; - Ok(attrs) + Ok(MovingUnitTypeAttributes { + move_sprite: read_opt_u16(&mut input)?, + run_sprite: read_opt_u16(&mut input)?, + turn_speed: input.read_f32::()?, + size_class: input.read_u8()?, + trailing_unit: read_opt_u16(&mut input)?, + trailing_options: input.read_u8()?, + trailing_spacing: input.read_f32::()?, + move_algorithm: input.read_u8()?, + turn_radius: input.read_f32::()?, + turn_radius_speed: input.read_f32::()?, + maximum_yaw_per_second_moving: input.read_f32::()?, + stationary_yaw_revolution_time: input.read_f32::()?, + maximum_yaw_per_second_stationary: input.read_f32::()?, + }) } /// Write this unit type to an output stream. @@ -759,17 +759,18 @@ pub struct ActionUnitTypeAttributes { impl ActionUnitTypeAttributes { pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut attrs = Self::default(); - attrs.default_task = read_opt_u16(&mut input)?; - attrs.search_radius = input.read_f32::()?; - attrs.work_rate = input.read_f32::()?; - attrs.drop_site = read_opt_u16(&mut input)?; - attrs.backup_drop_site = read_opt_u16(&mut input)?; - attrs.task_by_group = input.read_u8()?; - attrs.command_sound = read_opt_u16(&mut input)?; - attrs.move_sound = read_opt_u16(&mut input)?; - attrs.run_pattern = input.read_u8()?; - Ok(attrs) + Ok(ActionUnitTypeAttributes { + default_task: read_opt_u16(&mut input)?, + search_radius: input.read_f32::()?, + work_rate: input.read_f32::()?, + drop_site: read_opt_u16(&mut input)?, + backup_drop_site: read_opt_u16(&mut input)?, + task_by_group: input.read_u8()?, + command_sound: read_opt_u16(&mut input)?, + move_sound: read_opt_u16(&mut input)?, + run_pattern: input.read_u8()?, + ..Default::default() + }) } /// Write this unit type to an output stream. @@ -853,11 +854,13 @@ pub struct BaseCombatUnitTypeAttributes { impl BaseCombatUnitTypeAttributes { pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.base_armor = if version < 11.52 { - input.read_u8()?.into() - } else { - input.read_u16::()? + let mut attrs = BaseCombatUnitTypeAttributes { + base_armor: if version < 11.52 { + input.read_u8()?.into() + } else { + input.read_u16::()? + }, + ..Default::default() }; let num_weapons = input.read_u16::()?; for _ in 0..num_weapons { @@ -942,14 +945,14 @@ pub struct MissileUnitTypeAttributes { impl MissileUnitTypeAttributes { /// Read this unit type from an input stream. pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut attrs = Self::default(); - attrs.missile_type = input.read_u8()?; - attrs.targetting_type = input.read_u8()?; - attrs.missile_hit_info = input.read_u8()?; - attrs.missile_die_info = input.read_u8()?; - attrs.area_effect_specials = input.read_u8()?; - attrs.ballistics_ratio = input.read_f32::()?; - Ok(attrs) + Ok(MissileUnitTypeAttributes { + missile_type: input.read_u8()?, + targetting_type: input.read_u8()?, + missile_hit_info: input.read_u8()?, + missile_die_info: input.read_u8()?, + area_effect_specials: input.read_u8()?, + ballistics_ratio: input.read_f32::()?, + }) } /// Write this unit type to an output stream. @@ -1168,21 +1171,23 @@ pub struct BuildingUnitTypeAttributes { impl BuildingUnitTypeAttributes { /// Read this unit type from an input stream. pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.construction_sprite = read_opt_u16(&mut input)?; - attrs.snow_sprite = if version < 11.53 { - None - } else { - read_opt_u16(&mut input)? + let mut attrs = BuildingUnitTypeAttributes { + construction_sprite: read_opt_u16(&mut input)?, + snow_sprite: if version < 11.53 { + None + } else { + read_opt_u16(&mut input)? + }, + connect_flag: input.read_u8()?, + facet: input.read_i16::()?, + destroy_on_build: input.read_u8()? != 0, + on_build_make_unit: read_opt_u16(&mut input)?, + on_build_make_tile: read_opt_u16(&mut input)?, + on_build_make_overlay: input.read_i16::()?, + on_build_make_tech: read_opt_u16(&mut input)?, + can_burn: input.read_u8()? != 0, + ..Default::default() }; - attrs.connect_flag = input.read_u8()?; - attrs.facet = input.read_i16::()?; - attrs.destroy_on_build = input.read_u8()? != 0; - attrs.on_build_make_unit = read_opt_u16(&mut input)?; - attrs.on_build_make_tile = read_opt_u16(&mut input)?; - attrs.on_build_make_overlay = input.read_i16::()?; - attrs.on_build_make_tech = read_opt_u16(&mut input)?; - attrs.can_burn = input.read_u8()? != 0; for _ in 0..attrs.linked_buildings.capacity() { let link = LinkedBuilding::read_from(&mut input)?; if link.unit_id != 0xFFFF.into() { @@ -1213,12 +1218,12 @@ impl BuildingUnitTypeAttributes { } output.write_u8(self.connect_flag)?; output.write_i16::(self.facet)?; - output.write_u8(if self.destroy_on_build { 1 } else { 0 })?; + output.write_u8(u8::from(self.destroy_on_build))?; output.write_u16::(self.on_build_make_unit.map_into().unwrap_or(0xFFFF))?; output.write_u16::(self.on_build_make_tile.map_into().unwrap_or(0xFFFF))?; output.write_i16::(self.on_build_make_overlay)?; output.write_u16::(self.on_build_make_tech.map_into().unwrap_or(0xFFFF))?; - output.write_u8(if self.can_burn { 1 } else { 0 })?; + output.write_u8(u8::from(self.can_burn))?; for i in 0..self.linked_buildings.capacity() { match self.linked_buildings.get(i) { Some(link) => link.write_to(&mut output)?, diff --git a/crates/genie-drs/Cargo.toml b/crates/genie-drs/Cargo.toml index 69827fc..bd03e4a 100644 --- a/crates/genie-drs/Cargo.toml +++ b/crates/genie-drs/Cargo.toml @@ -1,19 +1,21 @@ [package] name = "genie-drs" version = "0.2.1" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later" description = "Read .drs archive files from the Genie Engine, used in Age of Empires 1/2 and SWGB" -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" -readme = "README.md" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-drs" +documentation = "https://docs.rs/genie-drs" +repository.workspace = true +readme = "./README.md" exclude = ["*.drs"] [dependencies] -byteorder = "1.3.4" -sorted-vec = "0.5.1" -thiserror = "1.0.21" +byteorder.workspace = true +sorted-vec = "0.8.0" +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-drs/README.md b/crates/genie-drs/README.md index 664e086..04c628f 100644 --- a/crates/genie-drs/README.md +++ b/crates/genie-drs/README.md @@ -1,5 +1,14 @@ # genie-drs +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--drs-blue?style=flat-square&color=blue)](https://docs.rs/genie-drs/) +[![crates.io](https://img.shields.io/crates/v/genie-drs.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-drs) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/crates/genie-drs/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) + +Read .drs archive files from the Genie Engine, used in Age of Empires 1/2 and SWGB + +## About DRS + .drs is the resource archive file format for the Genie Engine, used by Age of Empires 1/2 and Star Wars: Galactic Battlegrounds. .drs files contain tables, each of which contain resources of a single type. Resources are identified by a numeric identifier. @@ -12,7 +21,7 @@ Add to Cargo.toml: ```toml [dependencies] -genie-drs = "^0.1.1" +genie-drs = "^0.2.1" ``` ## Example diff --git a/crates/genie-drs/src/lib.rs b/crates/genie-drs/src/lib.rs index deb641a..6cee7a8 100644 --- a/crates/genie-drs/src/lib.rs +++ b/crates/genie-drs/src/lib.rs @@ -129,7 +129,7 @@ impl From<&[u8]> for ResourceType { fn from(u: &[u8]) -> Self { assert!(u.len() <= 4); let mut bytes = [b' '; 4]; - (&mut bytes[0..u.len()]).copy_from_slice(u); + bytes[0..u.len()].copy_from_slice(u); Self(bytes) } } @@ -171,9 +171,9 @@ impl DRSHeader { #[inline] /// Read a DRS archive header from a `Read`able handle. fn from(source: &mut R) -> Result { - let mut banner_msg = [0 as u8; 40]; - let mut version = [0 as u8; 4]; - let mut password = [0 as u8; 12]; + let mut banner_msg = [0_u8; 40]; + let mut version = [0_u8; 4]; + let mut password = [0_u8; 12]; source.read_exact(&mut banner_msg)?; source.read_exact(&mut version)?; source.read_exact(&mut password)?; @@ -240,7 +240,7 @@ impl DRSTable { /// Read a DRS table header from a `Read`able handle. #[inline] fn from(source: &mut R) -> Result { - let mut resource_type = [0 as u8; 4]; + let mut resource_type = [0_u8; 4]; source.read_exact(&mut resource_type)?; let offset = source.read_u32::()?; let num_resources = source.read_u32::()?; diff --git a/crates/genie-drs/src/write.rs b/crates/genie-drs/src/write.rs index 2d11808..e391027 100644 --- a/crates/genie-drs/src/write.rs +++ b/crates/genie-drs/src/write.rs @@ -97,7 +97,7 @@ where }); for _ in &table.resources { let bytes = data.next().expect("genie-drs bug: mismatch between InMemoryStrategy resources and DRSWriter table data"); - drs.output.write_all(&bytes)?; + drs.output.write_all(bytes)?; } } diff --git a/crates/genie-hki/Cargo.toml b/crates/genie-hki/Cargo.toml index 1eec1d0..f882cce 100644 --- a/crates/genie-hki/Cargo.toml +++ b/crates/genie-hki/Cargo.toml @@ -1,19 +1,22 @@ [package] name = "genie-hki" version = "0.2.1" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read Age of Empires I/II hotkey files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-hki" +documentation = "https://docs.rs/genie-hki" +repository.workspace = true +readme = "./README.md" exclude = ["test/files"] [dependencies] -byteorder = "1.3.4" -flate2 = "1.0.18" +byteorder.workspace = true +flate2.workspace = true genie-lang = { version = "^0.2.0", path = "../genie-lang" } -thiserror = "1.0.21" +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-hki/README.md b/crates/genie-hki/README.md index a494d5f..835003f 100644 --- a/crates/genie-hki/README.md +++ b/crates/genie-hki/README.md @@ -1,14 +1,11 @@ # genie-hki -Read Age of Empires 2 hotkey files. - -## Usage - -See [docs.rs](https://docs.rs/genie-hki) for API documentation. +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--hki-blue?style=flat-square&color=blue)](https://docs.rs/genie-hki/) +[![crates.io](https://img.shields.io/crates/v/genie-hki.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-hki) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) -## License - -[GPL-3.0](../../LICENSE.md) +Read Age of Empires 2 hotkey files. ## Hotkeys Descriptions in Language Files @@ -118,3 +115,7 @@ Unlisted keycodes are blank or whitespace in the hotkey menu. * 253 -> Middle Button * 254 -> Wheel Down * 255 -> Wheel Up + +## License + +[GPL-3.0](../../LICENSE.md) diff --git a/crates/genie-hki/src/lib.rs b/crates/genie-hki/src/lib.rs index 311b15e..1689f26 100644 --- a/crates/genie-hki/src/lib.rs +++ b/crates/genie-hki/src/lib.rs @@ -769,9 +769,9 @@ impl Hotkey { pub(crate) fn write_to(&self, output: &mut W) -> io::Result<()> { output.write_i32::(self.key)?; output.write_i32::(self.string_id)?; - output.write_u8(if self.ctrl { 1 } else { 0 })?; - output.write_u8(if self.alt { 1 } else { 0 })?; - output.write_u8(if self.shift { 1 } else { 0 })?; + output.write_u8(u8::from(self.ctrl))?; + output.write_u8(u8::from(self.alt))?; + output.write_u8(u8::from(self.shift))?; output.write_i8(self.mouse)?; Ok(()) } @@ -788,12 +788,12 @@ impl Hotkey { /// let mut lang_file = LangFile::new(); /// lang_file.insert(StringKey::from(5u32), String::from("A")); /// let hotkey = Hotkey::default().key(65).string_id(5).ctrl(true); - /// assert_eq!("A (5): ctrl-65", hotkey.to_string_lang(&lang_file)); + /// assert_eq!("A (5): ctrl-65", hotkey.get_string_from_lang(&lang_file)); /// /// let default = Hotkey::default(); - /// assert_eq!("-1: 0", default.to_string_lang(&lang_file)); + /// assert_eq!("-1: 0", default.get_string_from_lang(&lang_file)); /// ``` - pub fn to_string_lang(&self, lang_file: &genie_lang::LangFile) -> String { + pub fn get_string_from_lang(&self, lang_file: &genie_lang::LangFile) -> String { let ctrl = if self.ctrl { "ctrl-" } else { "" }; let alt = if self.alt { "ctrl-" } else { "" }; let shift = if self.shift { "ctrl-" } else { "" }; @@ -917,7 +917,7 @@ impl HotkeyGroup { /// Returns a string representation of this hotkey group, using the strings /// from `lang_file` and the group name string key `sk`. - pub fn to_string_lang(&self, lang_file: &LangFile, sk: &StringKey) -> String { + pub fn get_string_from_lang(&self, lang_file: &LangFile, sk: &StringKey) -> String { let group_name = if let Some(name) = lang_file.get(sk) { format!("{} ({}):\n ", name, sk) } else { @@ -926,7 +926,7 @@ impl HotkeyGroup { let hotkeys: Vec = self .hotkeys .iter() - .map(|hki| hki.to_string_lang(&lang_file)) + .map(|hki| hki.get_string_from_lang(lang_file)) .collect(); format!("{}{}", group_name, hotkeys.join("\n ")) } @@ -1136,18 +1136,18 @@ impl HotkeyInfo { } /// Returns a string representation of this `HotkeyInfo` struct using the - /// strings from `lang_file` and the group name sting keys given in `him`. + /// strings from `lang_file` and the group name string keys given in `him`. /// /// # Panics /// /// Panics if the number of hotkeys in any group of this file differs from /// the number of hotkeys given to that group in `him`. - pub fn to_string_lang(&self, lang_file: &LangFile, him: &HotkeyInfoMetadata) -> String { + pub fn get_string_from_lang(&self, lang_file: &LangFile, him: &HotkeyInfoMetadata) -> String { let groups: Vec = self .groups .iter() .zip(him.iter()) - .map(|(grp, sk)| grp.to_string_lang(&lang_file, sk)) + .map(|(grp, sk)| grp.get_string_from_lang(lang_file, sk)) .collect(); format!("Version: {}\n{}", self.version, groups.join("\n")) } diff --git a/crates/genie-lang/Cargo.toml b/crates/genie-lang/Cargo.toml index 4a0551f..d23017d 100644 --- a/crates/genie-lang/Cargo.toml +++ b/crates/genie-lang/Cargo.toml @@ -1,21 +1,24 @@ [package] name = "genie-lang" version = "0.2.1" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read different types of language resource files from Age of Empires 1 and 2." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-lang" +documentation = "https://docs.rs/genie-lang" +repository.workspace = true +readme = "./README.md" exclude = ["test/dlls"] [dependencies] -byteorder = "1.3.4" -encoding_rs = "0.8.24" -encoding_rs_io = "0.1.7" -genie-support = { version = "1.0.0", path = "../genie-support" } -pelite = { version = "0.9.0", default-features = false, features = ["mmap"] } -thiserror = "1.0.21" +byteorder.workspace = true +encoding_rs.workspace = true +encoding_rs_io.workspace = true +genie-support = { version = "2.0.0", path = "../genie-support" } +pelite = { version = "0.9.1", default-features = false, features = ["mmap"] } +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-lang/README.md b/crates/genie-lang/README.md new file mode 100644 index 0000000..da61e1e --- /dev/null +++ b/crates/genie-lang/README.md @@ -0,0 +1,12 @@ +# genie-lang + +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--lang-blue?style=flat-square&color=blue)](https://docs.rs/genie-lang/) +[![crates.io](https://img.shields.io/crates/v/genie-lang.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-lang) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) + +Read different types of language resource files from Age of Empires 1 and 2. + +## License + +[GPL-3.0](../../LICENSE.md) diff --git a/crates/genie-rec/Cargo.toml b/crates/genie-rec/Cargo.toml index 8a2c6aa..983de1d 100644 --- a/crates/genie-rec/Cargo.toml +++ b/crates/genie-rec/Cargo.toml @@ -1,21 +1,32 @@ [package] name = "genie-rec" version = "0.1.1" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read Age of Empires I/II recorded game files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-rec" +documentation = "https://docs.rs/genie-rec" +repository.workspace = true +readme = "./README.md" [dependencies] -arrayvec = "0.7.0" -byteorder = "1.3.4" -flate2 = { version = "1.0.18", features = ["rust_backend"], default-features = false } +arrayvec.workspace = true +byteorder.workspace = true +flate2.workspace = true genie-dat = { version = "0.1.0", path = "../genie-dat" } genie-scx = { version = "4.0.0", path = "../genie-scx" } -genie-support = { version = "1.0.0", path = "../genie-support", features = ["strings"] } -thiserror = "1.0.21" +genie-support = { version = "2.0.0", path = "../genie-support", features = [ + "strings", +] } +thiserror.workspace = true +serde_json = "1.0.64" +nom = "7.1.1" +nom-derive = "0.10.0" +bincode = { version = "1.3.3", features = ["i128"] } +serde = { version = "1.0.145", features = ["derive", "alloc"] } +comfy-table = "6.1.0" [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-rec/README.md b/crates/genie-rec/README.md index 5313fa4..033efdf 100644 --- a/crates/genie-rec/README.md +++ b/crates/genie-rec/README.md @@ -1,10 +1,11 @@ # genie-rec -Age of Empires 2 recorded game file reader (incomplete). - -## Usage +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--rec-blue?style=flat-square&color=blue)](https://docs.rs/genie-rec) +[![crates.io](https://img.shields.io/crates/v/genie-rec.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-rec) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) -See [docs.rs](https://docs.rs/genie-rec) for API documentation. +Age of Empires 2 recorded game file reader (incomplete). ## License diff --git a/crates/genie-rec/examples/SD-AgeIIDE_Replay_181966005.aoe2record b/crates/genie-rec/examples/SD-AgeIIDE_Replay_181966005.aoe2record new file mode 100644 index 0000000..9153151 Binary files /dev/null and b/crates/genie-rec/examples/SD-AgeIIDE_Replay_181966005.aoe2record differ diff --git a/crates/genie-rec/examples/dump_header.rs b/crates/genie-rec/examples/dump_header.rs new file mode 100644 index 0000000..4423395 --- /dev/null +++ b/crates/genie-rec/examples/dump_header.rs @@ -0,0 +1,30 @@ +#![allow(unused_imports)] +#![allow(dead_code)] + +use anyhow::Context; +use genie_rec::actions::Action; +use genie_rec::RecordedGame; +use std::env::args; +use std::fs::File; +use std::io::{stdout, Seek, SeekFrom}; + +fn main() { + dump().context("Failed to dump recording").unwrap(); +} + +#[track_caller] +fn dump() -> Result<(), anyhow::Error> { + let mut args = args(); + // skip executable + args.next(); + let filename = args + .next() + .expect("Please give a filename of a record to dump"); + + let mut f = File::open(filename)?; + let mut r = RecordedGame::new(&mut f)?; + println!("{:?}", r.header()?); + let mut header = r.get_header_deflate()?; + std::io::copy(&mut header, &mut stdout())?; + Ok(()) +} diff --git a/crates/genie-rec/examples/dump_rec.rs b/crates/genie-rec/examples/dump_rec.rs new file mode 100644 index 0000000..a86653d --- /dev/null +++ b/crates/genie-rec/examples/dump_rec.rs @@ -0,0 +1,55 @@ +#![allow(unused_imports)] +#![allow(dead_code)] + +use anyhow::Context; +use genie_rec::actions::Action; +use genie_rec::RecordedGame; +use std::env::args; +use std::fs::File; +use std::io::{Seek, SeekFrom}; + +fn main() { + dump().context("Failed to dump recording").unwrap(); +} + +#[track_caller] +fn dump() -> Result<(), anyhow::Error> { + let mut args = args(); + // skip executable + args.next(); + let filename = args + .next() + .expect("Please give a filename of a record to dump"); + + let mut f = File::open(filename)?; + let mut r = RecordedGame::new(&mut f)?; + println!("version, {}", r.save_version()); + let header = r.get_header_data()?; + std::fs::write(r"header.bin", header)?; + match r.header() { + Ok(_) => {} + Err(err) => { + println!("Failed parsing header: {}", err); + } + } + // for act in r.actions()? { + // match act { + // Ok(Action::Command(command)) => { + // println!("{:#?}", command); + // } + // Ok(Action::Chat(chat)) => { + // println!("{:#?}", chat); + // } + // Ok(Action::Embedded(embedded)) => { + // println!("{:#?}", embedded); + // } + // + // Ok(_) => {} + // Err(err) => { + // println!("Position: {:?}", f.seek(SeekFrom::Current(0))); + // return Err(err.into()); + // } + // } + // } + Ok(()) +} diff --git a/crates/genie-rec/examples/nom_parser.rs b/crates/genie-rec/examples/nom_parser.rs new file mode 100644 index 0000000..26277cc --- /dev/null +++ b/crates/genie-rec/examples/nom_parser.rs @@ -0,0 +1,48 @@ +#![allow(unused_imports)] +#![allow(dead_code)] + +extern crate nom; +use anyhow::Context; +use flate2::bufread::DeflateDecoder; +use genie_rec::actions::Action; +use genie_rec::RecordedGame; +use nom::{ + bytes::complete::{tag, take_while_m_n}, + combinator::map_res, + error::dbg_dmp, + error::Error, + error::FromExternalError, + sequence::tuple, + IResult, +}; +use nom_derive::*; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::BufRead; +use std::io::Read; +use std::io::{stdout, Seek, SeekFrom}; +use std::{env::args, io::BufReader}; + +fn main() { + dump().context("Failed to dump recording").unwrap(); +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct RecordingFile(Vec); + +#[track_caller] +fn dump() -> Result<(), anyhow::Error> { + let file = include_bytes!("SD-AgeIIDE_Replay_181966005.aoe2record"); + let decoded: RecordingFile = bincode::deserialize(&file[..]).unwrap(); + + let mut deflate_buf_reader = DeflateDecoder::new(BufReader::new(decoded.0.as_slice())); + + // for byte in deflate_buf_reader { + // println!("{byte:?}"); + // } + let mut header = vec![]; + deflate_buf_reader.read_to_end(&mut header)?; + std::fs::write(r"header2.bin", header)?; + + Ok(()) +} diff --git a/crates/genie-rec/old.bin b/crates/genie-rec/old.bin new file mode 100644 index 0000000..0f6b837 Binary files /dev/null and b/crates/genie-rec/old.bin differ diff --git a/crates/genie-rec/src/actions.rs b/crates/genie-rec/src/actions.rs index 24574cb..b474bec 100644 --- a/crates/genie-rec/src/actions.rs +++ b/crates/genie-rec/src/actions.rs @@ -1,11 +1,13 @@ //! Player actions executed during a game. -use crate::{ObjectID, PlayerID, Result}; +use crate::{Error, ObjectID, PlayerID, Result}; use arrayvec::ArrayVec; -use byteorder::{ReadBytesExt, WriteBytesExt, LE}; -use genie_support::{f32_neq, read_opt_u32, ReadSkipExt, ReadStringsExt, TechID, UnitTypeID}; +use byteorder::ByteOrder; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt, LE}; +use genie_support::{f32_neq, read_opt_u32, read_str, ReadSkipExt, TechID, UnitTypeID}; +use serde_json::Value; use std::convert::TryInto; -use std::io::{Read, Write}; +use std::io::{Cursor, Read, Write}; /// A location with an X and Y coordinate. pub type Location2 = (f32, f32); @@ -66,11 +68,12 @@ impl Default for ObjectsList { impl ObjectsList { /// Read a list of objects from an input stream. - pub fn read_from(mut input: impl Read, count: i32) -> Result { - if count < 0xFF { + pub fn read_from(mut input: impl Read, count: i8) -> Result { + if count != -1 { let mut list = vec![]; for _ in 0..count { - list.push(input.read_i32::()?.try_into().unwrap()); + let id = input.read_u32::()?; + list.push(id.into()); } Ok(ObjectsList::List(list)) } else { @@ -120,11 +123,14 @@ pub struct OrderCommand { impl OrderCommand { /// Read an Order command from an input stream. pub fn read_from(mut input: impl Read) -> Result { - let mut command = Self::default(); - command.player_id = input.read_u8()?.into(); + let mut command = OrderCommand { + player_id: input.read_u8()?.into(), + ..Default::default() + }; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; - let selected_count = input.read_i32::()?; + let selected_count = input.read_i8()?; + input.skip(3)?; command.location = (input.read_f32::()?, input.read_f32::()?); command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) @@ -159,7 +165,7 @@ impl StopCommand { pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); let selected_count = input.read_i8()?; - command.objects = ObjectsList::read_from(input, selected_count as i32)?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } @@ -191,7 +197,7 @@ impl WorkCommand { let selected_count = input.read_i8()?; input.skip(3)?; command.location = (input.read_f32::()?, input.read_f32::()?); - command.objects = ObjectsList::read_from(input, selected_count as i32)?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } @@ -217,6 +223,8 @@ pub struct MoveCommand { pub target_id: Option, /// The target location of this command. pub location: Location2, + /// ??? + pub flags: Option<[bool; 8]>, /// The objects being tasked. pub objects: ObjectsList, } @@ -224,14 +232,35 @@ pub struct MoveCommand { impl MoveCommand { /// Read a Move command from an input stream. pub fn read_from(mut input: impl Read) -> Result { - let mut command = Self::default(); - command.player_id = input.read_u8()?.into(); + let mut command = MoveCommand { + player_id: input.read_u8()?.into(), + ..Default::default() + }; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; let selected_count = input.read_i8()?; input.skip(3)?; command.location = (input.read_f32::()?, input.read_f32::()?); - command.objects = ObjectsList::read_from(input, selected_count as i32)?; + + let mut buffer = vec![]; + input.read_to_end(&mut buffer)?; + + let size_with_flags = 8 + (selected_count.max(0) as usize * 4); + let mut buffer_cursor = Cursor::new(&buffer); + + command.flags = if buffer.len() == size_with_flags { + let mut flags = [false; 8]; + + for flag in &mut flags { + *flag = buffer_cursor.read_u8()? == 1 + } + + Some(flags) + } else { + None + }; + + command.objects = ObjectsList::read_from(buffer_cursor, selected_count)?; Ok(command) } @@ -247,6 +276,7 @@ impl MoveCommand { Ok(()) } } + /// A command that instantly places a unit type at a given location. /// /// Typically used for cheats and the like. @@ -345,7 +375,7 @@ pub struct AIOrderCommand { impl AIOrderCommand { pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_i8()?); + let selected_count = input.read_i8()?; command.player_id = input.read_u8()?.into(); command.issuer = input.read_u8()?.into(); let object_id = input.read_u32::()?; @@ -398,8 +428,8 @@ impl AIOrderCommand { output.write_f32::(self.target_location.1)?; output.write_f32::(self.target_location.2)?; output.write_f32::(self.range)?; - output.write_u8(if self.immediate { 1 } else { 0 })?; - output.write_u8(if self.add_to_front { 1 } else { 0 })?; + output.write_u8(u8::from(self.immediate))?; + output.write_u8(u8::from(self.add_to_front))?; output.write_all(&[0, 0])?; if self.objects.len() > 1 { self.objects.write_to(output)?; @@ -436,7 +466,7 @@ impl ResignCommand { pub fn write_to(&self, output: &mut W) -> Result<()> { output.write_u8(self.player_id.into())?; output.write_u8(self.comm_player_id.into())?; - output.write_u8(if self.dropped { 1 } else { 0 })?; + output.write_u8(u8::from(self.dropped))?; Ok(()) } } @@ -451,13 +481,13 @@ pub struct GroupWaypointCommand { impl GroupWaypointCommand { pub fn read_from(mut input: impl Read) -> Result { let player_id = input.read_u8()?.into(); - let num_units = input.read_u8()?; + let num_units = input.read_i8()?; let x = input.read_u8()?; let y = input.read_u8()?; Ok(Self { player_id, location: (x, y), - objects: ObjectsList::read_from(input, i32::from(num_units))?, + objects: ObjectsList::read_from(input, num_units)?, }) } @@ -483,9 +513,9 @@ pub struct UnitAIStateCommand { impl UnitAIStateCommand { /// Read a UnitAIState command from an input stream. pub fn read_from(mut input: impl Read) -> Result { - let selected_count = input.read_u8()?; + let selected_count = input.read_i8()?; let state = input.read_i8()?; - let objects = ObjectsList::read_from(input, i32::from(selected_count))?; + let objects = ObjectsList::read_from(input, selected_count)?; Ok(Self { state, objects }) } @@ -511,7 +541,7 @@ impl GuardCommand { /// Read a Guard command from an input stream. pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_u8()?); + let selected_count = input.read_i8()?; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; command.objects = ObjectsList::read_from(input, selected_count)?; @@ -545,7 +575,7 @@ impl FollowCommand { /// Read a Follow command from an input stream. pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_u8()?); + let selected_count = input.read_i8()?; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; command.objects = ObjectsList::read_from(input, selected_count)?; @@ -592,7 +622,7 @@ impl PatrolCommand { .waypoints .try_extend_from_slice(&raw_waypoints[0..usize::from(waypoint_count)]) .unwrap(); - command.objects = ObjectsList::read_from(input, i32::from(selected_count))?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } @@ -629,7 +659,7 @@ impl FormFormationCommand { command.player_id = input.read_u8()?.into(); let _padding = input.read_u8()?; command.formation_type = input.read_i32::()?; - command.objects = ObjectsList::read_from(input, i32::from(selected_count))?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } @@ -812,7 +842,7 @@ impl BuildCommand { command.unique_id = read_opt_u32(&mut input)?; command.frame = input.read_u8()?; input.skip(3)?; - command.builders = ObjectsList::read_from(input, i32::from(selected_count))?; + command.builders = ObjectsList::read_from(input, selected_count)?; Ok(command) } } @@ -971,19 +1001,40 @@ impl GameCommand { #[derive(Debug, Default, Clone)] pub struct BuildWallCommand { pub player_id: PlayerID, - pub start: (u8, u8), - pub end: (u8, u8), + pub start: (u16, u16), + pub end: (u16, u16), pub unit_type_id: UnitTypeID, + pub flags: Option, pub builders: ObjectsList, } impl BuildWallCommand { fn read_from(mut input: impl Read) -> Result { + let mut data = vec![]; + input.read_to_end(&mut data)?; + let data_size = data.len(); + let mut input = Cursor::new(&data); + let selected_count = input.read_i8()?; + + // TODO: Check for a more elegant way? I would love to have parser state here + // But requires "full rewrite" + let is_de = data_size - 15 - (selected_count as usize * 4) == 8; + let player_id = input.read_u8()?.into(); - let start = (input.read_u8()?, input.read_u8()?); - let end = (input.read_u8()?, input.read_u8()?); - let _padding = input.read_u8()?; + + let (start, end) = if is_de { + let _padding = input.read_u8()?; + let start = (input.read_u16::()?, input.read_u16::()?); + let end = (input.read_u16::()?, input.read_u16::()?); + (start, end) + } else { + let start = (input.read_u8()?.into(), input.read_u8()?.into()); + let end = (input.read_u8()?.into(), input.read_u8()?.into()); + let _padding = input.read_u8()?; + (start, end) + }; + let unit_type_id = input.read_u16::()?.into(); let _padding = input.read_u16::()?; assert_eq!( @@ -991,6 +1042,9 @@ impl BuildWallCommand { 0xFFFF_FFFF, "check out what this is for" ); + + let flags = is_de.then(|| input.read_u32::()).transpose()?; + let builders = if selected_count == -1 { ObjectsList::SameAsLast } else { @@ -1006,6 +1060,7 @@ impl BuildWallCommand { start, end, unit_type_id, + flags, builders, }) } @@ -1052,10 +1107,10 @@ impl AttackGroundCommand { /// Read a AttackGround command from an input stream. pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_i8()?); + let selected_count = input.read_i8()?; input.skip(2)?; command.location = (input.read_f32::()?, input.read_f32::()?); - command.objects = ObjectsList::read_from(input, selected_count as i32)?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } @@ -1083,7 +1138,7 @@ impl RepairCommand { /// Read a Repair command from an input stream. pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_u8()?); + let selected_count = input.read_i8()?; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; command.repairers = ObjectsList::read_from(input, selected_count)?; @@ -1128,7 +1183,7 @@ impl UngarrisonCommand { command.ungarrison_type = input.read_i8()?; input.skip(3)?; command.unit_type_id = read_opt_u32(&mut input)?; - command.objects = ObjectsList::read_from(input, i32::from(selected_count))?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } } @@ -1194,7 +1249,7 @@ impl UnitOrderCommand { None }; command.unique_id = read_opt_u32(&mut input)?; - command.objects = ObjectsList::read_from(input, i32::from(selected_count))?; + command.objects = ObjectsList::read_from(input, selected_count)?; Ok(command) } } @@ -1245,7 +1300,7 @@ pub struct SetGatherPointCommand { impl SetGatherPointCommand { pub fn read_from(mut input: impl Read) -> Result { let mut command = Self::default(); - let selected_count = i32::from(input.read_i8()?); + let selected_count = input.read_i8()?; input.skip(2)?; command.target_id = read_opt_u32(&mut input)?; command.target_type_id = match input.read_u16::()? { @@ -1285,12 +1340,12 @@ macro_rules! buy_sell_impl { ($name:ident) => { impl $name { pub fn read_from(mut input: impl Read) -> Result { - let mut command = Self::default(); - command.player_id = input.read_u8()?.into(); - command.resource = input.read_u8()?; - command.amount = input.read_i8()?; - command.market_id = input.read_u32::()?.into(); - Ok(command) + Ok(Self { + player_id: input.read_u8()?.into(), + resource: input.read_u8()?, + amount: input.read_i8()?, + market_id: input.read_u32::()?.into(), + }) } pub fn write_to(&self, output: &mut W) -> Result<()> { @@ -1365,6 +1420,37 @@ impl BackToWorkCommand { } } +/// AoE2: DE uses a different queue command that combines multi and single queue +#[derive(Debug, Default, Clone)] +pub struct DEQueueCommand { + pub player_id: PlayerID, + pub building_type: UnitTypeID, + pub unit_type: UnitTypeID, + pub queue_amount: u8, + pub buildings: ObjectsList, +} + +impl DEQueueCommand { + pub fn read_from(mut input: impl Read) -> Result { + let player_id = input.read_u8()?.into(); + let building_type = input.read_u16::()?.into(); + let selected = input.read_i8()?; + let _padding = input.read_u8()?; + let unit_type = input.read_u16::()?.into(); + let queue_amount = input.read_u8()?; + let _padding = input.read_u8()?; + let buildings = ObjectsList::read_from(input, selected)?; + + Ok(DEQueueCommand { + player_id, + building_type, + unit_type, + queue_amount, + buildings, + }) + } +} + /// A player command. #[derive(Debug, Clone)] pub enum Command { @@ -1400,14 +1486,19 @@ pub enum Command { BuyResource(BuyResourceCommand), Unknown7F(Unknown7FCommand), BackToWork(BackToWorkCommand), + DEQueue(DEQueueCommand), + Unknown(u8, Vec), } impl Command { pub fn read_from(input: &mut R) -> Result { let len = input.read_u32::()?; + let mut data = vec![0u8; (len - 1) as usize]; + let command_id = input.read_u8()?; + input.read_exact(&mut data)?; + let mut cursor = Cursor::new(&data); - let mut cursor = input.by_ref().take(len.into()); - let command = match cursor.read_u8()? { + let command = match command_id { 0x00 => OrderCommand::read_from(&mut cursor).map(Command::Order), 0x01 => StopCommand::read_from(&mut cursor).map(Command::Stop), 0x02 => WorkCommand::read_from(&mut cursor).map(Command::Work), @@ -1440,10 +1531,9 @@ impl Command { 0x7b => BuyResourceCommand::read_from(&mut cursor).map(Command::BuyResource), 0x7f => Unknown7FCommand::read_from(&mut cursor).map(Command::Unknown7F), 0x80 => BackToWorkCommand::read_from(&mut cursor).map(Command::BackToWork), - id => panic!("unsupported command type {:#x}", id), + 0x81 => DEQueueCommand::read_from(&mut cursor).map(Command::DEQueue), + id => Ok(Command::Unknown(id, data)), }; - // Consume any excess bytes. - std::io::copy(&mut cursor, &mut std::io::sink())?; let _world_time = input.read_u32::()?; command @@ -1459,8 +1549,10 @@ pub struct Time { impl Time { pub fn read_from(input: &mut R) -> Result { - let mut time = Self::default(); - time.time = input.read_u32::()?; + let mut time = Time { + time: input.read_u32::()?, + ..Default::default() + }; let is_old_record = false; if is_old_record { time.old_world_time = input.read_u32::()?; @@ -1570,13 +1662,74 @@ impl Meta { #[derive(Debug, Clone)] pub struct Chat { message: String, + de_info: Option, +} + +/// DE introduces chat, but JSON, ye.. +#[derive(Debug, Clone)] +pub struct ChatDeInfo { + player: PlayerID, + // since it's JSON, we kinda have to assume it's f64, + // but let's assume the folks at FE don't make channels on + channel: u32, + message: String, } impl Chat { pub fn read_from(input: &mut R) -> Result { assert_eq!(input.read_i32::()?, -1); - let message = input.read_u32_length_prefixed_str()?.unwrap_or_default(); - Ok(Self { message }) + let length = input.read_u32::()?; + let mut buffer = vec![0u8; length as usize]; + input.read_exact(&mut buffer)?; + Self::parse_from_buffer(buffer) + } + + pub fn parse_from_buffer>(buffer: T) -> Result { + let buffer = buffer.as_ref(); + if buffer.is_empty() { + return Ok(Chat { + message: "".to_string(), + de_info: None, + }); + } + + // Both latin-1 and UTF-8 have '{' on the same codepoint + // old aoe messages _can't_ start with { + if buffer[0] != b'{' { + let message = read_str(buffer)?.unwrap_or_default(); + Ok(Self { + message, + de_info: None, + }) + } else { + let mut value: serde_json::Map = serde_json::from_slice(buffer)?; + let player = (value + .remove("player") + .and_then(|x| x.as_u64()) + .ok_or(Error::ParseDEChatMessageError("player"))? as u8) + .into(); + let channel = value + .remove("channel") + .and_then(|x| x.as_u64()) + .ok_or(Error::ParseDEChatMessageError("channel"))? as u32; + let message = value + .remove("message") + .and_then(|x| x.as_str().map(str::to_string)) + .ok_or(Error::ParseDEChatMessageError("message"))?; + let fallback_message = value + .remove("messageAGP") + .and_then(|x| x.as_str().map(str::to_string)) + .ok_or(Error::ParseDEChatMessageError("messageAGP"))?; + + Ok(Self { + message: fallback_message, + de_info: Some(ChatDeInfo { + player, + channel, + message, + }), + }) + } } } @@ -1588,4 +1741,31 @@ pub enum Action { Sync(Sync), ViewLock(ViewLock), Chat(Chat), + Embedded(EmbeddedAction), +} + +#[derive(Debug, Clone)] +pub enum EmbeddedAction { + Chat(Chat), + // TODO, I think it's _just_ a header? + SavedChapter, + // TODO: aoc-mgz says something about it being a partial action? + Other, + // ??? + Unknown, +} + +impl EmbeddedAction { + pub fn from_buffer>(buffer: T) -> Result { + let data = buffer.as_ref(); + let op: u32 = LittleEndian::read_u32(&data[..4]); + let action = match op { + 0 => EmbeddedAction::SavedChapter, + 9024 => EmbeddedAction::Chat(Chat::parse_from_buffer(&data[4..])?), + 65535 => EmbeddedAction::Other, + _ => EmbeddedAction::Unknown, + }; + + Ok(action) + } } diff --git a/crates/genie-rec/src/ai.rs b/crates/genie-rec/src/ai.rs index 0576219..4fc1fec 100644 --- a/crates/genie-rec/src/ai.rs +++ b/crates/genie-rec/src/ai.rs @@ -1,5 +1,7 @@ //! Read and write player AI state. +use crate::element::{ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::RecordingHeaderReader; use crate::unit::Waypoint; use crate::{ObjectID, PlayerID, Result}; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; @@ -13,8 +15,8 @@ pub struct MainAI { pub objects: Vec, } -impl MainAI { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for MainAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let num_objects = input.read_u32::()?; let mut objects = vec![]; for _ in 0..num_objects { @@ -42,11 +44,14 @@ pub struct BuildItem { pub is_forward: bool, } -impl BuildItem { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut item = Self::default(); - item.name = input.read_u32_length_prefixed_str()?; - item.type_id = input.read_u32::()?; +impl ReadableHeaderElement for BuildItem { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut item = BuildItem { + name: input.read_u32_length_prefixed_str()?, + type_id: input.read_u32::()?, + ..Default::default() + }; + let _a2 = input.read_u32::()?; item.game_id = input.read_u32::()?; let _v21 = input.read_u32::()?; @@ -70,7 +75,7 @@ impl BuildItem { let _v12 = input.read_u32::()?; let _v29 = input.read_u32::()?; let _v31 = input.read_u8()?; - item.is_forward = if version > 10.87 { + item.is_forward = if input.version() > 10.87 { input.read_u32::()? != 0 } else { false @@ -91,15 +96,15 @@ pub struct BuildAI { pub queued_unit_count: u32, } -impl BuildAI { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for BuildAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut ai = Self::default(); let build_list_len = input.read_u32::()?; ai.build_list_name = input.read_u32_length_prefixed_str()?; ai.last_build_item_requested = input.read_u32_length_prefixed_str()?; ai.current_build_item_requested = input.read_u32_length_prefixed_str()?; ai.next_build_item_requested = input.read_u32_length_prefixed_str()?; - let _items_into_build_queue = if version > 11.02 { + let _items_into_build_queue = if input.version() > 11.02 { input.read_u32::()? } else { build_list_len @@ -107,8 +112,7 @@ impl BuildAI { let num_build_items = input.read_u32::()?; for _ in 0..num_build_items { - ai.build_queue - .push(BuildItem::read_from(&mut input, version)?); + ai.build_queue.push(BuildItem::read_from(input)?); } for _ in 0..600 { @@ -135,11 +139,14 @@ pub struct ConstructionItem { pub built: u32, } -impl ConstructionItem { - pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut item = Self::default(); - item.name = input.read_u32_length_prefixed_str()?; - item.type_id = input.read_u32::()?; +impl ReadableHeaderElement for ConstructionItem { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut item = ConstructionItem { + name: input.read_u32_length_prefixed_str()?, + type_id: input.read_u32::()?, + ..Default::default() + }; + let _a2 = input.read_u32::()?; let _v27 = input.read_u32::()?; item.x = input.read_f32::()?; @@ -155,6 +162,7 @@ impl ConstructionItem { Ok(item) } } + #[derive(Debug, Default, Clone)] pub struct ConstructionAI { pub plan_name: Option, @@ -164,8 +172,8 @@ pub struct ConstructionAI { pub random_construction_lots: Vec, } -impl ConstructionAI { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for ConstructionAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut ai = Self::default(); let num_lots = input.read_u32::()?; ai.plan_name = input.read_u32_length_prefixed_str()?; @@ -177,12 +185,12 @@ impl ConstructionAI { ai.map_size = (input.read_u32::()?, input.read_u32::()?); for _ in 0..num_lots { ai.construction_lots - .push(ConstructionItem::read_from(&mut input, version)?); + .push(ConstructionItem::read_from(input)?); } let num_lots = input.read_u32::()?; for _ in 0..num_lots { ai.random_construction_lots - .push(ConstructionItem::read_from(&mut input, version)?); + .push(ConstructionItem::read_from(input)?); } Ok(ai) } @@ -197,8 +205,8 @@ pub struct DiplomacyAI { pub changeable: [u8; 10], } -impl DiplomacyAI { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for DiplomacyAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut ai = Self::default(); for i in 0..10 { ai.dislike[i] = input.read_u32::()?; @@ -214,8 +222,8 @@ pub struct EmotionalAI { pub state: [u32; 6], } -impl EmotionalAI { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for EmotionalAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut ai = Self::default(); input.read_u32_into::(&mut ai.state)?; Ok(ai) @@ -239,23 +247,23 @@ pub struct ImportantObjectMemory { pub is_garrisoned: u32, } -impl ImportantObjectMemory { - pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut object = Self::default(); - object.id = read_opt_u32(&mut input)?; - object.unit_type_id = read_opt_u16(&mut input)?; - object.unit_class = read_opt_u16(&mut input)?; - object.location = (input.read_u8()?, input.read_u8()?, input.read_u8()?); - object.owner = input.read_u8()?.into(); - object.hit_points = input.read_u16::()?; - object.attack_attempts = input.read_u32::()?; - object.kills = input.read_u8()?; - object.damage_capability = input.read_f32::()?; - object.rate_of_fire = input.read_f32::()?; - object.range = input.read_f32::()?; - object.time_seen = read_opt_u32(&mut input)?; - object.is_garrisoned = input.read_u32::()?; - Ok(object) +impl ReadableHeaderElement for ImportantObjectMemory { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(ImportantObjectMemory { + id: read_opt_u32(input)?, + unit_type_id: read_opt_u16(input)?, + unit_class: read_opt_u16(input)?, + location: (input.read_u8()?, input.read_u8()?, input.read_u8()?), + owner: input.read_u8()?.into(), + hit_points: input.read_u16::()?, + attack_attempts: input.read_u32::()?, + kills: input.read_u8()?, + damage_capability: input.read_f32::()?, + rate_of_fire: input.read_f32::()?, + range: input.read_f32::()?, + time_seen: read_opt_u32(input)?, + is_garrisoned: input.read_u32::()?, + }) } } @@ -266,9 +274,9 @@ pub struct BuildingLot { pub location: (u8, u8), } -impl BuildingLot { - pub fn read_from(mut input: impl Read) -> Result { - let unit_type_id = read_opt_u32(&mut input)?; +impl ReadableHeaderElement for BuildingLot { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let unit_type_id = read_opt_u32(input)?; let status = input.read_u8()?; let x = input.read_u8()?; let y = input.read_u8()?; @@ -293,21 +301,21 @@ pub struct WallLine { pub line_end: (u32, u32), } -impl WallLine { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for WallLine { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut line = Self::default(); - if version >= 10.78 { + if input.version() >= 10.78 { line.line_type = input.read_u32::()?; } - line.wall_type = read_opt_u32(&mut input)?; - if version >= 10.78 { + line.wall_type = read_opt_u32(input)?; + if input.version() >= 10.78 { line.gate_count = input.read_u32::()?; } line.segment_count = input.read_u32::()?; - if version >= 11.34 { + if input.version() >= 11.34 { line.invisible_segment_count = input.read_u32::()?; } - if version >= 11.29 { + if input.version() >= 11.29 { line.unfinished_segment_count = input.read_u32::()?; } line.line_start = (input.read_u32::()?, input.read_u32::()?); @@ -330,25 +338,27 @@ pub struct PerimeterWall { pub next_segment_to_refresh: u32, } -impl PerimeterWall { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut wall = Self::default(); - wall.enabled = if version >= 11.22 { - input.read_u32::()? != 0 - } else { - true +impl ReadableHeaderElement for PerimeterWall { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut wall = PerimeterWall { + enabled: if input.version() >= 11.22 { + input.read_u32::()? != 0 + } else { + true + }, + ..Default::default() }; let num_lines = input.read_u32::()?; - if version >= 11.20 { + if input.version() >= 11.20 { wall.gate_count = input.read_u32::()?; wall.percentage_complete = input.read_u32::()?; wall.segment_count = input.read_u32::()?; - if version >= 11.34 { + if input.version() >= 11.34 { wall.invisible_segment_count = input.read_u32::()?; } wall.unfinished_segment_count = input.read_u32::()?; } - if version >= 11.29 { + if input.version() >= 11.29 { wall.next_line_to_refresh = input.read_u32::()?; wall.next_segment_to_refresh = input.read_u32::()?; } @@ -356,7 +366,7 @@ impl PerimeterWall { wall.lines = { let mut list = vec![]; for _ in 0..num_lines { - list.push(WallLine::read_from(&mut input, version)?); + list.push(WallLine::read_from(input)?); } list }; @@ -381,29 +391,31 @@ pub struct AttackMemory { pub play: Option, } -impl AttackMemory { - pub fn read_from(mut input: impl Read) -> Result { - let mut mem = Self::default(); - mem.id = read_opt_u32(&mut input)?; - mem.typ = input.read_u8()?; - mem.min_x = input.read_u8()?; - mem.min_y = input.read_u8()?; - mem.max_x = input.read_u8()?; - mem.max_y = input.read_u8()?; - mem.attacking_owner = match input.read_i8()? { - -1 => None, - id => Some(id.try_into().unwrap()), - }; - mem.target_owner = match input.read_i8()? { - -1 => None, - id => Some(id.try_into().unwrap()), +impl ReadableHeaderElement for AttackMemory { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut mem = AttackMemory { + id: read_opt_u32(input)?, + typ: input.read_u8()?, + min_x: input.read_u8()?, + min_y: input.read_u8()?, + max_x: input.read_u8()?, + max_y: input.read_u8()?, + attacking_owner: match input.read_i8()? { + -1 => None, + id => Some(id.try_into().unwrap()), + }, + target_owner: match input.read_i8()? { + -1 => None, + id => Some(id.try_into().unwrap()), + }, + ..Default::default() }; input.skip(1)?; mem.kills = input.read_u16::()?; mem.success = input.read_u8()? != 0; input.skip(1)?; - mem.timestamp = read_opt_u32(&mut input)?; - mem.play = read_opt_u32(&mut input)?; + mem.timestamp = read_opt_u32(input)?; + mem.play = read_opt_u32(input)?; Ok(mem) } } @@ -422,20 +434,23 @@ pub struct ResourceMemory { pub attacked_time: Option, } -impl ResourceMemory { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut mem = Self::default(); - mem.id = input.read_u32::()?.into(); - mem.location = (input.read_u8()?, input.read_u8()?); - mem.gather_attempts = input.read_u8()?; - mem.gather = input.read_u32::()?; - mem.valid = input.read_u8()? != 0; - mem.gone = input.read_u8()? != 0; - mem.drop_distance = input.read_u8()?; - mem.resource_type = input.read_u8()?; - mem.dropsite_id = input.read_u32::()?.into(); - if version >= 10.91 { - mem.attacked_time = read_opt_u32(&mut input)?; +impl ReadableHeaderElement for ResourceMemory { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut mem = ResourceMemory { + id: input.read_u32::()?.into(), + location: (input.read_u8()?, input.read_u8()?), + gather_attempts: input.read_u8()?, + gather: input.read_u32::()?, + valid: input.read_u8()? != 0, + gone: input.read_u8()? != 0, + drop_distance: input.read_u8()?, + resource_type: input.read_u8()?, + dropsite_id: input.read_u32::()?.into(), + ..Default::default() + }; + + if input.version() >= 10.91 { + mem.attacked_time = read_opt_u32(input)?; } Ok(mem) } @@ -450,8 +465,8 @@ pub struct InfluenceMap { pub values: Vec, } -impl InfluenceMap { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for InfluenceMap { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut map = Self::default(); map.width = input.read_u32::()?; map.height = input.read_u32::()?; @@ -470,8 +485,8 @@ pub struct QuadrantLog { pub attacks_by_us: u32, } -impl QuadrantLog { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for QuadrantLog { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let explored_tiles = input.read_u32::()?; let attacks_on_us = input.read_u32::()?; let attacks_by_us = input.read_u32::()?; @@ -506,9 +521,9 @@ pub struct InformationAI { pub quadrant_log: [QuadrantLog; 4], } -impl InformationAI { +impl ReadableHeaderElement for InformationAI { #[allow(clippy::cognitive_complexity)] - pub fn read_from(mut input: impl Read, version: f32) -> Result { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut ai = Self::default(); for _ in 0..4096 { let _garbage = input.read_u32::()?; @@ -547,7 +562,7 @@ impl InformationAI { let max_important_object_memory = input.read_u32::()?; let mut important_objects = vec![]; for _ in 0..max_important_object_memory { - let important_object = ImportantObjectMemory::read_from(&mut input, version)?; + let important_object = ImportantObjectMemory::read_from(input)?; if important_object.id.is_none() { continue; } @@ -560,7 +575,7 @@ impl InformationAI { let len = input.read_u32::()?; let mut lots = vec![]; for _ in 0..len { - let lot = BuildingLot::read_from(&mut input)?; + let lot = BuildingLot::read_from(input)?; if lot.unit_type_id.is_none() { continue; } @@ -570,15 +585,15 @@ impl InformationAI { }; ai.perimeter_walls = ( - PerimeterWall::read_from(&mut input, version)?, - PerimeterWall::read_from(&mut input, version)?, + PerimeterWall::read_from(input)?, + PerimeterWall::read_from(input)?, ); ai.attack_memories = { let len = input.read_u32::()?; let mut attacks = vec![]; for _ in 0..len { - let attack = AttackMemory::read_from(&mut input)?; + let attack = AttackMemory::read_from(input)?; // if attack.unit_type_id.is_none() { // continue; // } @@ -587,17 +602,17 @@ impl InformationAI { attacks }; - ai.important_object_ids = read_id_list(&mut input)?; - ai.important_unit_ids = read_id_list(&mut input)?; - ai.important_misc_ids = read_id_list(&mut input)?; - ai.items_to_defend = read_id_list(&mut input)?; - ai.player_buildings = read_id_list(&mut input)?; - ai.player_objects = read_id_list(&mut input)?; + ai.important_object_ids = read_id_list(input)?; + ai.important_unit_ids = read_id_list(input)?; + ai.important_misc_ids = read_id_list(input)?; + ai.items_to_defend = read_id_list(input)?; + ai.player_buildings = read_id_list(input)?; + ai.player_objects = read_id_list(input)?; ai.object_counts = { - let num_counts = if version < 11.51 { + let num_counts = if input.version() < 11.51 { 750 - } else if version < 11.65 { + } else if input.version() < 11.65 { 850 } else { 900 @@ -610,26 +625,26 @@ impl InformationAI { let _building_count = input.read_u32::()?; - ai.path_map = InfluenceMap::read_from(&mut input)?; + ai.path_map = InfluenceMap::read_from(input)?; let _last_wall_position = (input.read_i32::()?, input.read_i32::()?); let _last_wall_position_2 = (input.read_i32::()?, input.read_i32::()?); - if version < 10.78 { + if input.version() < 10.78 { input.skip(4 + 4 * 16)?; } let _save_learn_information = input.read_u32::()? != 0; let _learn_path = input.read_u32_length_prefixed_str()?; - if version < 11.25 { + if input.version() < 11.25 { input.skip(0xFF)?; } ai.quadrant_log = [ - QuadrantLog::read_from(&mut input)?, - QuadrantLog::read_from(&mut input)?, - QuadrantLog::read_from(&mut input)?, - QuadrantLog::read_from(&mut input)?, + QuadrantLog::read_from(input)?, + QuadrantLog::read_from(input)?, + QuadrantLog::read_from(input)?, + QuadrantLog::read_from(input)?, ]; let _max_resources = [ @@ -650,7 +665,7 @@ impl InformationAI { for (list, &num) in resources.iter_mut().zip(num_resources.iter()) { list.reserve(num as usize); for _ in 0..num { - list.push(ResourceMemory::read_from(&mut input, version)?); + list.push(ResourceMemory::read_from(input)?); } } resources @@ -679,18 +694,18 @@ impl InformationAI { ]; let _found_forest_tiles = input.read_u32::()?; - if version < 10.85 { + if input.version() < 10.85 { input.skip(64_000)?; } - if version >= 10.90 { + if input.version() >= 10.90 { let mut relics_victory = [0; 9]; input.read_exact(&mut relics_victory)?; let mut wonder_victory = [0; 9]; input.read_exact(&mut wonder_victory)?; } - if version >= 10.94 { + if input.version() >= 10.94 { let should_farm = input.read_u32::()?; let have_seen_forage = input.read_u32::()?; let have_seen_gold = input.read_u32::()?; @@ -702,28 +717,28 @@ impl InformationAI { have_seen_stone, ); } - if version >= 10.95 { + if input.version() >= 10.95 { let have_seen_forest = input.read_u32::()?; dbg!(have_seen_forest); } - if version > 10.99 { + if input.version() > 10.99 { let last_player_count_refresh_time = input.read_u32::()?; dbg!(last_player_count_refresh_time); } - let player_unit_counts_size = if version >= 11.51 { 120 } else { 102 }; + let player_unit_counts_size = if input.version() >= 11.51 { 120 } else { 102 }; let mut player_unit_counts = vec![vec![0; player_unit_counts_size as usize]; 8]; for unit_counts in player_unit_counts.iter_mut() { input.read_u32_into::(unit_counts)?; } - if version >= 11.09 { + if input.version() >= 11.09 { let mut player_total_building_counts = [0; 8]; input.read_u32_into::(&mut player_total_building_counts)?; let mut player_real_total_building_counts = [0; 8]; - if version >= 11.21 { + if input.version() >= 11.21 { input.read_u32_into::(&mut player_real_total_building_counts)?; } @@ -747,13 +762,15 @@ pub struct ResourceAI { pub num_resources: u32, } -impl ResourceAI { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ResourceAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let num_resources = input.read_u32::()?; Ok(Self { num_resources }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for ResourceAI { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u32::(self.num_resources)?; Ok(()) } @@ -777,32 +794,28 @@ pub struct StrategyAI { pub expert_list_id: Option, } -impl StrategyAI { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut ai = Self::default(); - ai.current_victory_condition = input.read_u32::()?; - ai.target_id = input.read_u32::()?; - ai.second_target_id = input.read_u32::()?; - ai.second_target_type = input.read_u32::()?; - ai.target_point = Waypoint::read_from(&mut input)?; - ai.target_point_2 = Waypoint::read_from(&mut input)?; - ai.target_attribute = input.read_u32::()?; - ai.target_number = input.read_u32::()?; - ai.victory_condition_change_timeout = input.read_u32::()?; - ai.ruleset_name = input.read_u32_length_prefixed_str()?; - - ai.vc_ruleset = read_id_list(&mut input)?; - ai.executing_rules = read_id_list(&mut input)?; - ai.idle_rules = read_id_list(&mut input)?; - if version >= 9.71 { - ai.expert_list_id = Some(input.read_u32::()?); - } - - Ok(ai) - } - - pub fn write_to(&self, _output: impl Write) -> Result<()> { - todo!() +impl ReadableHeaderElement for StrategyAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(StrategyAI { + current_victory_condition: input.read_u32::()?, + target_id: input.read_u32::()?, + second_target_id: input.read_u32::()?, + second_target_type: input.read_u32::()?, + target_point: Waypoint::read_from(input)?, + target_point_2: Waypoint::read_from(input)?, + target_attribute: input.read_u32::()?, + target_number: input.read_u32::()?, + victory_condition_change_timeout: input.read_u32::()?, + ruleset_name: input.read_u32_length_prefixed_str()?, + vc_ruleset: read_id_list(input)?, + executing_rules: read_id_list(input)?, + idle_rules: read_id_list(input)?, + expert_list_id: if input.version() >= 9.71 { + Some(input.read_u32::()?) + } else { + None + }, + }) } } @@ -831,32 +844,29 @@ pub struct TacticalAI { pub groups: Vec<()>, } -impl TacticalAI { - pub fn read_from(mut input: impl Read, _version: f32) -> Result { - let mut ai = Self::default(); - - ai.civilians = read_id_list(&mut input)?; - ai.civilian_explorers = read_id_list(&mut input)?; +impl ReadableHeaderElement for TacticalAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut ai = TacticalAI { + civilians: read_id_list(input)?, + civilian_explorers: read_id_list(input)?, + ..Default::default() + }; let _num_gatherers = input.read_u32::()?; let _desired_num_gatherers = input.read_u32::()?; - // more stuff here - - ai.soldiers = read_id_list(&mut input)?; - ai.ungrouped_soldiers = read_id_list(&mut input)?; - ai.boats = read_id_list(&mut input)?; - ai.war_boats = read_id_list(&mut input)?; - ai.fishing_boats = read_id_list(&mut input)?; - ai.trade_boats = read_id_list(&mut input)?; - ai.transport_boats = read_id_list(&mut input)?; - ai.artifacts = read_id_list(&mut input)?; - ai.trade_carts = read_id_list(&mut input)?; + // FIXME: more stuff here - todo!() - } + ai.soldiers = read_id_list(input)?; + ai.ungrouped_soldiers = read_id_list(input)?; + ai.boats = read_id_list(input)?; + ai.war_boats = read_id_list(input)?; + ai.fishing_boats = read_id_list(input)?; + ai.trade_boats = read_id_list(input)?; + ai.transport_boats = read_id_list(input)?; + ai.artifacts = read_id_list(input)?; + ai.trade_carts = read_id_list(input)?; - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { todo!() } } @@ -873,16 +883,16 @@ pub struct PlayerAI { pub strategy_ai: StrategyAI, } -impl PlayerAI { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let main_ai = MainAI::read_from(&mut input)?; - let build_ai = BuildAI::read_from(&mut input, version)?; - let construction_ai = ConstructionAI::read_from(&mut input, version)?; - let diplomacy_ai = DiplomacyAI::read_from(&mut input)?; - let emotional_ai = EmotionalAI::read_from(&mut input)?; - let information_ai = InformationAI::read_from(&mut input, version)?; - let resource_ai = ResourceAI::read_from(&mut input)?; - let strategy_ai = StrategyAI::read_from(&mut input, version)?; +impl ReadableHeaderElement for PlayerAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let main_ai = MainAI::read_from(input)?; + let build_ai = BuildAI::read_from(input)?; + let construction_ai = ConstructionAI::read_from(input)?; + let diplomacy_ai = DiplomacyAI::read_from(input)?; + let emotional_ai = EmotionalAI::read_from(input)?; + let information_ai = InformationAI::read_from(input)?; + let resource_ai = ResourceAI::read_from(input)?; + let strategy_ai = StrategyAI::read_from(input)?; Ok(Self { main_ai, @@ -895,13 +905,9 @@ impl PlayerAI { strategy_ai, }) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } -fn read_id_list>(mut input: impl Read) -> Result> { +fn read_id_list, R: Read>(input: &mut R) -> Result> { let len = input.read_u32::()?; let mut ids = vec![]; for _ in 0..len { diff --git a/crates/genie-rec/src/element.rs b/crates/genie-rec/src/element.rs new file mode 100644 index 0000000..9b2d7d9 --- /dev/null +++ b/crates/genie-rec/src/element.rs @@ -0,0 +1,45 @@ +use crate::reader::RecordingHeaderReader; +use std::io::{Read, Write}; + +pub trait OptionalReadableElement: Sized { + fn read_from(input: &mut RecordingHeaderReader) -> crate::Result>; +} + +impl ReadableElement> for T { + fn read_from(input: &mut RecordingHeaderReader) -> crate::Result> { + OptionalReadableElement::read_from(input) + } +} + +pub trait ReadableElement: Sized { + fn read_from(input: &mut RecordingHeaderReader) -> crate::Result; +} + +pub trait ReadableHeaderElement: Sized { + fn read_from(input: &mut RecordingHeaderReader) -> crate::Result; +} + +impl ReadableElement for T { + fn read_from(input: &mut RecordingHeaderReader) -> crate::Result { + ReadableHeaderElement::read_from(input) + } +} + +pub trait WritableElement { + fn write_to(element: &T, output: &mut W) -> crate::Result<()>; +} + +pub trait WritableHeaderElement { + fn write_to(&self, output: &mut W) -> crate::Result<()> { + // we need to use `output` otherwise we'll get warnings that it's not used + // prefixing it would make any traits auto completed also be prefixed with _ and that's annoying + let _ = output; + unimplemented!() + } +} + +impl WritableElement for T { + fn write_to(element: &T, output: &mut W) -> crate::Result<()> { + WritableHeaderElement::write_to(element, output) + } +} diff --git a/crates/genie-rec/src/error.rs b/crates/genie-rec/src/error.rs new file mode 100644 index 0000000..d60b266 --- /dev/null +++ b/crates/genie-rec/src/error.rs @@ -0,0 +1,43 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum SyncError { + #[error("Got a sync message, but the log header said there would be a sync message {0} ticks later. The recorded game file may be corrupt")] + UnexpectedSync(u32), + #[error("Expected a sync message at this point, the recorded game file may be corrupt")] + ExpectedSync, +} + +/// Errors that may occur while reading a recorded game file. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + IoError(#[from] io::Error), + #[error(transparent)] + SyncError(#[from] SyncError), + #[error(transparent)] + DecodeStringError(#[from] genie_support::DecodeStringError), + #[error("Could not read embedded scenario data: {0}")] + ReadScenarioError(#[from] genie_scx::Error), + #[error("Failed to parse DE JSON chat message: {0}")] + DEChatMessageJsonError(#[from] serde_json::Error), + #[error( + "Failed to parse DE JSON chat message, JSON is missing the key {0}, or value is invalid" + )] + ParseDEChatMessageError(&'static str), + #[error( + "Failed to find static marker in recording (expected {1:#x} ({1}), found {2:#x} ({2}), version {0}, {3}:{4}, found next {1:#x} ({1}) {5} bytes further)" + )] + MissingMarker(f32, u128, u128, &'static str, u32, u64), + #[error("Failed parsing header at position {0}: {1}")] + HeaderError(u64, Box), +} + +impl From for Error { + fn from(err: genie_support::ReadStringError) -> Self { + match err { + genie_support::ReadStringError::DecodeStringError(inner) => inner.into(), + genie_support::ReadStringError::IoError(inner) => inner.into(), + } + } +} diff --git a/crates/genie-rec/src/game_options.rs b/crates/genie-rec/src/game_options.rs new file mode 100644 index 0000000..1c74d47 --- /dev/null +++ b/crates/genie-rec/src/game_options.rs @@ -0,0 +1,186 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StartingResources { + None = -1, + Standard = 0, + Low = 1, + Medium = 2, + High = 3, + UltraHigh = 4, + Infinite = 5, + Random = 6, +} + +impl From for StartingResources { + fn from(val: i32) -> Self { + match val { + -1 => StartingResources::None, + 0 => StartingResources::Standard, + 1 => StartingResources::Low, + 2 => StartingResources::Medium, + 3 => StartingResources::High, + 4 => StartingResources::UltraHigh, + 5 => StartingResources::Infinite, + 6 => StartingResources::Random, + _ => unimplemented!("Don't know any starting resource with value {}", val), + } + } +} + +impl Default for StartingResources { + fn default() -> Self { + StartingResources::Standard + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VictoryType { + Standard = 0, + Conquest = 1, + Exploration = 2, + Ruins = 3, + Artifacts = 4, + Discoveries = 5, + Gold = 6, + TimeLimit = 7, + Score = 8, + Standard2 = 9, + Regicide = 10, + LastManStanding = 11, +} + +impl From for VictoryType { + fn from(val: u32) -> Self { + match val { + 0 => VictoryType::Standard, + 1 => VictoryType::Conquest, + 2 => VictoryType::Exploration, + 3 => VictoryType::Ruins, + 4 => VictoryType::Artifacts, + 5 => VictoryType::Discoveries, + 6 => VictoryType::Gold, + 7 => VictoryType::TimeLimit, + 8 => VictoryType::Score, + 9 => VictoryType::Standard2, + 10 => VictoryType::Regicide, + 11 => VictoryType::LastManStanding, + _ => unimplemented!("Don't know any victory type with value {}", val), + } + } +} + +impl Default for VictoryType { + fn default() -> Self { + VictoryType::Standard + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Difficulty { + Easiest = 4, + Easy = -1, + /// Age of Empires 2: Definitive Edition only. + Extreme = 5, + Hard = 1, + Hardest = 0, + Moderate = 2, + Standard = 3, +} + +impl From for Difficulty { + fn from(val: u32) -> Self { + match val { + 0 => Difficulty::Hardest, + 1 => Difficulty::Hard, + 2 => Difficulty::Moderate, + 3 => Difficulty::Standard, + 4 => Difficulty::Easiest, + 5 => Difficulty::Extreme, + _ => unimplemented!("Don't know any difficulty with value {}", val), + } + } +} + +impl Default for Difficulty { + fn default() -> Self { + Difficulty::Standard + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MapSize { + Tiny, + Small, + Medium, + Normal, + Large, + Giant, + Ludicrous, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MapType {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ResourceLevel {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Age {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Visibility { + Normal, + Explored, + AllVisible, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum GameMode { + RM = 0, + Regicide = 1, + DM = 2, + Scenario = 3, + Campaign = 4, + KingOfTheHill = 5, + WonderRace = 6, + DefendTheWonder = 7, + TurboRandom = 8, + CaptureTheRelic = 10, + SuddenDeath = 11, + BattleRoyale = 12, + EmpireWars = 13, +} + +impl From for GameMode { + fn from(n: u32) -> Self { + match n { + 0 => GameMode::RM, + 1 => GameMode::Regicide, + 2 => GameMode::DM, + 3 => GameMode::Scenario, + 4 => GameMode::Campaign, + 5 => GameMode::KingOfTheHill, + 6 => GameMode::WonderRace, + 7 => GameMode::DefendTheWonder, + 8 => GameMode::TurboRandom, + 10 => GameMode::CaptureTheRelic, + 11 => GameMode::SuddenDeath, + 12 => GameMode::BattleRoyale, + 13 => GameMode::EmpireWars, + _ => unimplemented!("Don't know any game mode with value {}", n), + } + } +} + +impl Default for GameMode { + fn default() -> Self { + GameMode::RM + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum GameSpeed { + Slow, + Casual, + Normal, + Fast, +} diff --git a/crates/genie-rec/src/header.rs b/crates/genie-rec/src/header.rs index 2398940..3c04df7 100644 --- a/crates/genie-rec/src/header.rs +++ b/crates/genie-rec/src/header.rs @@ -1,15 +1,25 @@ +use crate::game_options::{Difficulty, GameMode, StartingResources, VictoryType}; use crate::map::Map; use crate::player::Player; +use crate::reader::RecordingHeaderReader; use crate::string_table::StringTable; +use crate::GameVariant::DefinitiveEdition; +use crate::{element::ReadableHeaderElement, reader::Peek}; use crate::{GameVersion, Result}; use byteorder::{ReadBytesExt, LE}; -use genie_scx::TribeScen; -use genie_support::ReadSkipExt; +use genie_scx::{AgeIdentifier, TribeScen}; pub use genie_support::SpriteID; -use std::convert::TryInto; +use genie_support::{ReadSkipExt, ReadStringsExt}; use std::fmt::{self, Debug}; use std::io::Read; +#[cfg(debug_assertions)] +use crate::dbg_dmp; + +const DE_HEADER_SEPARATOR: u32 = u32::from_le_bytes(*b"\xa3_\x02\x00"); +const DE_STRING_SEPARATOR: u16 = u16::from_le_bytes(*b"\x60\x0A"); +const DE_PLAYER_SEPARATOR: u32 = u32::from_le_bytes(*b"\x00\x00\x00\x00"); + #[derive(Debug, Default, Clone)] pub struct AICommand { pub command_type: i32, @@ -17,11 +27,14 @@ pub struct AICommand { pub parameters: [i32; 4], } -impl AICommand { - pub fn read_from(mut input: impl Read) -> Result { - let mut cmd = Self::default(); - cmd.command_type = input.read_i32::()?; - cmd.id = input.read_u16::()?; +impl ReadableHeaderElement for AICommand { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut cmd = AICommand { + command_type: input.read_i32::()?, + id: input.read_u16::()?, + ..Default::default() + }; + input.skip(2)?; input.read_i32_into::(&mut cmd.parameters)?; Ok(cmd) @@ -38,18 +51,20 @@ pub struct AIListRule { actions: Vec, } -impl AIListRule { - pub fn read_from(mut input: impl Read) -> Result { - let mut rule = Self::default(); - rule.in_use = input.read_u32::()? != 0; - rule.enable = input.read_u32::()? != 0; - rule.rule_id = input.read_u16::()?; - rule.next_in_group = input.read_u16::()?; +impl ReadableHeaderElement for AIListRule { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut rule = AIListRule { + in_use: input.read_u32::()? != 0, + enable: input.read_u32::()? != 0, + rule_id: input.read_u16::()?, + next_in_group: input.read_u16::()?, + ..Default::default() + }; let num_facts = input.read_u8()?; let num_facts_actions = input.read_u8()?; input.read_u16::()?; for i in 0..16 { - let cmd = AICommand::read_from(&mut input)?; + let cmd = AICommand::read_from(input)?; if i < num_facts { rule.facts.push(cmd); } else if i < num_facts_actions { @@ -68,16 +83,18 @@ pub struct AIList { rules: Vec, } -impl AIList { - pub fn read_from(mut input: impl Read) -> Result { - let mut list = Self::default(); - list.in_use = input.read_u32::()? != 0; - list.id = input.read_i32::()?; - list.max_rules = input.read_u16::()?; +impl ReadableHeaderElement for AIList { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut list = AIList { + in_use: input.read_u32::()? != 0, + id: input.read_i32::()?, + max_rules: input.read_u16::()?, + ..Default::default() + }; let num_rules = input.read_u16::()?; input.read_u32::()?; for _ in 0..num_rules { - list.rules.push(AIListRule::read_from(&mut input)?); + list.rules.push(AIListRule::read_from(input)?); } Ok(list) } @@ -89,10 +106,13 @@ pub struct AIGroupTable { groups: Vec, } -impl AIGroupTable { - pub fn read_from(mut input: impl Read) -> Result { - let mut table = Self::default(); - table.max_groups = input.read_u16::()?; +impl ReadableHeaderElement for AIGroupTable { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut table = AIGroupTable { + max_groups: input.read_u16::()?, + ..Default::default() + }; + let num_groups = input.read_u16::()?; input.read_u32::()?; for _ in 0..num_groups { @@ -143,8 +163,8 @@ impl Debug for AIFactState { } } -impl AIFactState { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for AIFactState { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let save_version = input.read_f32::()?; let version = input.read_f32::()?; let death_match = input.read_u32::()? != 0; @@ -198,24 +218,24 @@ pub struct AIScripts { pub fact_state: AIFactState, } -impl AIScripts { - pub fn read_from(mut input: impl Read) -> Result { - let string_table = StringTable::read_from(&mut input)?; +impl ReadableHeaderElement for AIScripts { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let string_table = StringTable::read_from(input)?; let _max_facts = input.read_u16::()?; let _max_actions = input.read_u16::()?; let max_lists = input.read_u16::()?; let mut lists = vec![]; for _ in 0..max_lists { - lists.push(AIList::read_from(&mut input)?); + lists.push(AIList::read_from(input)?); } let mut groups = vec![]; for _ in 0..max_lists { - groups.push(AIGroupTable::read_from(&mut input)?); + groups.push(AIGroupTable::read_from(input)?); } - let fact_state = AIFactState::read_from(&mut input)?; + let fact_state = AIFactState::read_from(input)?; Ok(AIScripts { string_table, @@ -230,6 +250,7 @@ impl AIScripts { pub struct Header { game_version: GameVersion, save_version: f32, + de_extension_header: Option, ai_scripts: Option, map: Map, particle_system: ParticleSystem, @@ -241,15 +262,26 @@ impl Header { pub fn players(&self) -> impl Iterator { self.players.iter() } +} - pub fn read_from(mut input: impl Read) -> Result { - let mut header = Header::default(); - header.game_version = GameVersion::read_from(&mut input)?; - header.save_version = input.read_f32::()?; +impl ReadableHeaderElement for Header { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut header = Header { + game_version: GameVersion::read_from(input)?, + save_version: input.read_f32::()?, + ..Default::default() + }; + + // update reader state + input.set_version(header.game_version, header.save_version); + + if input.variant() >= DefinitiveEdition { + header.de_extension_header = Some(DeExtensionHeader::read_from(input)?) + } let includes_ai = input.read_u32::()? != 0; if includes_ai { - header.ai_scripts = Some(AIScripts::read_from(&mut input)?); + header.ai_scripts = Some(AIScripts::read_from(input)?); } let _old_time = input.read_u32::()?; @@ -266,7 +298,8 @@ impl Header { let _random_seed2 = input.read_u32::()?; let _current_player = input.read_u16::()?; let num_players = input.read_u16::()?; - if header.save_version >= 11.76 { + input.set_num_players(num_players); + if input.version() >= 11.76 { let _aegis_enabled = input.read_u8()? != 0; let _cheats_enabled = input.read_u8()? != 0; } @@ -274,7 +307,7 @@ impl Header { let _campaign = input.read_u32::()?; let _campaign_player = input.read_u32::()?; let _campaign_scenario = input.read_u32::()?; - if header.save_version >= 10.13 { + if input.version() >= 10.13 { let _king_campaign = input.read_u32::()?; let _king_campaign_player = input.read_u8()?; let _king_campaign_scenario = input.read_u8()?; @@ -283,11 +316,16 @@ impl Header { let mut player_time_delta = [0; 9]; input.read_u32_into::(&mut player_time_delta[..])?; - header.map = Map::read_from(&mut input)?; + if header.save_version >= 12.97 { + // ??? + input.skip(8)?; + } + + header.map = Map::read_from(input)?; // TODO is there another num_players here for restored games? - header.particle_system = ParticleSystem::read_from(&mut input)?; + header.particle_system = ParticleSystem::read_from(input)?; if header.save_version >= 11.07 { let _identifier = input.read_u32::()?; @@ -295,17 +333,17 @@ impl Header { header.players.reserve(num_players.try_into().unwrap()); for _ in 0..num_players { - header.players.push(Player::read_from( - &mut input, - header.save_version, - num_players as u8, - )?); + header.players.push(Player::read_from(input)?); } for player in &mut header.players { - player.read_info(&mut input, header.save_version)?; + player.read_info(input)?; } - header.scenario = TribeScen::read_from(&mut input)?; + header.scenario = TribeScen::read_from(&mut *input)?; + + if input.variant() >= DefinitiveEdition { + input.skip(133)?; + } let _difficulty = if header.save_version >= 7.16 { Some(input.read_u32::()?) @@ -360,6 +398,433 @@ impl Header { } } +#[derive(Debug, Default, Clone)] +pub struct DeExtensionHeader { + pub build: Option, // save_version >= 25.22 + pub timestamp: Option, // save_version >= 26.16 + pub version: f32, + pub interval_version: u32, + pub game_options_version: u32, + pub dlc_count: u32, + pub dlc_ids: Vec, + pub dataset_ref: u32, + pub difficulty: Difficulty, // unsure, always "4" + pub selected_map_id: u32, + pub resolved_map_id: u32, + pub reveal_map: u32, + pub victory_type_id: u32, + pub victory_type: VictoryType, + pub starting_resources_id: i32, + pub starting_resources: StartingResources, + pub starting_age_id: i32, + pub starting_age: AgeIdentifier, + pub ending_age_id: i32, + pub ending_age: AgeIdentifier, + pub game_mode: GameMode, + // DE_HEADER_SEPARATOR, + // DE_HEADER_SEPARATOR, + pub speed: f32, + pub treaty_length: u32, + pub population_limit: u32, + pub num_players: u32, + pub unused_player_color: u32, + pub victory_amount: u32, + // DE_HEADER_SEPARATOR, + pub trade_enabled: bool, + pub team_bonus_disabled: bool, + pub random_positions: bool, + pub all_techs: bool, + pub num_starting_units: u8, + pub lock_teams: bool, + pub lock_speed: bool, + pub multiplayer: bool, + pub cheats_enabled: bool, + pub record_game: bool, + pub animals_enabled: bool, + pub predators_enabled: bool, + pub turbo_enabled: bool, + pub shared_exploration: bool, + pub team_positions: bool, + pub sub_game_mode: Option, // save_version >= 13.34 + pub battle_royale_time: Option, // save_version >= 13.34 + pub handicap: Option, // save_version >= 25.06 + // DE_HEADER_SEPARATOR, + pub players: [DePlayer; 8], + // 9 bytes + pub fog_of_war: bool, + pub cheat_notifications: bool, + pub colored_chat: bool, + // DE_HEADER_SEPARATOR + pub ranked: bool, + pub allow_spectators: bool, + pub lobby_visibility: u32, + pub hidden_civs: bool, + pub matchmaking: bool, + pub spectator_delay: u32, + pub scenario_civ: Option, // save_version >= 13.13 + pub rms_crc: Option<[u8; 4]>, // save_version >= 13.13 + // Skipped for now, check https://github.com/happyleavesaoc/aoc-mgz/blob/44cd0a6d8ea19524c82893f11be928b468c46bea/mgz/header/de.py#L111 + // 8 bytes; save_version >= 25.02 + pub num_ai_files: i64, + // TODO: ai_files, skipped + pub guid: u128, + pub lobby_name: String, + // 8 bytes; save_version >= 25.22 + pub modded_dataset: String, + // 19 bytes + // 5 bytes; save_version >= 13.13 + // 9 bytes; save_version >= 13.17 + // 1 bytes; save_version >= 20.06 + // 8 bytes; save_version >= 20.16 + // 21 bytes; save_version >= 25.06 + // 4 bytes; save_version >= 25.22 + // 8 bytes; save_version >= 26.16 + // DeString + // 5 bytes + // 1 byte; save_version >= 13.13 + // Struct + // 2 byte; save_version >= 13.17 +} + +impl ReadableHeaderElement for DeExtensionHeader { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut header = Self::default(); + if input.version() >= 25.22 { + header.build = Some(input.read_u32::()?) + } else { + header.build = None + }; + + if input.version() >= 26.16 { + header.timestamp = Some(input.read_u32::()?) + } else { + header.timestamp = None + }; + + header.version = input.read_f32::()?; + header.interval_version = input.read_u32::()?; + header.game_options_version = input.read_u32::()?; + header.dlc_count = input.read_u32::()?; + + for _ in 0..header.dlc_count { + header.dlc_ids.push(input.read_u32::()?) + } + + header.dataset_ref = input.read_u32::()?; + // TODO: This is definitely wrong. Not the Difficulty. + header.difficulty = input.read_u32::()?.into(); + header.selected_map_id = input.read_u32::()?; + header.resolved_map_id = input.read_u32::()?; + header.reveal_map = input.read_u32::()?; + header.victory_type_id = input.read_u32::()?; + header.victory_type = header.victory_type_id.into(); + header.starting_resources_id = input.read_i32::()?; + header.starting_resources = header.starting_resources_id.into(); + header.starting_age_id = input.read_i32::()?; + header.starting_age = AgeIdentifier::try_from(header.starting_age_id, input.version()) + .expect("Converting starting age identifier failed."); + header.ending_age_id = input.read_i32::()?; + header.ending_age = AgeIdentifier::try_from(header.ending_age_id, input.version()) + .expect("Converting ending age identifier failed."); + header.game_mode = input.read_u32::()?.into(); + assert_eq!(input.read_u32::()?, DE_HEADER_SEPARATOR); + assert_eq!(input.read_u32::()?, DE_HEADER_SEPARATOR); + header.speed = input.read_f32::()?; + header.treaty_length = input.read_u32::()?; + header.population_limit = input.read_u32::()?; + header.num_players = input.read_u32::()?; + header.unused_player_color = input.read_u32::()?; + header.victory_amount = input.read_u32::()?; + assert_eq!(input.read_u32::()?, DE_HEADER_SEPARATOR); + header.trade_enabled = input.read_u8()? == 1; + header.team_bonus_disabled = input.read_u8()? == 1; + header.random_positions = input.read_u8()? == 1; + header.all_techs = input.read_u8()? == 1; + header.num_starting_units = input.read_u8()?; + header.lock_teams = input.read_u8()? == 1; + header.lock_speed = input.read_u8()? == 1; + header.multiplayer = input.read_u8()? == 1; + header.cheats_enabled = input.read_u8()? == 1; + header.record_game = input.read_u8()? == 1; + header.animals_enabled = input.read_u8()? == 1; + header.predators_enabled = input.read_u8()? == 1; + header.turbo_enabled = input.read_u8()? == 1; + header.shared_exploration = input.read_u8()? == 1; + header.team_positions = input.read_u8()? == 1; + if input.version() >= 13.34 { + header.sub_game_mode = Some(input.read_u32::()?) + } else { + header.sub_game_mode = None + }; + + if input.version() >= 13.34 { + header.battle_royale_time = Some(input.read_u32::()?) + } else { + header.battle_royale_time = None + }; + + if input.version() >= 25.06 { + header.handicap = Some(input.read_u8()? == 1) + } else { + header.handicap = None + }; + + assert_eq!(input.read_u32::()?, DE_HEADER_SEPARATOR); + + for player in &mut header.players { + player.apply_from(input)?; + } + + // Skip 9 unknown bytes + input.skip(9)?; + + header.fog_of_war = input.read_u8()? == 1; + header.cheat_notifications = input.read_u8()? == 1; + header.colored_chat = input.read_u8()? == 1; + + assert_eq!(input.read_u32::()?, DE_HEADER_SEPARATOR); + + // input.skip(12)?; + + header.ranked = input.read_u8()? == 1; + header.allow_spectators = input.read_u8()? == 1; + header.lobby_visibility = input.read_u32::()?; + header.hidden_civs = input.read_u8()? == 1; + header.matchmaking = input.read_u8()? == 1; + header.spectator_delay = input.read_u32::()?; + + if input.version() >= 13.13 { + header.scenario_civ = Some(input.read_u8()?) + } else { + header.scenario_civ = None + }; + + if input.version() >= 13.13 { + let mut temp = [0u8; 4]; + #[allow(clippy::needless_range_loop)] + for val in &mut temp { + *val = input.read_u8()? + } + header.rms_crc = Some(temp) + } else { + header.rms_crc = None + }; + + // TODO: read strings ??? + for _ in 0..23 { + let _string = input.read_tlv_str()?; + while [3, 21, 23, 42, 44, 45, 46, 47].contains(&input.read_u32::()?) {} + } + + // TODO "strategic numbers" ??? + input.skip(59 * 4)?; + + // num ai files + header.num_ai_files = input.read_i64::()?; + + for _ in 0..header.num_ai_files { + input.skip(4)?; + // CONTINUE HERE + #[cfg(debug_assertions)] + dbg_dmp!(input, 32); + input.read_tlv_str()?; + input.skip(4)?; + } + + if input.version() >= 25.02 { + input.skip(8)?; + } + + header.guid = input.read_u128::()?; + + header.lobby_name = input.read_tlv_str()?.unwrap_or_default(); + + if input.version() >= 25.22 { + input.skip(8)?; + } + + header.modded_dataset = input.read_tlv_str()?.unwrap_or_default(); + + input.skip(19)?; + + if input.version() >= 13.13 { + input.skip(5)?; + } + + if input.version() >= 13.17 { + input.skip(9)?; + } + + if input.version() >= 20.06 { + input.skip(1)?; + } + + if input.version() >= 20.16 { + input.skip(8)?; + } + + if input.version() >= 25.06 { + input.skip(21)?; + } + + if input.version() >= 25.22 { + input.skip(4)?; + } + + if input.version() >= 26.16 { + input.skip(8)?; + } + + input.read_tlv_str()?; + + input.skip(5)?; + + if input.version() >= 13.13 { + input.skip(1)?; + } + + if input.version() < 13.17 { + input.read_tlv_str()?; + input.skip(4)?; + input.skip(4)?; // usually 0 + } + + if input.version() >= 13.17 { + input.skip(2)?; + } + + Ok(header) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PlayerType { + Absent = 0, + Closed = 1, + Human = 2, + Eliminated = 3, + Computer = 4, + Cyborg = 5, + Spectator = 6, + Unknown = 999, +} + +impl Default for PlayerType { + fn default() -> Self { + PlayerType::Human + } +} + +impl From for PlayerType { + #[inline] + fn from(condition: u32) -> PlayerType { + match condition { + 0 => PlayerType::Absent, + 1 => PlayerType::Closed, + 2 => PlayerType::Human, + 3 => PlayerType::Eliminated, + 4 => PlayerType::Computer, + 5 => PlayerType::Cyborg, + 6 => PlayerType::Spectator, + 7..=u32::MAX => PlayerType::Unknown, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct DePlayer { + dlc_id: u32, + color_id: i32, + selected_color: i8, + selected_team_id: u8, + resolved_team_id: u8, + // TODO: Not the actual dat_crc32/64, probably filler + // dat_crc: u64, + mp_game_version: u8, + civ_id: u8, + ai_type: String, + ai_civ_name_index: u8, + ai_name: String, + name: String, + player_type: PlayerType, + profile_id: u32, + // DE_PLAYER_SEPARATOR, + player_number: i8, + hd_rm_elo: Option, // save_version < 25.22 + hd_dm_elo: Option, // save_version < 25.22 + prefer_random: bool, + custom_ai: bool, + handicap: Option, // save_version < 25.06 +} + +impl DePlayer { + pub fn apply_from(&mut self, input: &mut RecordingHeaderReader) -> Result<()> { + self.dlc_id = input.read_u32::()?; + self.color_id = input.read_i32::()?; + self.selected_color = input.read_i8()?; + self.selected_team_id = input.read_u8()?; + self.resolved_team_id = input.read_u8()?; + // TODO: Probably filler + //self.dat_crc = input.read_u64::()?; + input.skip(5)?; + self.mp_game_version = input.read_u8()?; + input.skip(3)?; + self.civ_id = input.read_u8()?; + input.skip(3)?; + + self.ai_type = input.read_tlv_str()?.unwrap_or_default(); + self.ai_civ_name_index = input.read_u8()?; + self.ai_name = input.read_tlv_str()?.unwrap_or_default(); + self.name = input.read_tlv_str()?.unwrap_or_default(); + self.player_type = input.read_u32::()?.into(); + self.profile_id = input.read_u32::()?; + + // DE_PLAYER_SEPARATOR + assert_eq!( + input.read_u32::()?, + 0, + "DE Player Separator not found after Profile_ID!" + ); + + self.player_number = input.read_i8()?; + + if input.version() < 25.22 { + self.hd_rm_elo = Some(input.read_u32::()?) + } else { + self.hd_rm_elo = None; + }; + let _ = std::u16::MAX; + if input.version() < 25.22 { + self.hd_dm_elo = Some(input.read_u32::()?) + } else { + self.hd_dm_elo = None; + }; + + self.prefer_random = input.read_u8()? == 1; + self.custom_ai = input.read_u8()? == 1; + + if input.version() >= 25.06 { + self.handicap = Some(input.read_u8()?) + } else { + self.handicap = None; + }; + + input.skip(2)?; + + let pos = input.position(); + assert_eq!( + input.read_u32::()?, + u32::from_le_bytes(*b"\xFF\xFF\xFF\xFF"), + "DE Player Separator (End) not found at position {:#X}!", + pos + ); + + input.skip(4)?; + + Ok(()) + } +} + #[derive(Debug, Default, Clone)] struct Particle { pub start: u32, @@ -370,20 +835,20 @@ struct Particle { pub flags: u8, } -impl Particle { - pub fn read_from(mut input: impl Read) -> Result { - let mut particle = Self::default(); - particle.start = input.read_u32::()?; - particle.facet = input.read_u32::()?; - particle.update = input.read_u32::()?; - particle.sprite_id = input.read_u16::()?.into(); - particle.location = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - particle.flags = input.read_u8()?; - Ok(particle) +impl ReadableHeaderElement for Particle { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(Particle { + start: input.read_u32::()?, + facet: input.read_u32::()?, + update: input.read_u32::()?, + sprite_id: input.read_u16::()?.into(), + location: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + flags: input.read_u8()?, + }) } } @@ -393,13 +858,13 @@ struct ParticleSystem { pub particles: Vec, } -impl ParticleSystem { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ParticleSystem { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let world_time = input.read_u32::()?; let num_particles = input.read_u32::()?; let mut particles = Vec::with_capacity(num_particles.try_into().unwrap()); for _ in 0..num_particles { - particles.push(Particle::read_from(&mut input)?); + particles.push(Particle::read_from(input)?); } Ok(Self { world_time, diff --git a/crates/genie-rec/src/lib.rs b/crates/genie-rec/src/lib.rs index 49acc6c..7b4e7e1 100644 --- a/crates/genie-rec/src/lib.rs +++ b/crates/genie-rec/src/lib.rs @@ -17,25 +17,104 @@ #![deny(unsafe_code)] // #![warn(missing_docs)] #![warn(unused)] +// Development +#![allow(unused_imports)] +#![allow(dead_code)] pub mod actions; pub mod ai; +pub mod element; +pub mod error; +pub mod game_options; pub mod header; pub mod map; +pub mod parser; pub mod player; +pub mod reader; pub mod string_table; pub mod unit; pub mod unit_action; pub mod unit_type; - -use crate::actions::{Action, Meta}; +pub mod version; + +use crate::element::ReadableElement; +use crate::error::Error; +use crate::error::SyncError; +use crate::game_options::Difficulty::{Easiest, Extreme, Hard, Hardest, Moderate, Standard}; +use crate::reader::RecordingHeaderReader; +use crate::{ + actions::{Action, Meta}, + reader::Peek, +}; use byteorder::{ReadBytesExt, LE}; use flate2::bufread::DeflateDecoder; use genie_scx::DLCOptions; use genie_support::{fallible_try_from, fallible_try_into, infallible_try_into}; pub use header::Header; -use std::fmt::{self, Debug, Display}; +use std::fmt::Debug; use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom}; +pub use version::*; + +#[cfg(debug_assertions)] +#[macro_export] +/// Print the current hex position in the file while parsing. +macro_rules! dbg_dmp { + ($x:expr, $y:expr) => {{ + + use comfy_table::*; + use comfy_table::presets::UTF8_FULL; + use comfy_table::modifiers::UTF8_ROUND_CORNERS; + + let pos = &($x).position(); + let peek = &($x) + .peek($y) + .expect(&format!("Peeking for {:?} bytes failed!", $y)); + + println!("Current position: 0x{:08X}", pos); + println!("Peeking for {:?} bytes", $y); + + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::DynamicFullWidth); + + let mut pos_loop = pos.clone(); + let mut cells = vec![]; + + peek.iter().for_each( + |elem| { + + cells.push( + Cell::new(format!( + " + Position @ 0x{:08X}: + Values: + hex: 0x{:X} + u8: {} + utf-8 {}", + pos_loop, + elem, + u8::from(*elem), + String::from_utf8_lossy(std::slice::from_ref(elem)) + ))); + + pos_loop += 1; + }); + + + let rows = cells.chunks(4).map(|s| s.into()).collect::>>(); + + table.add_rows(rows); + + println!("{table}"); + + }}; + + ($x:expr) => {{ + println!("Current position: {:#X}", &($x)); + }}; +} /// ID identifying a player (0-8). #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -99,68 +178,6 @@ fallible_try_from!(ObjectID, i32); fallible_try_into!(ObjectID, i16); fallible_try_into!(ObjectID, i32); -/// The game data version string. In practice, this does not really reflect the game version. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct GameVersion([u8; 8]); - -impl Default for GameVersion { - fn default() -> Self { - Self([0; 8]) - } -} - -impl Debug for GameVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", std::str::from_utf8(&self.0).unwrap()) - } -} - -impl Display for GameVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", std::str::from_utf8(&self.0).unwrap()) - } -} - -impl GameVersion { - /// Read the game version string from an input stream. - pub fn read_from(mut input: impl Read) -> Result { - let mut game_version = [0; 8]; - input.read_exact(&mut game_version)?; - Ok(Self(game_version)) - } -} - -/// -#[derive(Debug, thiserror::Error)] -pub enum SyncError { - #[error("Got a sync message, but the log header said there would be a sync message {0} ticks later. The recorded game file may be corrupt")] - UnexpectedSync(u32), - #[error("Expected a sync message at this point, the recorded game file may be corrupt")] - ExpectedSync, -} - -/// Errors that may occur while reading a recorded game file. -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - IoError(#[from] io::Error), - #[error(transparent)] - SyncError(#[from] SyncError), - #[error(transparent)] - DecodeStringError(#[from] genie_support::DecodeStringError), - #[error("Could not read embedded scenario data: {0}")] - ReadScenarioError(#[from] genie_scx::Error), -} - -impl From for Error { - fn from(err: genie_support::ReadStringError) -> Self { - match err { - genie_support::ReadStringError::DecodeStringError(inner) => inner.into(), - genie_support::ReadStringError::IoError(inner) => inner.into(), - } - } -} - /// Result type alias with `genie_rec::Error` as the error type. pub type Result = std::result::Result; @@ -169,6 +186,7 @@ pub struct BodyActions where R: BufRead, { + data_version: f32, input: R, meta: Meta, remaining_syncs_until_checksum: u32, @@ -186,6 +204,7 @@ where }; let remaining_syncs_until_checksum = meta.checksum_interval; Ok(Self { + data_version, input, meta, remaining_syncs_until_checksum, @@ -227,6 +246,16 @@ where } Ok(0x03) => Some(actions::ViewLock::read_from(&mut self.input).map(Action::ViewLock)), Ok(0x04) => Some(actions::Chat::read_from(&mut self.input).map(Action::Chat)), + // AoE2:DE however (also) uses the op field as length field + Ok(length) if self.data_version >= DE_SAVE_VERSION => { + let mut buffer = vec![0u8; length as usize]; + match self.input.read_exact(&mut buffer) { + Ok(_) => { + Some(actions::EmbeddedAction::from_buffer(buffer).map(Action::Embedded)) + } + Err(err) => Some(Err(err.into())), + } + } Ok(id) => panic!("unsupported action type {:#x}", id), Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => None, Err(err) => Some(Err(err.into())), @@ -234,121 +263,6 @@ where } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Difficulty { - Easiest, - Easy, - Standard, - Hard, - Hardest, - /// Age of Empires 2: Definitive Edition only. - Extreme, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MapSize {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum MapType {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Visibility {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ResourceLevel {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Age {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum GameMode {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum GameSpeed {} - -#[derive(Debug, Clone)] -pub struct HDGameOptions { - pub dlc_options: DLCOptions, - pub difficulty: Difficulty, - pub map_size: MapSize, - pub map_type: MapType, - pub visibility: Visibility, - pub starting_resources: ResourceLevel, - pub starting_age: Age, - pub ending_age: Age, - pub game_mode: GameMode, - // if version < 1001 - pub random_map_name: Option, - // if version < 1001 - pub scenario_name: Option, - pub game_speed: GameSpeed, - pub treaty_length: i32, - pub population_limit: i32, - pub num_players: i32, - pub victory_amount: i32, - pub trading_enabled: bool, - pub team_bonuses_enabled: bool, - pub randomize_positions_enabled: bool, - pub full_tech_tree_enabled: bool, - pub num_starting_units: i8, - pub teams_locked: bool, - pub speed_locked: bool, - pub multiplayer: bool, - pub cheats_enabled: bool, - pub record_game: bool, - pub animals_enabled: bool, - pub predators_enabled: bool, - // if version > 1.16 && version < 1002 - pub scenario_player_indices: Vec, -} - -/// A struct implementing `BufRead` that uses a small, single-use, stack-allocated buffer, intended -/// for reading only the first few bytes from a file. -struct SmallBufReader -where - R: Read, -{ - buffer: [u8; 256], - pointer: usize, - reader: R, -} - -impl SmallBufReader -where - R: Read, -{ - fn new(reader: R) -> Self { - Self { - buffer: [0; 256], - pointer: 0, - reader, - } - } -} - -impl Read for SmallBufReader -where - R: Read, -{ - fn read(&mut self, output: &mut [u8]) -> io::Result { - self.reader.read(output) - } -} - -impl BufRead for SmallBufReader -where - R: Read, -{ - fn fill_buf(&mut self) -> io::Result<&[u8]> { - self.reader.read_exact(&mut self.buffer[self.pointer..])?; - Ok(&self.buffer[self.pointer..]) - } - - fn consume(&mut self, len: usize) { - self.pointer += len; - } -} - /// Recorded game reader. pub struct RecordedGame where @@ -373,6 +287,7 @@ where { pub fn new(mut input: R) -> Result { let file_size = { + input.seek(SeekFrom::Start(0))?; let size = input.seek(SeekFrom::End(0))?; input.seek(SeekFrom::Start(0))?; size @@ -391,7 +306,7 @@ where let (game_version, save_version) = { input.seek(SeekFrom::Start(header_start))?; - let version_reader = SmallBufReader::new(&mut input); + let version_reader = BufReader::new(&mut input); let mut deflate = DeflateDecoder::new(version_reader); let game_version = GameVersion::read_from(&mut deflate)?; let save_version = deflate.read_f32::()?; @@ -408,23 +323,42 @@ where }) } + pub fn save_version(&self) -> f32 { + self.save_version + } + fn seek_to_first_header(&mut self) -> Result<()> { self.inner.seek(SeekFrom::Start(self.header_start))?; Ok(()) } + pub fn get_header_data(&mut self) -> Result> { + let mut header = vec![]; + let mut deflate = self.get_header_deflate()?; + deflate.read_to_end(&mut header)?; + Ok(header) + } + fn seek_to_body(&mut self) -> Result<()> { self.inner.seek(SeekFrom::Start(self.header_end))?; Ok(()) } - pub fn header(&mut self) -> Result
{ + pub fn get_header_deflate(&mut self) -> Result>>> { self.seek_to_first_header()?; let reader = BufReader::new(&mut self.inner).take(self.header_end - self.header_start); - let deflate = DeflateDecoder::new(reader); - let header = Header::read_from(deflate)?; + Ok(DeflateDecoder::new(reader)) + } + + pub fn header(&mut self) -> Result
{ + let deflate = self.get_header_deflate()?; + let mut reader = RecordingHeaderReader::new(deflate); + let header = Header::read_from(&mut reader).map_err(|err| match err { + Error::HeaderError(pos, err) => Error::HeaderError(pos, err), + err => Error::HeaderError(reader.position() as u64, Box::new(err)), + })?; Ok(header) } @@ -445,7 +379,7 @@ mod tests { #[test] // AI data parsing is incomplete: remove this attribute when the test starts passing - #[should_panic = "assertion failed"] + #[should_panic = "AI data cannot be fully parsed"] fn incomplete_up_15_rec_with_ai() { let f = File::open("test/rec.20181208-195117.mgz").unwrap(); let mut r = RecordedGame::new(f).unwrap(); @@ -461,13 +395,7 @@ mod tests { let mut r = RecordedGame::new(f)?; r.header()?; for act in r.actions()? { - match act { - Ok(act) => println!("{:?}", act), - Err(Error::DecodeStringError(_)) => { - // Skip invalid utf8 chat for now - } - Err(err) => return Err(err.into()), - } + let _ = act?; } Ok(()) } @@ -478,8 +406,32 @@ mod tests { let mut r = RecordedGame::new(f)?; r.header()?; for act in r.actions()? { - println!("{:?}", act?); + let _ = act?; } Ok(()) } + + // #[test] + // fn aoe2de_rec() -> anyhow::Result<()> { + // let f = File::open("test/AgeIIDE_Replay_90000059.aoe2record")?; + // let mut r = RecordedGame::new(f)?; + // println!("aoe2de save version {}", r.save_version); + // let _header = r.header()?; + // for act in r.actions()? { + // let _ = act?; + // } + // Ok(()) + // } + + // #[test] + // fn aoe2de_2_rec() -> anyhow::Result<()> { + // let f = File::open("test/AgeIIDE_Replay_90889731.aoe2record")?; + // let mut r = RecordedGame::new(f)?; + // println!("aoe2de save version {}", r.save_version); + // let _header = r.header()?; + // for act in r.actions()? { + // let _ = act?; + // } + // Ok(()) + // } } diff --git a/crates/genie-rec/src/map.rs b/crates/genie-rec/src/map.rs index 58a6f24..3fbfd23 100644 --- a/crates/genie-rec/src/map.rs +++ b/crates/genie-rec/src/map.rs @@ -1,3 +1,6 @@ +use crate::element::{ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::RecordingHeaderReader; +use crate::GameVariant::DefinitiveEdition; use crate::Result; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use genie_support::ReadSkipExt; @@ -16,10 +19,28 @@ pub struct Tile { pub original_terrain: Option, } -impl Tile { +impl ReadableHeaderElement for Tile { /// Read a tile from an input stream. - pub fn read_from(mut input: impl Read) -> Result { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let terrain = input.read_u8()?; + if input.variant() >= DefinitiveEdition { + input.skip(1)?; + let elevation = input.read_u8()?; + input.skip(4)?; + + // there's another DE version (12.97) that does this, + // but for that we need peek/seek support and I'm not rewriting all of this project rn + if input.version() >= 13.03 { + input.skip(2)?; + } + + return Ok(Tile { + terrain, + elevation, + original_terrain: None, + }); + } + let (terrain, elevation, original_terrain) = if terrain == 0xFF { (input.read_u8()?, input.read_u8()?, Some(input.read_u8()?)) } else { @@ -31,9 +52,11 @@ impl Tile { original_terrain, }) } +} +impl WritableHeaderElement for Tile { /// Write a tile to an output stream. - pub fn write_to(&self, mut output: impl Write) -> Result<()> { + fn write_to(&self, output: &mut W) -> Result<()> { match self.original_terrain { Some(t) => { output.write_u8(0xFF)?; @@ -74,22 +97,50 @@ impl Default for MapZone { } impl MapZone { - pub fn read_from(mut input: impl Read, map_size: (u32, u32)) -> Result { + pub fn info(&self) -> &[i8] { + assert_eq!(self.info.len(), 255); + &self.info + } + + pub fn info_mut(&mut self) -> &mut [i8] { + assert_eq!(self.info.len(), 255); + &mut self.info + } + + pub fn tiles(&self) -> &[i32] { + assert_eq!(self.tiles.len(), 255); + &self.tiles + } + + pub fn tiles_mut(&mut self) -> &mut [i32] { + assert_eq!(self.tiles.len(), 255); + &mut self.tiles + } +} + +impl ReadableHeaderElement for MapZone { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut zone = Self::default(); - input.read_i8_into(&mut zone.info)?; - input.read_i32_into::(&mut zone.tiles)?; - zone.zone_map = vec![0; (map_size.0 * map_size.1).try_into().unwrap()]; - input.read_i8_into(&mut zone.zone_map)?; + // this changed in HD/DE, but I have no clue + if input.version() > 11.93 { + input.skip((2048 + (input.tile_count() * 2)) as u64)?; + } else { + input.read_i8_into(&mut zone.info)?; + input.read_i32_into::(&mut zone.tiles)?; + zone.zone_map = vec![0; input.tile_count()]; + input.read_i8_into(&mut zone.zone_map)?; + } let num_rules = input.read_u32::()?; - zone.passability_rules = vec![0.0; num_rules.try_into().unwrap()]; + zone.passability_rules = vec![0.0; num_rules as usize]; input.read_f32_into::(&mut zone.passability_rules)?; - zone.num_zones = input.read_u32::()?; Ok(zone) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for MapZone { + fn write_to(&self, output: &mut W) -> Result<()> { for val in &self.info { output.write_i8(*val)?; } @@ -106,26 +157,6 @@ impl MapZone { output.write_u32::(self.num_zones)?; Ok(()) } - - pub fn info(&self) -> &[i8] { - assert_eq!(self.info.len(), 255); - &self.info - } - - pub fn info_mut(&mut self) -> &mut [i8] { - assert_eq!(self.info.len(), 255); - &mut self.info - } - - pub fn tiles(&self) -> &[i32] { - assert_eq!(self.tiles.len(), 255); - &self.tiles - } - - pub fn tiles_mut(&mut self) -> &mut [i32] { - assert_eq!(self.tiles.len(), 255); - &mut self.tiles - } } /// @@ -139,8 +170,8 @@ pub struct VisibilityMap { pub visibility: Vec, } -impl VisibilityMap { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for VisibilityMap { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let width = input.read_u32::()?; let height = input.read_u32::()?; let mut visibility = vec![0; (width * height).try_into().unwrap()]; @@ -151,8 +182,10 @@ impl VisibilityMap { visibility, }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for VisibilityMap { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u32::(self.width)?; output.write_u32::(self.height)?; for value in &self.visibility { @@ -181,26 +214,29 @@ pub struct Map { pub visibility: VisibilityMap, } -impl Map { +impl ReadableHeaderElement for Map { /// Read map data from an input stream. - pub fn read_from(mut input: impl Read) -> Result { - let mut map = Self::default(); - map.width = input.read_u32::()?; - map.height = input.read_u32::()?; + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut map = Map { + width: input.read_u32::()?, + height: input.read_u32::()?, + ..Default::default() + }; + + input.set_map_size(map.width, map.height); let num_zones = input.read_u32::()?; map.zones = Vec::with_capacity(num_zones.try_into().unwrap()); for _ in 0..num_zones { - map.zones - .push(MapZone::read_from(&mut input, (map.width, map.height))?); + map.zones.push(MapZone::read_from(input)?); } map.all_visible = input.read_u8()? != 0; map.fog_of_war = input.read_u8()? != 0; - map.tiles = Vec::with_capacity((map.width * map.height).try_into().unwrap()); - for _ in 0..(map.width * map.height) { - map.tiles.push(Tile::read_from(&mut input)?); + map.tiles = Vec::with_capacity(input.tile_count()); + for _ in 0..input.tile_count() { + map.tiles.push(Tile::read_from(input)?); } - let _umv = { + { let data_count = input.read_u32::()?; let _capacity = input.read_u32::()?; input.skip(u64::from(data_count) * 4)?; @@ -210,13 +246,8 @@ impl Map { } }; - map.visibility = VisibilityMap::read_from(&mut input)?; + map.visibility = VisibilityMap::read_from(input)?; Ok(map) } - - /// Write map data to an output stream. - pub fn write_to(&self, _output: impl Write) -> Result<()> { - unimplemented!() - } } diff --git a/crates/genie-rec/src/parser.rs b/crates/genie-rec/src/parser.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/genie-rec/src/parser.rs @@ -0,0 +1 @@ + diff --git a/crates/genie-rec/src/player.rs b/crates/genie-rec/src/player.rs index eab2855..7f6683d 100644 --- a/crates/genie-rec/src/player.rs +++ b/crates/genie-rec/src/player.rs @@ -1,15 +1,59 @@ use crate::ai::PlayerAI; +use crate::element::{OptionalReadableElement, ReadableHeaderElement, WritableHeaderElement}; +use crate::error::Error; +use crate::reader::RecordingHeaderReader; use crate::unit::Unit; use crate::unit_type::CompactUnitType; +use crate::GameVariant::DefinitiveEdition; use crate::{ObjectID, PlayerID, Result}; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use genie_dat::tech_tree::TechTree; use genie_dat::CivilizationID; use genie_scx::VictoryConditions; +use genie_support::ReadSkipExt; use genie_support::{read_opt_u32, ReadStringsExt}; use std::convert::TryInto; use std::io::{Read, Write}; +// TODO! reset to warn before merging +#[allow(unused_macro_rules)] +macro_rules! assert_marker { + ($val:expr, $marker:expr) => { + let found = $val.read_u8()?; + if found != $marker { + let mut skipped: u64 = 1; + while $val.read_u8()? != $marker { + skipped += 1; + } + + return Err(Error::HeaderError( + $val.position() as u64 - skipped, + Box::new(Error::MissingMarker( + $val.version(), + $marker as u128, + found as u128, + file!(), + line!(), + skipped, + )), + )); + } + }; + + ($found:ident eq $marker:expr, $version:expr) => { + if $found != $marker { + return Err(Error::MissingMarker( + $version, + $marker as u128, + $found as u128, + file!(), + line!(), + 0, + )); + } + }; +} + #[derive(Debug, Default, Clone)] pub struct Player { player_type: u8, @@ -46,36 +90,49 @@ impl Player { &self.name } - #[allow(clippy::cognitive_complexity)] - pub fn read_from(mut input: impl Read, version: f32, num_players: u8) -> Result { - let mut player = Self::default(); + pub fn read_info(&mut self, input: &mut RecordingHeaderReader) -> Result<()> { + self.victory = VictoryConditions::read_from(input, true)?; + Ok(()) + } +} - player.player_type = input.read_u8()?; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); +impl ReadableHeaderElement for Player { + #[allow(clippy::cognitive_complexity)] + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut player = Player { + player_type: input.read_u8()?, + ..Default::default() + }; + if input.version() >= 10.55 { + assert_marker!(input, 11); } - player.relations = vec![0; usize::from(num_players)]; + player.relations = vec![0; input.num_players() as usize]; input.read_exact(&mut player.relations)?; input.read_u32_into::(&mut player.diplomacy)?; player.allied_los = input.read_u32::()? != 0; player.allied_victory = input.read_u8()? != 0; - player.name = input - .read_u16_length_prefixed_str()? - .unwrap_or_else(String::new); - if version >= 10.55 { - assert_eq!(input.read_u8()?, 22); + player.name = input.read_u16_length_prefixed_str()?.unwrap_or_default(); + + if input.version() >= 10.55 { + assert_marker!(input, 22); } let num_attributes = input.read_u32::()?; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 33); + if input.version() >= 10.55 { + assert_marker!(input, 33); } player.attributes = vec![0.0; num_attributes.try_into().unwrap()]; input.read_f32_into::(&mut player.attributes)?; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + + if input.variant() >= DefinitiveEdition { + input.skip((num_attributes * 4) as u64)?; } + + if input.version() >= 10.55 { + assert_marker!(input, 11); + } + player.initial_view = (input.read_f32::()?, input.read_f32::()?); - if version >= 11.62 { + if input.version() >= 11.62 { let num_saved_views = input.read_i32::()?; // saved view count can be negative player.saved_views = vec![(0.0, 0.0); num_saved_views.try_into().unwrap_or(0)]; @@ -88,24 +145,25 @@ impl Player { player.civilization_id = input.read_u8()?.into(); player.game_status = input.read_u8()?; player.resigned = input.read_u8()? != 0; - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } let _color = input.read_u8()?; - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } let _pathing_attempt_cap = input.read_u32::()?; let _pathing_delay_cap = input.read_u32::()?; // Unit counts - let counts = if version >= 11.65 { + let counts = if input.version() >= 11.65 { (900, 100, 900, 100) - } else if version >= 11.51 { + } else if input.version() >= 11.51 { (850, 100, 850, 100) } else { (750, 100, 750, 100) }; + let mut object_categories_count = vec![0; counts.0]; input.read_u16_into::(&mut object_categories_count)?; let mut object_groups_count = vec![0; counts.1]; @@ -128,7 +186,7 @@ impl Player { let _column_to_line_distance = input.read_u32::()?; let _auto_formations = input.read_u32::()?; let _formations_influence_distance = input.read_f32::()?; - let _break_auto_formations_by_speed = if version >= 10.81 { + let _break_auto_formations_by_speed = if input.version() >= 10.81 { input.read_f32::()? } else { 0.0 @@ -155,7 +213,7 @@ impl Player { ); // view scrolling - if version >= 10.51 { + if input.version() >= 10.51 { let _scroll_vector = (input.read_f32::()?, input.read_f32::()?); let _scroll_end = (input.read_f32::()?, input.read_f32::()?); let _scroll_start = (input.read_f32::()?, input.read_f32::()?); @@ -164,14 +222,14 @@ impl Player { } // AI state - if version >= 11.45 { + if input.version() >= 11.45 { let _easiest_reaction_percent = input.read_f32::()?; let _easier_reaction_percent = input.read_f32::()?; let _task_ungrouped_soldiers = input.read_u8()? != 0; } // selected units - if version >= 11.72 { + if input.version() >= 11.72 { let num_selections = input.read_u32::()?; let _selection = if num_selections > 0 { let object_id: ObjectID = input.read_u32::()?.into(); @@ -186,17 +244,50 @@ impl Player { }; } - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); - assert_eq!(input.read_u8()?, 11); + if input.variant() == DefinitiveEdition { + let mut pre_skipped = 32435; + if input.version() >= 13.0 { + pre_skipped += 8; + } + + if input.version() >= 13.07 { + pre_skipped += 1; + } + + if input.version() >= 13.13 { + pre_skipped += 5; + } + + if input.version() >= 13.34 { + pre_skipped += 4; + } + + input.skip(pre_skipped)?; + // I am crying + loop { + let first = input.read_u8()?; + if first != 11 { + continue; + } + let second = input.read_u8()?; + if second != 11 { + continue; + } + + break; + } + } else if input.version() >= 10.55 { + assert_marker!(input, 11); + assert_marker!(input, 11); } let _ty = input.read_u8()?; + let _update_count = input.read_u32::()?; let _update_count_need_help = input.read_u32::()?; // ai attack data - if version >= 10.02 { + if input.version() >= 10.02 { let _alerted_enemy_count = input.read_u32::()?; let _regular_attack_count = input.read_u32::()?; let _regular_attack_mode = input.read_u8()?; @@ -208,28 +299,30 @@ impl Player { let _fog_update = input.read_u32::()?; let _update_time = input.read_f32::()?; - // if is userpatch - if genie_support::f32_eq!(version, 11.97) { - player.userpatch_data = Some(UserPatchData::read_from(&mut input)?); + if genie_support::f32_eq!(input.version(), 11.97) { + player.userpatch_data = Some(UserPatchData::read_from(input)?); } - player.tech_state = PlayerTech::read_from(&mut input)?; + player.tech_state = PlayerTech::read_from(input)?; let _update_history_count = input.read_u32::()?; - player.history_info = HistoryInfo::read_from(&mut input, version)?; - if version >= 5.30 { + assert_marker!(input, 11); + + player.history_info = HistoryInfo::read_from(input)?; + + if input.version() >= 5.30 { let _ruin_held_time = input.read_u32::()?; let _artifact_held_time = input.read_u32::()?; } - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } // diplomacy - if version >= 9.13 { + if input.version() >= 9.13 { let mut diplomacy = [0; 9]; let mut intelligence = [0; 9]; let mut trade = [0; 9]; @@ -239,32 +332,32 @@ impl Player { intelligence[i] = input.read_u8()?; trade[i] = input.read_u8()?; - offer.push(DiplomacyOffer::read_from(&mut input)?); + offer.push(DiplomacyOffer::read_from(input)?); } let _fealty = input.read_u16::()?; } - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } // off-map trade - if version >= 9.17 { + if input.version() >= 9.17 { let mut off_map_trade_route_explored = [0; 20]; input.read_exact(&mut off_map_trade_route_explored)?; } - if version >= 9.18 { + if input.version() >= 9.18 { let mut off_map_trade_route_being_explored = [0; 20]; input.read_exact(&mut off_map_trade_route_being_explored)?; } - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } // market trading - if version >= 9.22 { + if input.version() >= 9.22 { let _max_trade_amount = input.read_u32::()?; let _old_max_trade_amount = input.read_u32::()?; let _max_trade_limit = input.read_u32::()?; @@ -278,35 +371,35 @@ impl Player { let _trade_refresh_rate = input.read_u32::()?; } - let _prod_queue_enabled = if version >= 9.67 { + let _prod_queue_enabled = if input.version() >= 9.67 { input.read_u8()? != 0 } else { true }; // ai dodging ability - if version >= 9.90 { + if input.version() >= 9.90 { let _chance_to_dodge_missiles = input.read_u8()?; let _chance_for_archers_to_maintain_distance = input.read_u8()?; } - let _open_gates_for_pathing_count = if version >= 11.42 { + let _open_gates_for_pathing_count = if input.version() >= 11.42 { input.read_u32::()? } else { 0 }; - let _farm_queue_count = if version >= 11.57 { + let _farm_queue_count = if input.version() >= 11.57 { input.read_u32::()? } else { 0 }; - let _nomad_build_lock = if version >= 11.75 { + let _nomad_build_lock = if input.version() >= 11.75 { input.read_u32::()? != 0 } else { false }; - if version >= 9.30 { + if input.version() >= 9.30 { let _old_kills = input.read_u32::()?; let _old_razings = input.read_u32::()?; let _battle_mode = input.read_u32::()?; @@ -315,44 +408,57 @@ impl Player { let _total_razings = input.read_u32::()?; } - if version >= 9.31 { + if input.version() >= 9.31 { let _old_hit_points = input.read_u32::()?; let _total_hit_points = input.read_u32::()?; } - if version >= 9.32 { + if input.version() >= 9.32 { let mut old_player_kills = [0; 9]; input.read_u32_into::(&mut old_player_kills)?; } - player.tech_tree = if version >= 9.38 { - Some(TechTree::read_from(&mut input)?) + if input.variant() >= DefinitiveEdition { + input.skip(11)?; + } + + player.tech_tree = if input.version() >= 9.38 { + Some(TechTree::read_from(&mut *input)?) } else { None }; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.variant() >= DefinitiveEdition { + // Just, random 4 zero bytes :) + input.skip(4)?; + + if player.player_type != 2 { + input.skip(4)?; + } + } + + if input.version() >= 10.55 { + assert_marker!(input, 11); } let _player_ai = if player.player_type == 3 && input.read_u32::()? == 1 { - Some(PlayerAI::read_from(&mut input, version)?) + Some(PlayerAI::read_from(input)?) } else { None }; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } player.gaia = if player.player_type == 2 { - Some(GaiaData::read_from(&mut input)?) + Some(GaiaData::read_from(input)?) } else { None }; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } let num_unit_types = input.read_u32::()?; @@ -361,8 +467,8 @@ impl Player { *available = input.read_u32::()? != 0; } - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } player.unit_types.reserve(available_unit_types.len()); @@ -370,25 +476,25 @@ impl Player { player.unit_types.push(if !available { None } else { - if version >= 10.55 { - assert_eq!(input.read_u8()?, 22); + if input.version() >= 10.55 { + assert_marker!(input, 22); } - let ty = CompactUnitType::read_from(&mut input, version)?; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 33); + let ty = CompactUnitType::read_from(input)?; + if input.version() >= 10.55 { + assert_marker!(input, 33); } Some(ty) }); } - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } - player.visible_map = VisibleMap::read_from(&mut input, version)?; - player.visible_resources = VisibleResources::read_from(&mut input)?; + player.visible_map = VisibleMap::read_from(input)?; + player.visible_resources = VisibleResources::read_from(input)?; - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } @@ -396,27 +502,27 @@ impl Player { let _list_size = input.read_u32::()?; let _grow_size = input.read_u32::()?; let mut units = vec![]; - while let Some(unit) = Unit::read_from(&mut input, version)? { + while let Some(unit) = Unit::read_from(input)? { units.push(unit); } units }; - if version >= 10.55 { - assert_eq!(input.read_u8()?, 11); + if input.version() >= 10.55 { + assert_marker!(input, 11); } player.sleeping_units = { let _list_size = input.read_u32::()?; let _grow_size = input.read_u32::()?; let mut units = vec![]; - while let Some(unit) = Unit::read_from(&mut input, version)? { + while let Some(unit) = Unit::read_from(input)? { units.push(unit); } units }; - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } @@ -424,23 +530,18 @@ impl Player { let _list_size = input.read_u32::()?; let _grow_size = input.read_u32::()?; let mut units = vec![]; - while let Some(unit) = Unit::read_from(&mut input, version)? { + while let Some(unit) = Unit::read_from(input)? { units.push(unit); } units }; - if version >= 10.55 { + if input.version() >= 10.55 { assert_eq!(input.read_u8()?, 11); } Ok(player) } - - pub fn read_info(&mut self, input: impl Read, _version: f32) -> Result<()> { - self.victory = VictoryConditions::read_from(input, true)?; - Ok(()) - } } #[derive(Debug, Default, Clone)] @@ -449,20 +550,28 @@ pub struct VisibleMap { pub height: u32, pub explored_tiles_count: u32, pub player_id: PlayerID, - pub tiles: Vec, + pub tiles: Vec, } -impl VisibleMap { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut map = Self::default(); - map.width = input.read_u32::()?; - map.height = input.read_u32::()?; - if version >= 6.70 { +impl ReadableHeaderElement for VisibleMap { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut map = VisibleMap { + width: input.read_u32::()?, + height: input.read_u32::()?, + ..Default::default() + }; + if input.version() >= 6.70 { map.explored_tiles_count = input.read_u32::()?; } map.player_id = input.read_u16::()?.try_into().unwrap(); - map.tiles = vec![0; (map.width * map.height).try_into().unwrap()]; - input.read_i8_into(&mut map.tiles)?; + if input.variant() >= DefinitiveEdition { + map.tiles = vec![0; (map.width * map.height) as usize]; + input.read_i16_into::(&mut map.tiles)?; + } else { + let mut tiles = vec![0i8; (map.width * map.height) as usize]; + input.read_i8_into(&mut tiles)?; + map.tiles.extend(tiles.iter().map(|x| *x as i16)); + } Ok(map) } } @@ -475,14 +584,14 @@ pub struct VisibleResource { pub location: (u8, u8), } -impl VisibleResource { - pub fn read_from(mut input: impl Read) -> Result { - let mut vis = Self::default(); - vis.object_id = input.read_u32::()?.into(); - vis.distance = input.read_u8()?; - vis.zone = input.read_i8()?; - vis.location = (input.read_u8()?, input.read_u8()?); - Ok(vis) +impl ReadableHeaderElement for VisibleResource { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(VisibleResource { + object_id: input.read_u32::()?.into(), + distance: input.read_u8()?, + zone: input.read_i8()?, + location: (input.read_u8()?, input.read_u8()?), + }) } } @@ -491,8 +600,8 @@ pub struct VisibleResources { lists: Vec>, } -impl VisibleResources { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for VisibleResources { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let num_lists = input.read_u32::()?; let mut sizes = vec![]; for _ in 0..num_lists { @@ -503,7 +612,7 @@ impl VisibleResources { for size in sizes { let mut list = Vec::with_capacity(size.try_into().unwrap()); for _ in 0..size { - list.push(VisibleResource::read_from(&mut input)?); + list.push(VisibleResource::read_from(input)?); } lists.push(list); } @@ -530,13 +639,15 @@ pub struct GaiaData { wolf_counts: [u32; 10], } -impl GaiaData { - pub fn read_from(mut input: impl Read) -> Result { - let mut gaia = Self::default(); - gaia.update_time = input.read_u32::()?; - gaia.update_nature = input.read_u32::()?; +impl ReadableHeaderElement for GaiaData { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut gaia = GaiaData { + update_time: input.read_u32::()?, + update_nature: input.read_u32::()?, + ..Default::default() + }; for creature in gaia.creatures.iter_mut() { - *creature = GaiaCreature::read_from(&mut input)?; + *creature = GaiaCreature::read_from(input)?; } gaia.next_wolf_attack_update_time = input.read_u32::()?; gaia.wolf_attack_update_interval = input.read_u32::()?; @@ -552,12 +663,12 @@ impl GaiaData { for v in gaia.wolf_current_villagers.iter_mut() { *v = input.read_u32::()?; } - gaia.wolf_current_villager = read_opt_u32(&mut input)?; + gaia.wolf_current_villager = read_opt_u32(input)?; gaia.wolf_villager_count = input.read_u32::()?; for wolf in gaia.wolves.iter_mut() { - *wolf = GaiaWolfInfo::read_from(&mut input)?; + *wolf = GaiaWolfInfo::read_from(input)?; } - gaia.current_wolf = read_opt_u32(&mut input)?; + gaia.current_wolf = read_opt_u32(input)?; input.read_u32_into::(&mut gaia.wolf_counts[..])?; Ok(gaia) } @@ -570,16 +681,18 @@ pub struct GaiaCreature { pub max: u32, } -impl GaiaCreature { - pub fn read_from(mut input: impl Read) -> Result { - let mut creature = Self::default(); - creature.growth_rate = input.read_f32::()?; - creature.remainder = input.read_f32::()?; - creature.max = input.read_u32::()?; - Ok(creature) +impl ReadableHeaderElement for GaiaCreature { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(GaiaCreature { + growth_rate: input.read_f32::()?, + remainder: input.read_f32::()?, + max: input.read_u32::()?, + }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for GaiaCreature { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.growth_rate)?; output.write_f32::(self.remainder)?; output.write_u32::(self.max)?; @@ -593,15 +706,17 @@ pub struct GaiaWolfInfo { pub distance: f32, } -impl GaiaWolfInfo { - pub fn read_from(mut input: impl Read) -> Result { - let mut wolf = Self::default(); - wolf.id = input.read_u32::()?; - wolf.distance = input.read_f32::()?; - Ok(wolf) +impl ReadableHeaderElement for GaiaWolfInfo { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(GaiaWolfInfo { + id: input.read_u32::()?, + distance: input.read_f32::()?, + }) } +} - pub fn write_to(self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for GaiaWolfInfo { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u32::(self.id)?; output.write_f32::(self.distance)?; Ok(()) @@ -627,22 +742,24 @@ struct DiplomacyOffer { status: u8, } -impl DiplomacyOffer { - pub fn read_from(mut input: impl Read) -> Result { - let mut offer = Self::default(); - offer.sequence = input.read_u8()?; - offer.started_by = input.read_u8()?; - offer.actual_time = 0; - offer.game_time = input.read_u32::()?; - offer.declare = input.read_u8()?; - offer.old_diplomacy = input.read_u8()?; - offer.new_diplomacy = input.read_u8()?; - offer.old_intelligence = input.read_u8()?; - offer.new_intelligence = input.read_u8()?; - offer.old_trade = input.read_u8()?; - offer.new_trade = input.read_u8()?; - offer.demand = input.read_u8()?; - offer.gold = input.read_u32::()?; +impl ReadableHeaderElement for DiplomacyOffer { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut offer = DiplomacyOffer { + sequence: input.read_u8()?, + started_by: input.read_u8()?, + actual_time: 0, + game_time: input.read_u32::()?, + declare: input.read_u8()?, + old_diplomacy: input.read_u8()?, + new_diplomacy: input.read_u8()?, + old_intelligence: input.read_u8()?, + new_intelligence: input.read_u8()?, + old_trade: input.read_u8()?, + new_trade: input.read_u8()?, + demand: input.read_u8()?, + gold: input.read_u32::()?, + ..Default::default() + }; let message_len = input.read_u8()?; offer.message = input.read_str(usize::from(message_len))?; offer.status = input.read_u8()?; @@ -656,23 +773,24 @@ pub struct HistoryInfo { pub events: Vec, } -impl HistoryInfo { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let _padding = input.read_u8()?; +impl ReadableHeaderElement for HistoryInfo { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + // TODO: Check for padding + // let _padding = input.read_u8()?; let num_entries = input.read_u32::()?; let _num_events = input.read_u32::()?; let entries_capacity = input.read_u32::()?; let mut entries = Vec::with_capacity(entries_capacity.try_into().unwrap()); for _ in 0..num_entries { - entries.push(HistoryEntry::read_from(&mut input, version)?); + entries.push(HistoryEntry::read_from(input)?); } - let _padding = input.read_u8()?; - + // 22 for gaia, but 0 for players? + let _value = input.read_u8()?; let num_events = input.read_u32::()?; let mut events = Vec::with_capacity(num_events.try_into().unwrap()); for _ in 0..num_events { - events.push(HistoryEvent::read_from(&mut input)?); + events.push(HistoryEvent::read_from(input)?); } let _razings = input.read_i32::()?; @@ -720,18 +838,18 @@ pub struct HistoryEvent { pub params: (f32, f32, f32), } -impl HistoryEvent { - pub fn read_from(mut input: impl Read) -> Result { - let mut event = Self::default(); - event.event_type = input.read_i8()?; - event.time_slice = input.read_u32::()?; - event.world_time = input.read_u32::()?; - event.params = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - Ok(event) +impl ReadableHeaderElement for HistoryEvent { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(HistoryEvent { + event_type: input.read_i8()?, + time_slice: input.read_u32::()?, + world_time: input.read_u32::()?, + params: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + }) } } @@ -741,10 +859,11 @@ pub struct HistoryEntry { pub military_population: u16, } -impl HistoryEntry { - pub fn read_from(mut input: impl Read, _version: f32) -> Result { +impl ReadableHeaderElement for HistoryEntry { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let civilian_population = input.read_u16::()?; let military_population = input.read_u16::()?; + Ok(HistoryEntry { civilian_population, military_population, @@ -760,17 +879,23 @@ pub struct TechState { pub time_modifier: i16, } -impl TechState { - pub fn read_from(mut input: impl Read) -> Result { - let mut state = Self::default(); - state.progress = input.read_f32::()?; - state.state = input.read_i16::()?; - state.modifiers = ( - input.read_i16::()?, - input.read_i16::()?, - input.read_i16::()?, - ); - state.time_modifier = input.read_i16::()?; +impl ReadableHeaderElement for TechState { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let state = TechState { + progress: input.read_f32::()?, + state: input.read_i16::()?, + modifiers: ( + input.read_i16::()?, + input.read_i16::()?, + input.read_i16::()?, + ), + time_modifier: input.read_i16::()?, + }; + + if input.variant() >= DefinitiveEdition { + input.skip(15)?; + } + Ok(state) } } @@ -780,12 +905,13 @@ pub struct PlayerTech { pub tech_states: Vec, } -impl PlayerTech { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for PlayerTech { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let num_techs = input.read_u16::()?; + let mut tech_states = Vec::with_capacity(usize::from(num_techs)); for _ in 0..num_techs { - tech_states.push(TechState::read_from(&mut input)?); + tech_states.push(TechState::read_from(input)?); } Ok(Self { tech_states }) } @@ -794,8 +920,8 @@ impl PlayerTech { #[derive(Debug, Clone)] pub struct UserPatchData {} -impl UserPatchData { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for UserPatchData { + fn read_from(input: &mut RecordingHeaderReader) -> Result { { let mut bytes = vec![0; 4080]; input.read_exact(&mut bytes)?; diff --git a/crates/genie-rec/src/reader.rs b/crates/genie-rec/src/reader.rs new file mode 100644 index 0000000..d8e281a --- /dev/null +++ b/crates/genie-rec/src/reader.rs @@ -0,0 +1,164 @@ +use crate::{GameVariant, GameVersion}; +use std::io::{BufRead, BufReader, Read}; +use std::{cmp::min, io::Seek}; +use std::{io, slice}; + +pub trait Peek { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]>; +} + +#[derive(Debug)] +/// Light wrapper around a reader, which allows us to store state +pub struct RecordingHeaderReader { + /// Inner reader, BufReader with peeking support + inner: BufReader, + /// Current state tracker by reader, stores version and map info + state: RecordingState, + /// Our current position in the header + position: usize, +} + +impl RecordingHeaderReader { + pub fn new(inner: R) -> Self { + Self { + inner: BufReader::new(inner), + state: Default::default(), + position: 0, + } + } + + pub fn position(&self) -> usize { + self.position + } + + pub fn version(&self) -> f32 { + self.state.version + } + + pub fn game_version(&self) -> GameVersion { + self.state.game_version + } + + pub fn variant(&self) -> GameVariant { + self.state.variant + } + + pub fn tile_count(&self) -> usize { + self.state.tile_count + } + + pub fn map_width(&self) -> u32 { + self.state.map_width + } + + pub fn map_height(&self) -> u32 { + self.state.map_height + } + + pub fn num_players(&self) -> u16 { + self.state.num_players + } + + pub fn set_version>(&mut self, game_version: V, version: f32) { + let game_version = game_version.into(); + // Should we actually throw here or smth? + if let Some(variant) = GameVariant::resolve_variant(&game_version, version) { + self.state.variant = variant; + } + + self.state.version = version; + self.state.game_version = game_version; + } + + pub fn set_map_size(&mut self, width: u32, height: u32) { + self.state.map_width = width; + self.state.map_height = height; + self.state.tile_count = width as usize * height as usize; + } + + pub fn set_num_players(&mut self, num_players: u16) { + self.state.num_players = num_players + } +} + +impl Read for RecordingHeaderReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let read = self.inner.read(buf)?; + self.position += read; + Ok(read) + } +} + +impl Peek for RecordingHeaderReader { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { + self.inner.peek(amount) + } +} + +impl Peek for BufReader { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { + let buffer = self.buffer(); + Ok(&buffer[..amount]) + } +} + +#[derive(Copy, Clone, Debug)] +struct RecordingState { + version: f32, + game_version: GameVersion, + variant: GameVariant, + num_players: u16, + map_width: u32, + map_height: u32, + /// width * height, for ease of use + tile_count: usize, +} + +impl Default for RecordingState { + fn default() -> Self { + RecordingState { + version: 0.0, + game_version: Default::default(), + variant: GameVariant::Trial, + num_players: 0, + map_width: 0, + map_height: 0, + tile_count: 0, + } + } +} + +#[cfg(test)] +mod tests { + use crate::reader::Peek; + use std::io::{BufReader, Cursor, Read}; + + #[test] + pub fn test_inflatable_buffer() { + let data = (0..20).into_iter().collect::>(); + let cursor = Cursor::new(data); + let mut buffered_reader = BufReader::with_capacity(10, cursor); + + let mut buffer = [0; 4]; + buffered_reader + .read_exact(&mut buffer) + .expect("Failed to read"); + assert_eq!(&[0, 1, 2, 3], &buffer); + assert_eq!(&[4, 5], buffered_reader.peek(2).expect("Failed to peek")); + assert_eq!( + &[4, 5, 6, 7], + buffered_reader.peek(4).expect("Failed to peek") + ); + assert_eq!( + &[4, 5, 6, 7, 8, 9], + buffered_reader.peek(6).expect("Failed to peek") + ); + assert_eq!(&[4, 5, 6], buffered_reader.peek(3).expect("Failed to peek")); + + let mut buffer = [0; 8]; + buffered_reader + .read_exact(&mut buffer) + .expect("Failed to read"); + assert_eq!(&[4, 5, 6, 7, 8, 9, 10, 11], &buffer); + } +} diff --git a/crates/genie-rec/src/reader.rs.old b/crates/genie-rec/src/reader.rs.old new file mode 100644 index 0000000..45f069b --- /dev/null +++ b/crates/genie-rec/src/reader.rs.old @@ -0,0 +1,379 @@ +use crate::{GameVariant, GameVersion}; +use itertools::{multipeek, Itertools, PeekingNext}; +use std::io::{BufRead, BufReader, Read}; +use std::{cmp::min, io::Seek}; +use std::{io, slice}; + +pub trait Peek { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]>; +} + +// /// A struct implementing `BufRead` that uses a small, single-use, stack-allocated buffer, intended +// /// for reading only the first few bytes from a file. +// pub(crate) struct SmallBufReader +// where +// R: Read, +// { +// buffer: [u8; 256], +// pointer: usize, +// reader: R, +// } + +// impl SmallBufReader +// where +// R: Read, +// { +// pub(crate) fn new(reader: R) -> Self { +// Self { +// buffer: [0; 256], +// pointer: 0, +// reader, +// } +// } +// } + +// impl Read for SmallBufReader +// where +// R: Read, +// { +// fn read(&mut self, output: &mut [u8]) -> io::Result { +// self.reader.read(output) +// } +// } + +// impl BufRead for SmallBufReader +// where +// R: Read, +// { +// fn fill_buf(&mut self) -> io::Result<&[u8]> { +// self.reader.read_exact(&mut self.buffer[self.pointer..])?; +// Ok(&self.buffer[self.pointer..]) +// } + +// fn consume(&mut self, len: usize) { +// self.pointer += len; +// } +// } + +// /// Dynamically allocated buffered reader, by method of "inflation" +// /// The inflation allows us to "peek" into streams, without destroying data +// /// Reading from the buffer also only advances the pointer inside the buffer +// /// +// /// Only once another peek is done, data is rearranged (or simply overwritten +// /// if the whole buffer has been consumed) +// #[derive(Debug)] +// pub struct InflatableReader { +// /// inner reader +// inner: R, +// /// buffer for peek support, only inflates when needed +// inflatable_buffer: Vec, +// /// Position in our inflatable buffer +// position_in_buffer: usize, +// } + +// impl InflatableReader { +// pub fn new(inner: R) -> Self { +// Self::new_with_capacity(inner, 4096) +// } + +// pub fn new_with_capacity(inner: R, capacity: usize) -> Self { +// InflatableReader { +// inner, +// inflatable_buffer: Vec::with_capacity(capacity), +// position_in_buffer: 0, +// } +// } +// } + +// impl Peek for &mut InflatableReader { +// fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { +// (*self).peek(amount) +// } +// } + +// impl Peek for InflatableReader { +// /// Peek into inner reader, returns a slice owned by the [InflatableReader], +// /// if data isn't available in the buffer yet, it will read it into the inner buffer +// /// and inflate the buffer if needed. +// fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { +// // pub struct InflatableReader { +// // /// inner reader +// // inner: R, +// // /// buffer for peek support, only inflates when needed +// // inflatable_buffer: Vec, +// // /// Position in our inflatable buffer +// // position_in_buffer: usize, +// // } + +// // buffer is initialised by `read_exact`, so we have our window with a view +// // into the data + +// // now we want to check what values come after the data +// // 1. so we get the outer right bound of the view into the data + +// // 2. from this bound we want to peek for amount of values into .inner +// // so we have right_outer_bound of buffer + +// use std::io::BufReader; +// let mut reader = BufReader::new(f); + +// // Cache this info, since we change `position_in_buffer` +// let buffered_data_length = self.inflatable_buffer.len() - self.position_in_buffer; + +// // quick return because we have all the data to peek already +// if buffered_data_length >= amount { +// return Ok( +// &self.inflatable_buffer[self.position_in_buffer..self.position_in_buffer + amount] +// ); +// } + +// // from this point on we can assume that we need to allocate more, and [amount] is always the bigger value + +// // see how much we're missing +// let missing = amount - buffered_data_length; + +// // we were at the end of our buffer, just reset the position without resizing yet. +// if self.position_in_buffer == self.inflatable_buffer.len() { +// self.position_in_buffer = 0; +// } else { +// // if we have enough capacity to house the missing data, just skip this part +// if self.inflatable_buffer.capacity() >= (missing + self.inflatable_buffer.len()) { +// let inflatable_buffer_len = self.inflatable_buffer.len(); +// // copy the data to the front, so we can allocate the least amount of data, +// // or, skip the allocation altogether if we're lucky :) +// self.inflatable_buffer +// .copy_within(self.position_in_buffer..inflatable_buffer_len, 0); +// self.position_in_buffer = buffered_data_length; +// } +// } + +// // // Check what the length would be of our new buffer, and resize if needed +// // let new_length = self.position_in_buffer + amount; +// // if new_length != self.inflatable_buffer.len() { +// // // BUG: the old data needs to be kept here +// // // but instead is overwritten with 0 +// // self.inflatable_buffer.resize(new_length, 1); +// // } + +// // Check what the length would be of our new buffer, and resize if needed +// let new_length = self.position_in_buffer + amount; +// if new_length > self.inflatable_buffer.len() { +// // BUG: the old data needs to be kept here +// // but instead is overwritten with 0 +// let mut tmp = self.inflatable_buffer.clone().into_iter(); +// self.inflatable_buffer.resize_with(new_length, || { +// if let Some(val) = tmp.next() { +// val +// } else { +// u8::default() +// } +// }); +// } else { +// self.inflatable_buffer.resize(new_length, u8::default()) +// } + +// // read the missing data +// let actually_read = self +// .inner +// .read(&mut self.inflatable_buffer[self.position_in_buffer + buffered_data_length..])?; +// // if e.g. end of file or reading from network stream +// if actually_read != missing { +// // will always be shorter +// self.inflatable_buffer +// .truncate(self.position_in_buffer + buffered_data_length + actually_read); +// } + +// Ok(&self.inflatable_buffer[self.position_in_buffer +// ..self.position_in_buffer + buffered_data_length + actually_read]) +// } +// } + +// impl Read for InflatableReader { +// fn read(&mut self, buf: &mut [u8]) -> io::Result { +// if buf.is_empty() { +// return Ok(0); +// } + +// let fulfilled: usize = if self.inflatable_buffer.len() != self.position_in_buffer { +// let to_consume = min( +// buf.len(), +// self.inflatable_buffer.len() - self.position_in_buffer, +// ); +// buf[..to_consume].copy_from_slice( +// &self.inflatable_buffer +// [self.position_in_buffer..to_consume + self.position_in_buffer], +// ); +// to_consume +// } else { +// 0 +// }; + +// if buf.len() - fulfilled == 0 { +// self.position_in_buffer += fulfilled; +// return Ok(fulfilled); +// } + +// match self.inner.read(&mut buf[fulfilled..]) { +// Ok(size) => { +// self.position_in_buffer += fulfilled; +// Ok(fulfilled + size) +// } +// err => err, +// } +// } +// } + +#[derive(Debug)] +/// Light wrapper around a reader, which allows us to store state +pub struct RecordingHeaderReader { + /// Inner reader, BufReader with peeking support + inner: BufReader, + /// Current state tracker by reader, stores version and map info + state: RecordingState, + /// Our current position in the header + position: usize, +} + +impl RecordingHeaderReader { + pub fn new(inner: R) -> Self { + Self { + inner: BufReader::new(inner), + state: Default::default(), + position: 0, + } + } + + pub fn position(&self) -> usize { + self.position + } + + pub fn version(&self) -> f32 { + self.state.version + } + + pub fn game_version(&self) -> GameVersion { + self.state.game_version + } + + pub fn variant(&self) -> GameVariant { + self.state.variant + } + + pub fn tile_count(&self) -> usize { + self.state.tile_count + } + + pub fn map_width(&self) -> u32 { + self.state.map_width + } + + pub fn map_height(&self) -> u32 { + self.state.map_height + } + + pub fn num_players(&self) -> u16 { + self.state.num_players + } + + pub fn set_version>(&mut self, game_version: V, version: f32) { + let game_version = game_version.into(); + // Should we actually throw here or smth? + if let Some(variant) = GameVariant::resolve_variant(&game_version, version) { + self.state.variant = variant; + } + + self.state.version = version; + self.state.game_version = game_version; + } + + pub fn set_map_size(&mut self, width: u32, height: u32) { + self.state.map_width = width; + self.state.map_height = height; + self.state.tile_count = width as usize * height as usize; + } + + pub fn set_num_players(&mut self, num_players: u16) { + self.state.num_players = num_players + } +} + +impl Read for RecordingHeaderReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let read = self.inner.read(buf)?; + self.position += read; + Ok(read) + } +} + +impl Peek for RecordingHeaderReader { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { + self.inner.peek(amount) + } +} + +impl Peek for BufReader { + fn peek(&mut self, amount: usize) -> io::Result<&[u8]> { + let buffer = self.buffer(); + Ok(&buffer[..amount]) + } +} + +#[derive(Copy, Clone, Debug)] +struct RecordingState { + version: f32, + game_version: GameVersion, + variant: GameVariant, + num_players: u16, + map_width: u32, + map_height: u32, + /// width * height, for ease of use + tile_count: usize, +} + +impl Default for RecordingState { + fn default() -> Self { + RecordingState { + version: 0.0, + game_version: Default::default(), + variant: GameVariant::Trial, + num_players: 0, + map_width: 0, + map_height: 0, + tile_count: 0, + } + } +} + +#[cfg(test)] +mod tests { + use crate::reader::Peek; + use std::io::{BufReader, Cursor, Read}; + + #[test] + pub fn test_inflatable_buffer() { + let data = (0..20).into_iter().collect::>(); + let cursor = Cursor::new(data); + let mut buffered_reader = BufReader::with_capacity(10, cursor); + + let mut buffer = [0; 4]; + buffered_reader + .read_exact(&mut buffer) + .expect("Failed to read"); + assert_eq!(&[0, 1, 2, 3], &buffer); + assert_eq!(&[4, 5], buffered_reader.peek(2).expect("Failed to peek")); + assert_eq!( + &[4, 5, 6, 7], + buffered_reader.peek(4).expect("Failed to peek") + ); + assert_eq!( + &[4, 5, 6, 7, 8, 9], + buffered_reader.peek(6).expect("Failed to peek") + ); + + let mut buffer = [0; 8]; + buffered_reader + .read_exact(&mut buffer) + .expect("Failed to read"); + assert_eq!(&[4, 5, 6, 7, 8, 9, 10, 11], &buffer); + } +} diff --git a/crates/genie-rec/src/string_table.rs b/crates/genie-rec/src/string_table.rs index 5c932d6..01b3005 100644 --- a/crates/genie-rec/src/string_table.rs +++ b/crates/genie-rec/src/string_table.rs @@ -1,3 +1,5 @@ +use crate::element::{ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::RecordingHeaderReader; use crate::Result; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::io::{Read, Write}; @@ -16,7 +18,23 @@ impl StringTable { } } - pub fn read_from(mut input: impl Read) -> Result { + pub fn max_strings(&self) -> u16 { + self.max_strings + } + + pub fn num_strings(&self) -> u16 { + let len = self.strings.len(); + assert!(len < u16::max_value() as usize); + len as u16 + } + + pub fn strings(&self) -> &Vec { + &self.strings + } +} + +impl ReadableHeaderElement for StringTable { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let max_strings = input.read_u16::()?; let num_strings = input.read_u16::()?; let _ptr = input.read_u32::()?; @@ -34,35 +52,23 @@ impl StringTable { strings, }) } +} - pub fn write_to(&self, handle: &mut W) -> Result<()> { +impl WritableHeaderElement for StringTable { + fn write_to(&self, handle: &mut W) -> Result<()> { handle.write_u16::(self.max_strings)?; handle.write_u16::(self.num_strings())?; handle.write_u32::(0)?; for string in &self.strings { let len = string.len(); - assert!(len < u32::max_value() as usize); + assert!(len < u32::MAX as usize); handle.write_u32::(len as u32)?; handle.write_all(string.as_bytes())?; } Ok(()) } - - pub fn max_strings(&self) -> u16 { - self.max_strings - } - - pub fn num_strings(&self) -> u16 { - let len = self.strings.len(); - assert!(len < u16::max_value() as usize); - len as u16 - } - - pub fn strings(&self) -> &Vec { - &self.strings - } } impl IntoIterator for StringTable { diff --git a/crates/genie-rec/src/unit.rs b/crates/genie-rec/src/unit.rs index 6402b75..1892a00 100644 --- a/crates/genie-rec/src/unit.rs +++ b/crates/genie-rec/src/unit.rs @@ -1,5 +1,8 @@ +use crate::element::{OptionalReadableElement, ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::{Peek, RecordingHeaderReader}; use crate::unit_action::UnitAction; use crate::unit_type::UnitBaseClass; +use crate::GameVariant::DefinitiveEdition; use crate::Result; use crate::{ObjectID, PlayerID}; use arrayvec::ArrayVec; @@ -8,7 +11,7 @@ pub use genie_dat::sprite::SpriteID; pub use genie_dat::terrain::TerrainID; pub use genie_dat::unit_type::AttributeCost; use genie_dat::unit_type::UnitType; -use genie_support::{read_opt_u32, ReadSkipExt}; +use genie_support::{read_opt_u32, ReadSkipExt, ReadStringsExt}; pub use genie_support::{StringKey, UnitTypeID}; use std::convert::TryInto; use std::io::{Read, Write}; @@ -26,14 +29,14 @@ pub struct Unit { pub building: Option, } -impl Unit { - pub fn read_from(mut input: impl Read, version: f32) -> Result> { +impl OptionalReadableElement for Unit { + fn read_from(input: &mut RecordingHeaderReader) -> Result> { let raw_class = input.read_u8()?; if raw_class == 0 { return Ok(None); } let unit_base_class = raw_class.try_into().unwrap(); - let static_ = StaticUnitAttributes::read_from(&mut input, version)?; + let static_ = StaticUnitAttributes::read_from(input)?; let mut unit = Self { unit_base_class, static_, @@ -46,50 +49,57 @@ impl Unit { building: None, }; if unit_base_class >= UnitBaseClass::Animated { - unit.animated = Some(AnimatedUnitAttributes::read_from(&mut input)?); + unit.animated = Some(AnimatedUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Moving { - unit.moving = Some(MovingUnitAttributes::read_from(&mut input, version)?); + unit.moving = Some(MovingUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Action { - unit.action = Some(ActionUnitAttributes::read_from(&mut input, version)?); + unit.action = Some(ActionUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::BaseCombat { - unit.base_combat = Some(BaseCombatUnitAttributes::read_from(&mut input, version)?); + unit.base_combat = Some(BaseCombatUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Missile { - unit.missile = Some(MissileUnitAttributes::read_from(&mut input, version)?); + unit.missile = Some(MissileUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Combat { - unit.combat = Some(CombatUnitAttributes::read_from(&mut input, version)?); + unit.combat = Some(CombatUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Building { - unit.building = Some(BuildingUnitAttributes::read_from(&mut input, version)?); + unit.building = Some(BuildingUnitAttributes::read_from(input)?); } + + if unit_base_class == UnitBaseClass::Moving && input.variant() >= DefinitiveEdition { + input.skip(17)?; + } + Ok(Some(unit)) } +} - pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> { +impl WritableHeaderElement for Unit { + fn write_to(&self, output: &mut W) -> Result<()> { let raw_class = self.unit_base_class as u8; output.write_u8(raw_class)?; - self.static_.write_to(&mut output, version)?; + self.static_.write_to(output)?; if let Some(animated) = &self.animated { - animated.write_to(&mut output)?; + animated.write_to(output)?; } if let Some(moving) = &self.moving { - moving.write_to(&mut output)?; + moving.write_to(output)?; } if let Some(action) = &self.action { - action.write_to(&mut output, version)?; + action.write_to(output)?; } if let Some(base_combat) = &self.base_combat { - base_combat.write_to(&mut output, version)?; + base_combat.write_to(output)?; } if let Some(missile) = &self.missile { - missile.write_to(&mut output, version)?; + missile.write_to(output)?; } if let Some(combat) = &self.combat { - combat.write_to(&mut output, version)?; + combat.write_to(output)?; } Ok(()) } @@ -106,20 +116,22 @@ pub struct SpriteNodeAnimation { pub last_speed: f32, } -impl SpriteNodeAnimation { - pub fn read_from(mut input: impl Read) -> Result { - let mut animation = Self::default(); - animation.animate_interval = input.read_u32::()?; - animation.animate_last = input.read_u32::()?; - animation.last_frame = input.read_u16::()?; - animation.frame_changed = input.read_u8()?; - animation.frame_looped = input.read_u8()?; - animation.animate_flag = input.read_u8()?; - animation.last_speed = input.read_f32::()?; - Ok(animation) +impl ReadableHeaderElement for SpriteNodeAnimation { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(SpriteNodeAnimation { + animate_interval: input.read_u32::()?, + animate_last: input.read_u32::()?, + last_frame: input.read_u16::()?, + frame_changed: input.read_u8()?, + frame_looped: input.read_u8()?, + animate_flag: input.read_u8()?, + last_speed: input.read_f32::()?, + }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for SpriteNodeAnimation { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u32::(self.animate_interval)?; output.write_u32::(self.animate_last)?; output.write_u16::(self.last_frame)?; @@ -144,38 +156,42 @@ pub struct SpriteNode { pub count: u8, } -impl SpriteNode { - pub fn read_from(mut input: impl Read) -> Result> { +impl OptionalReadableElement for SpriteNode { + fn read_from(input: &mut RecordingHeaderReader) -> Result> { let ty = input.read_u8()?; if ty == 0 { return Ok(None); } - let mut node = Self::default(); - node.id = input.read_u16::()?.into(); - node.x = input.read_u32::()?; - node.y = input.read_u32::()?; - node.frame = input.read_u16::()?; - node.invisible = input.read_u8()? != 0; - if ty == 2 { - node.animation = Some(SpriteNodeAnimation::read_from(&mut input)?); - } - node.order = input.read_u8()?; - node.flag = input.read_u8()?; - node.count = input.read_u8()?; - Ok(Some(node)) + Ok(Some(SpriteNode { + id: input.read_u16::()?.into(), + x: input.read_u32::()?, + y: input.read_u32::()?, + frame: input.read_u16::()?, + invisible: input.read_u8()? != 0, + animation: if ty == 2 { + Some(SpriteNodeAnimation::read_from(input)?) + } else { + None + }, + order: input.read_u8()?, + flag: input.read_u8()?, + count: input.read_u8()?, + })) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for SpriteNode { + fn write_to(&self, output: &mut W) -> Result<()> { let ty = if self.animation.is_some() { 2 } else { 1 }; output.write_u8(ty)?; output.write_u16::(self.id.into())?; output.write_u32::(self.x)?; output.write_u32::(self.y)?; output.write_u16::(self.frame)?; - output.write_u8(if self.invisible { 1 } else { 0 })?; + output.write_u8(u8::from(self.invisible))?; if let Some(animation) = &self.animation { - animation.write_to(&mut output)?; + animation.write_to(output)?; } output.write_u8(self.order)?; output.write_u8(self.flag)?; @@ -189,18 +205,20 @@ pub struct SpriteList { pub sprites: Vec, } -impl SpriteList { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for SpriteList { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut sprites = vec![]; - while let Some(node) = SpriteNode::read_from(&mut input)? { + while let Some(node) = SpriteNode::read_from(input)? { sprites.push(node); } Ok(Self { sprites }) } +} - pub fn write_to(&self, mut output: impl Write, _version: f32) -> Result<()> { +impl WritableHeaderElement for SpriteList { + fn write_to(&self, output: &mut W) -> Result<()> { for sprite in &self.sprites { - sprite.write_to(&mut output)?; + sprite.write_to(output)?; } output.write_u8(0)?; Ok(()) @@ -234,30 +252,39 @@ pub struct StaticUnitAttributes { pub group_id: Option, pub roo_already_called: u8, pub sprite_list: Option, + pub de_effect_block: Option, } -impl StaticUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.owner_id = input.read_u8()?.into(); - attrs.unit_type_id = input.read_u16::()?.into(); - attrs.sprite_id = input.read_u16::()?.into(); - attrs.garrisoned_in_id = read_opt_u32(&mut input)?; - attrs.hit_points = input.read_f32::()?; - attrs.object_state = input.read_u8()?; - attrs.sleep_flag = input.read_u8()? != 0; - attrs.dopple_flag = input.read_u8()? != 0; - attrs.go_to_sleep_flag = input.read_u8()? != 0; - attrs.id = input.read_u32::()?.into(); - attrs.facet = input.read_u8()?; - attrs.position = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - attrs.screen_offset = (input.read_u16::()?, input.read_u16::()?); - attrs.shadow_offset = (input.read_u16::()?, input.read_u16::()?); - if version < 11.58 { +#[derive(Debug, Default, Clone)] +pub struct DeEffectBlock { + pub has_effect: bool, + pub effect_name: Option, +} + +impl ReadableHeaderElement for StaticUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = StaticUnitAttributes { + owner_id: input.read_u8()?.into(), + unit_type_id: input.read_u16::()?.into(), + sprite_id: input.read_u16::()?.into(), + garrisoned_in_id: read_opt_u32(input)?, + hit_points: input.read_f32::()?, + object_state: input.read_u8()?, + sleep_flag: input.read_u8()? != 0, + dopple_flag: input.read_u8()? != 0, + go_to_sleep_flag: input.read_u8()? != 0, + id: input.read_u32::()?.into(), + facet: input.read_u8()?, + position: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + screen_offset: (input.read_u16::()?, input.read_u16::()?), + shadow_offset: (input.read_u16::()?, input.read_u16::()?), + ..Default::default() + }; + if input.version() < 11.58 { attrs.selected_group = match input.read_i8()? { -1 => None, id => Some(id.try_into().unwrap()), @@ -277,15 +304,62 @@ impl StaticUnitAttributes { } members }; - attrs.group_id = read_opt_u32(&mut input)?; + attrs.group_id = read_opt_u32(input)?; attrs.roo_already_called = input.read_u8()?; + + if input.variant() >= DefinitiveEdition { + input.skip(19)?; + } + if input.read_u8()? != 0 { - attrs.sprite_list = Some(SpriteList::read_from(&mut input)?); + attrs.sprite_list = Some(SpriteList::read_from(input)?); + } + + if input.variant() >= DefinitiveEdition { + input.skip(4)?; + let has_effect = input.read_u8()? == 1; + + let effect_name = if has_effect { + input.skip(1)?; + + let effect_name = input.read_tlv_str()?; + if effect_name.is_some() { + // effect arguments? + input.skip(34)?; + } + + effect_name + } else { + input.skip(1)?; + None + }; + + input.skip(4)?; + + if input.version() >= 13.15 { + input.skip(5)?; + } + + if input.version() >= 13.17 { + input.skip(2)?; + } + + if input.version() >= 13.34 { + input.skip(12)?; + } + + attrs.de_effect_block = Some(DeEffectBlock { + has_effect, + effect_name, + }); } + Ok(attrs) } +} - pub fn write_to(&self, mut output: impl Write, _version: f32) -> Result<()> { +impl WritableHeaderElement for StaticUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u8(self.owner_id.into())?; output.write_u16::(self.unit_type_id.into())?; output.write_u16::(self.sprite_id.into())?; @@ -298,13 +372,15 @@ pub struct AnimatedUnitAttributes { pub speed: f32, } -impl AnimatedUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for AnimatedUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let speed = input.read_f32::()?; Ok(Self { speed }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for AnimatedUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.speed)?; Ok(()) } @@ -327,17 +403,19 @@ pub struct PathData { pub flags: u32, } -impl PathData { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut path = Self::default(); - path.id = input.read_u32::()?; - path.linked_path_type = input.read_u32::()?; - path.waypoint_level = input.read_u32::()?; - path.path_id = input.read_u32::()?; - path.waypoint = input.read_u32::()?; - if version < 10.25 { +impl ReadableHeaderElement for PathData { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut path = PathData { + id: input.read_u32::()?, + linked_path_type: input.read_u32::()?, + waypoint_level: input.read_u32::()?, + path_id: input.read_u32::()?, + waypoint: input.read_u32::()?, + ..Default::default() + }; + if input.version() < 10.25 { path.disable_flags = Some(input.read_u32::()?); - if version >= 10.20 { + if input.version() >= 10.20 { path.enable_flags = Some(input.read_u32::()?); } } @@ -349,10 +427,6 @@ impl PathData { path.flags = input.read_u32::()?; Ok(path) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } #[derive(Debug, Default, Clone, Copy)] @@ -362,7 +436,7 @@ pub struct MovementData { } impl MovementData { - pub fn read_from(mut input: impl Read) -> Result { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let velocity = ( input.read_f32::()?, input.read_f32::()?, @@ -414,37 +488,44 @@ pub struct MovingUnitAttributes { pub consecutive_substitute_count: u32, } -impl MovingUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.trail_remainder = input.read_u32::()?; - attrs.velocity = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - attrs.angle = input.read_f32::()?; - attrs.turn_towards_time = input.read_u32::()?; - attrs.turn_timer = input.read_u32::()?; - attrs.continue_counter = input.read_u32::()?; - attrs.current_terrain_exception = (read_opt_u32(&mut input)?, read_opt_u32(&mut input)?); - attrs.waiting_to_move = input.read_u8()?; - attrs.wait_delays_count = input.read_u8()?; - attrs.on_ground = input.read_u8()?; - attrs.path_data = { - let num_paths = input.read_u32::()?; - let mut paths = vec![]; - for _ in 0..num_paths { - paths.push(PathData::read_from(&mut input, version)?); - } - paths +impl ReadableHeaderElement for MovingUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = MovingUnitAttributes { + trail_remainder: input.read_u32::()?, + velocity: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + angle: input.read_f32::()?, + turn_towards_time: input.read_u32::()?, + turn_timer: input.read_u32::()?, + continue_counter: input.read_u32::()?, + current_terrain_exception: (read_opt_u32(input)?, read_opt_u32(input)?), + waiting_to_move: input.read_u8()?, + wait_delays_count: input.read_u8()?, + on_ground: input.read_u8()?, + path_data: { + let num_paths = input.read_u32::()?; + let mut paths = vec![]; + for _ in 0..num_paths { + paths.push(PathData::read_from(input)?); + } + paths + }, + ..Default::default() }; if input.read_u32::()? != 0 { - attrs.future_path_data = Some(PathData::read_from(&mut input, version)?); + attrs.future_path_data = Some(PathData::read_from(input)?); } if input.read_u32::()? != 0 { - attrs.movement_data = Some(MovementData::read_from(&mut input)?); + attrs.movement_data = Some(MovementData::read_from(input)?); } + + if input.variant() >= DefinitiveEdition && input.version() < 13.2 { + input.skip(2)?; + } + attrs.position = ( input.read_f32::()?, input.read_f32::()?, @@ -487,12 +568,10 @@ impl MovingUnitAttributes { attrs.consecutive_substitute_count = input.read_u32::()?; Ok(attrs) } - - pub fn write_to(&self, _output: impl Write) -> Result<()> { - todo!() - } } +impl WritableHeaderElement for MovingUnitAttributes {} + #[derive(Debug, Default, Clone)] pub struct ActionUnitAttributes { pub waiting: bool, @@ -501,25 +580,26 @@ pub struct ActionUnitAttributes { pub actions: Vec, } -impl ActionUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.waiting = input.read_u8()? != 0; - if version >= 6.5 { +impl ReadableHeaderElement for ActionUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = ActionUnitAttributes { + waiting: input.read_u8()? != 0, + ..Default::default() + }; + + if input.version() >= 6.5 { attrs.command_flag = input.read_u8()?; } - if version >= 11.58 { + if input.version() >= 11.58 { attrs.selected_group_info = input.read_u16::()?; } - attrs.actions = UnitAction::read_list_from(input, version)?; + attrs.actions = UnitAction::read_list_from(input)?; Ok(attrs) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } +impl WritableHeaderElement for ActionUnitAttributes {} + #[derive(Debug, Default, Clone)] pub struct BaseCombatUnitAttributes { pub formation_id: u8, @@ -532,33 +612,31 @@ pub struct BaseCombatUnitAttributes { pub attack_count: u32, } -impl BaseCombatUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for BaseCombatUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut attrs = Self::default(); - if version >= 9.05 { + if input.version() >= 9.05 { attrs.formation_id = input.read_u8()?; attrs.formation_row = input.read_u8()?; attrs.formation_column = input.read_u8()?; } attrs.attack_timer = input.read_f32::()?; - if version >= 2.01 { + if input.version() >= 2.01 { attrs.capture_flag = input.read_u8()?; } - if version >= 9.09 { + if input.version() >= 9.09 { attrs.multi_unified_points = input.read_u8()?; attrs.large_object_radius = input.read_u8()?; } - if version >= 10.02 { + if input.version() >= 10.02 { attrs.attack_count = input.read_u32::()?; } Ok(attrs) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } +impl WritableHeaderElement for BaseCombatUnitAttributes {} + #[derive(Debug, Default, Clone)] pub struct MissileUnitAttributes { pub max_range: f32, @@ -566,26 +644,25 @@ pub struct MissileUnitAttributes { pub own_base: Option, } -impl MissileUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.max_range = input.read_f32::()?; - attrs.fired_from_id = input.read_u32::()?.into(); - attrs.own_base = { - if input.read_u8()? == 0 { - None - } else { - Some(UnitType::read_from(&mut input, version)?) - } - }; - Ok(attrs) - } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() +impl ReadableHeaderElement for MissileUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(MissileUnitAttributes { + max_range: input.read_f32::()?, + fired_from_id: input.read_u32::()?.into(), + own_base: { + if input.read_u8()? == 0 { + None + } else { + let version = input.version(); + Some(UnitType::read_from(&mut *input, version)?) + } + }, + }) } } +impl WritableHeaderElement for MissileUnitAttributes {} + #[derive(Debug, Default, Clone)] pub struct UnitAIOrder { issuer: u32, @@ -597,25 +674,21 @@ pub struct UnitAIOrder { range: f32, } -impl UnitAIOrder { - pub fn read_from(mut input: impl Read) -> Result { - let mut order = Self::default(); - order.issuer = input.read_u32::()?; - order.order_type = input.read_u32::()?; - order.priority = input.read_u32::()?; - order.target_id = input.read_u32::()?.into(); - order.target_player = input.read_u32::()?.try_into().unwrap(); - order.target_location = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - order.range = input.read_f32::()?; - Ok(order) - } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() +impl ReadableHeaderElement for UnitAIOrder { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(UnitAIOrder { + issuer: input.read_u32::()?, + order_type: input.read_u32::()?, + priority: input.read_u32::()?, + target_id: input.read_u32::()?.into(), + target_player: input.read_u32::()?.try_into().unwrap(), + target_location: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + range: input.read_f32::()?, + }) } } @@ -627,22 +700,18 @@ pub struct UnitAINotification { pub params: (u32, u32, u32), } -impl UnitAINotification { - pub fn read_from(mut input: impl Read) -> Result { - let mut notify = Self::default(); - notify.caller = input.read_u32::()?; - notify.recipient = input.read_u32::()?; - notify.notification_type = input.read_u32::()?; - notify.params = ( - input.read_u32::()?, - input.read_u32::()?, - input.read_u32::()?, - ); - Ok(notify) - } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() +impl ReadableHeaderElement for UnitAINotification { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(UnitAINotification { + caller: input.read_u32::()?, + recipient: input.read_u32::()?, + notification_type: input.read_u32::()?, + params: ( + input.read_u32::()?, + input.read_u32::()?, + input.read_u32::()?, + ), + }) } } @@ -657,31 +726,29 @@ pub struct UnitAIOrderHistory { target_position: (f32, f32, f32), } -impl UnitAIOrderHistory { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut order = Self::default(); - order.order = input.read_u32::()?; - order.action = input.read_u32::()?; - order.time = input.read_u32::()?; - order.position = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - order.target_id = input.read_u32::()?.into(); - if version >= 10.50 { - order.target_attack_category = read_opt_u32(&mut input)?; - } - order.target_position = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - Ok(order) - } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() +impl ReadableHeaderElement for UnitAIOrderHistory { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(UnitAIOrderHistory { + order: input.read_u32::()?, + action: input.read_u32::()?, + time: input.read_u32::()?, + position: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + target_id: input.read_u32::()?.into(), + target_attack_category: if input.version() >= 10.50 { + read_opt_u32(input)? + } else { + Default::default() + }, + target_position: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + }) } } @@ -691,8 +758,8 @@ pub struct UnitAIRetargetEntry { pub retarget_timeout: u32, } -impl UnitAIRetargetEntry { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for UnitAIRetargetEntry { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let target_id = input.read_u32::()?.into(); let retarget_timeout = input.read_u32::()?; Ok(Self { @@ -700,10 +767,6 @@ impl UnitAIRetargetEntry { retarget_timeout, }) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } #[derive(Debug, Default, Clone)] @@ -712,36 +775,29 @@ pub struct Waypoint { pub facet_to_next_waypoint: u8, } -impl Waypoint { - pub fn read_from(mut input: impl Read) -> Result { - let mut waypoint = Self::default(); - waypoint.location = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - waypoint.facet_to_next_waypoint = input.read_u8()?; +impl ReadableHeaderElement for Waypoint { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let waypoint = Waypoint { + location: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + facet_to_next_waypoint: input.read_u8()?, + }; let _padding = input.read_u8()?; let _padding = input.read_u8()?; let _padding = input.read_u8()?; Ok(waypoint) } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() - } } #[derive(Debug, Default, Clone, Copy)] pub struct PatrolPath {} -impl PatrolPath { - pub fn read_from(_input: impl Read) -> Result { - todo!() - } - - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() +impl ReadableHeaderElement for PatrolPath { + fn read_from(_: &mut RecordingHeaderReader) -> Result { + unimplemented!() } } @@ -788,17 +844,19 @@ pub struct UnitAI { formation_type: u8, } -impl UnitAI { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut ai = Self::default(); - ai.mood = read_opt_u32(&mut input)?; - ai.current_order = read_opt_u32(&mut input)?; - ai.current_order_priority = read_opt_u32(&mut input)?; - ai.current_action = read_opt_u32(&mut input)?; - ai.current_target = read_opt_u32(&mut input)?; - ai.current_target_type = match input.read_u16::()? { - 0xFFFF => None, - id => Some(id.try_into().unwrap()), +impl ReadableHeaderElement for UnitAI { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut ai = UnitAI { + mood: read_opt_u32(input)?, + current_order: read_opt_u32(input)?, + current_order_priority: read_opt_u32(input)?, + current_action: read_opt_u32(input)?, + current_target: read_opt_u32(input)?, + current_target_type: match input.read_u16::()? { + 0xFFFF => None, + id => Some(id.try_into().unwrap()), + }, + ..Default::default() }; input.skip(2)?; ai.current_target_location = ( @@ -807,25 +865,30 @@ impl UnitAI { input.read_f32::()?, ); ai.desired_target_distance = input.read_f32::()?; - ai.last_action = read_opt_u32(&mut input)?; - ai.last_order = read_opt_u32(&mut input)?; - ai.last_target = read_opt_u32(&mut input)?; - ai.last_target_type = read_opt_u32(&mut input)?; - ai.last_update_type = read_opt_u32(&mut input)?; + ai.last_action = read_opt_u32(input)?; + ai.last_order = read_opt_u32(input)?; + ai.last_target = read_opt_u32(input)?; + ai.last_target_type = read_opt_u32(input)?; + ai.last_update_type = read_opt_u32(input)?; ai.idle_timer = input.read_u32::()?; ai.idle_timeout = input.read_u32::()?; ai.adjusted_idle_timeout = input.read_u32::()?; ai.secondary_timer = input.read_u32::()?; ai.lookaround_timer = input.read_u32::()?; ai.lookaround_timeout = input.read_u32::()?; - ai.defend_target = read_opt_u32(&mut input)?; + ai.defend_target = read_opt_u32(input)?; ai.defense_buffer = input.read_f32::()?; - ai.last_world_position = Waypoint::read_from(&mut input)?; + ai.last_world_position = Waypoint::read_from(input)?; + + if input.version() >= 20.06 { + input.skip(8)?; + } + ai.orders = { let num_orders = input.read_u32::()?; let mut orders = vec![]; for _ in 0..num_orders { - orders.push(UnitAIOrder::read_from(&mut input)?); + orders.push(UnitAIOrder::read_from(input)?); } orders }; @@ -833,7 +896,7 @@ impl UnitAI { let num_notifications = input.read_u32::()?; let mut notifications = vec![]; for _ in 0..num_notifications { - notifications.push(UnitAINotification::read_from(&mut input)?); + notifications.push(UnitAINotification::read_from(input)?); } notifications }; @@ -850,53 +913,54 @@ impl UnitAI { ai.state_position = (input.read_f32::()?, input.read_f32::()?); ai.time_since_enemy_sighting = input.read_u32::()?; ai.alert_mode = input.read_u8()?; - ai.alert_mode_object_id = read_opt_u32(&mut input)?; + ai.alert_mode_object_id = read_opt_u32(input)?; ai.patrol_path = { let has_path = input.read_u32::()? != 0; if has_path { - Some(PatrolPath::read_from(&mut input)?) + Some(PatrolPath::read_from(input)?) } else { None } }; ai.patrol_current_waypoint = input.read_u32::()?; - if version >= 10.48 { + if input.version() >= 10.48 { ai.order_history = { let num_orders = input.read_u32::()?; let mut orders = vec![]; for _ in 0..num_orders { - orders.push(UnitAIOrderHistory::read_from(&mut input, version)?); + orders.push(UnitAIOrderHistory::read_from(input)?); } orders }; } - if version >= 10.50 { + if input.version() >= 10.50 { ai.last_retarget_time = input.read_u32::()?; } - if version >= 11.04 { + if input.version() >= 11.04 { ai.randomized_retarget_timer = input.read_u32::()?; } - if version >= 11.05 { + if input.version() >= 11.05 { ai.retarget_entries = { let num_entries = input.read_u32::()?; let mut entries = vec![]; for _ in 0..num_entries { - entries.push(UnitAIRetargetEntry::read_from(&mut input)?); + entries.push(UnitAIRetargetEntry::read_from(input)?); } entries }; } - if version >= 11.14 { - ai.best_unit_to_attack = read_opt_u32(&mut input)?; + if input.version() >= 11.14 { + ai.best_unit_to_attack = read_opt_u32(input)?; } - if version >= 11.44 { + if input.version() >= 11.44 { ai.formation_type = input.read_u8()?; } - Ok(ai) - } - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() + if input.variant() >= DefinitiveEdition { + input.skip(4)?; + } + + Ok(ai) } } @@ -922,44 +986,57 @@ pub struct CombatUnitAttributes { pub num_healers: u8, } -impl CombatUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for CombatUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut attrs = Self::default(); + + if input.variant() >= DefinitiveEdition { + input.skip(18)?; + } + attrs.next_volley = input.read_u8()?; attrs.using_special_attack_animation = input.read_u8()?; attrs.own_base = { if input.read_u8()? == 0 { None } else { - Some(UnitType::read_from(&mut input, version)?) + let version = input.version(); + Some(UnitType::read_from(&mut *input, version)?) } }; for amount in attrs.attribute_amounts.iter_mut() { *amount = input.read_u16::()?; } - if version >= 9.16 { + if input.version() >= 9.16 { attrs.decay_timer = input.read_u16::()?; } - if version >= 9.61 { + if input.version() >= 9.61 { attrs.raider_build_countdown = input.read_u32::()?; } - if version >= 9.65 { + if input.version() >= 9.65 { attrs.locked_down_count = input.read_u32::()?; } - if version >= 11.56 { + if input.version() >= 11.56 { attrs.inside_garrison_count = input.read_u8()?; } attrs.unit_ai = { let has_ai = input.read_u32::()? != 0; if has_ai { - Some(UnitAI::read_from(&mut input, version)?) + Some(UnitAI::read_from(input)?) } else { None } }; - if version >= 10.30 { + + // https://github.com/happyleavesaoc/aoc-mgz/blob/ce4e5dc6184fcd005d0c50d3abac58dd863778be/mgz/header/objects.py#L361 + // ??? + if input.peek(5)? != b"\x00\xff\xff\xff\xff" { + input.skip(13)?; + } + + if input.version() >= 10.30 { attrs.town_bell_flag = input.read_i8()?; - attrs.town_bell_target_id = read_opt_u32(&mut input)?; + attrs.town_bell_target_id = read_opt_u32(input)?; attrs.town_bell_target_location = { let location = (input.read_f32::()?, input.read_f32::()?); if location.0 >= 0.0 { @@ -969,30 +1046,33 @@ impl CombatUnitAttributes { } }; } - if version >= 11.71 { - attrs.town_bell_target_id_2 = read_opt_u32(&mut input)?; + if input.version() >= 11.71 { + attrs.town_bell_target_id_2 = read_opt_u32(input)?; attrs.town_bell_target_type = input.read_u32::()?; } - if version >= 11.74 { + if input.version() >= 11.74 { attrs.town_bell_action = input.read_u32::()?; } - if version >= 10.42 { + if input.version() >= 10.42 { attrs.berserker_timer = input.read_f32::()?; } - if version >= 10.46 { + if input.version() >= 10.46 { attrs.num_builders = input.read_u8()?; } - if version >= 11.69 { + if input.version() >= 11.69 { attrs.num_healers = input.read_u8()?; } - Ok(attrs) - } - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() + if input.version() >= 20.06 { + input.skip(4)?; + } + + Ok(attrs) } } +impl WritableHeaderElement for CombatUnitAttributes {} + #[derive(Debug, Clone)] pub enum GatherPoint { Location { x: f32, y: f32, z: f32 }, @@ -1005,8 +1085,8 @@ pub struct ProductionQueueEntry { pub count: u16, } -impl ProductionQueueEntry { - fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ProductionQueueEntry { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let unit_type_id = input.read_u16::()?.into(); let count = input.read_u16::()?; Ok(Self { @@ -1066,44 +1146,53 @@ pub struct BuildingUnitAttributes { pub snow_flag: bool, } -impl BuildingUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.built = input.read_u8()? != 0; - attrs.build_points = input.read_f32::()?; - attrs.unique_build_id = read_opt_u32(&mut input)?; - attrs.culture = input.read_u8()?; - attrs.burning = input.read_u8()?; - attrs.last_burn_time = input.read_u32::()?; - attrs.last_garrison_time = input.read_u32::()?; - attrs.relic_count = input.read_u32::()?; - attrs.specific_relic_count = input.read_u32::()?; - attrs.gather_point = { - let exists = input.read_u32::()? != 0; - let location = GatherPoint::Location { - x: input.read_f32::()?, - y: input.read_f32::()?, - z: input.read_f32::()?, - }; - let object_id = input.read_i32::()?; - let unit_type_id = input.read_i16::()?; - match (exists, object_id, unit_type_id) { - (false, _, _) => None, - (true, -1, -1) => Some(location), - (true, id, unit_type_id) => Some(GatherPoint::Object { - id: id.try_into().unwrap(), - unit_type: unit_type_id.try_into().unwrap(), - }), - } +impl ReadableHeaderElement for BuildingUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = BuildingUnitAttributes { + built: input.read_u8()? != 0, + build_points: input.read_f32::()?, + unique_build_id: read_opt_u32(input)?, + culture: input.read_u8()?, + burning: input.read_u8()?, + last_burn_time: input.read_u32::()?, + last_garrison_time: input.read_u32::()?, + relic_count: input.read_u32::()?, + specific_relic_count: input.read_u32::()?, + gather_point: { + let exists = input.read_u32::()? != 0; + let location = GatherPoint::Location { + x: input.read_f32::()?, + y: input.read_f32::()?, + z: input.read_f32::()?, + }; + let object_id = input.read_i32::()?; + let unit_type_id = input.read_i16::()?; + match (exists, object_id, unit_type_id) { + (false, _, _) => None, + (true, -1, -1) => Some(location), + (true, id, unit_type_id) => Some(GatherPoint::Object { + id: id.try_into().unwrap(), + unit_type: unit_type_id.try_into().unwrap(), + }), + } + }, + desolid_flag: input.read_u8()? != 0, + ..Default::default() }; - attrs.desolid_flag = input.read_u8()? != 0; - if version >= 10.54 { + if input.version() >= 10.54 { attrs.pending_order = input.read_u32::()?; } - attrs.linked_owner = read_opt_u32(&mut input)?; + attrs.linked_owner = read_opt_u32(input)?; attrs.linked_children = { let mut children: ArrayVec = Default::default(); - for _ in 0..4 { + + let num_children = if input.variant() >= DefinitiveEdition { + 3 + } else { + 4 + }; + + for _ in 0..num_children { let id = input.read_i32::()?; if id != -1 { children.push(id.try_into().unwrap()); @@ -1112,21 +1201,25 @@ impl BuildingUnitAttributes { children }; attrs.captured_unit_count = input.read_u8()?; - attrs.extra_actions = UnitAction::read_list_from(&mut input, version)?; - attrs.research_actions = UnitAction::read_list_from(&mut input, version)?; + attrs.extra_actions = UnitAction::read_list_from(input)?; + + if input.variant() != DefinitiveEdition { + attrs.research_actions = UnitAction::read_list_from(input)?; + } + attrs.production_queue = { let capacity = input.read_u16::()?; let mut queue = vec![ProductionQueueEntry::default(); capacity as usize]; for entry in queue.iter_mut() { - *entry = ProductionQueueEntry::read_from(&mut input)?; + *entry = ProductionQueueEntry::read_from(input)?; } let _size = input.read_u16::()?; queue }; attrs.production_queue_total_units = input.read_u16::()?; attrs.production_queue_enabled = input.read_u8()? != 0; - attrs.production_queue_actions = UnitAction::read_list_from(&mut input, version)?; - if version >= 10.65 { + attrs.production_queue_actions = UnitAction::read_list_from(input)?; + if input.version() >= 10.65 { // game reads into the same value twice, while there are two separate fields of this // type. likely a bug, but it doesn't appear to cause issues? is this unused? attrs.endpoint = ( @@ -1143,19 +1236,20 @@ impl BuildingUnitAttributes { attrs.first_update = input.read_u32::()?; attrs.close_timer = input.read_u32::()?; } - if version >= 10.67 { + if input.version() >= 10.67 { attrs.terrain_type = Some(input.read_u8()?.into()); } - if version >= 11.43 { + if input.version() >= 11.43 { attrs.semi_asleep = input.read_u8()? != 0; } - if version >= 11.54 { + if input.version() >= 11.54 { attrs.snow_flag = input.read_u8()? != 0; } - Ok(attrs) - } - pub fn write_to(&self, _output: impl Write, _version: f32) -> Result<()> { - todo!() + if input.variant() >= DefinitiveEdition { + input.skip(1)?; + } + + Ok(attrs) } } diff --git a/crates/genie-rec/src/unit_action.rs b/crates/genie-rec/src/unit_action.rs index 8db0ee7..9e06607 100644 --- a/crates/genie-rec/src/unit_action.rs +++ b/crates/genie-rec/src/unit_action.rs @@ -1,3 +1,5 @@ +use crate::element::{ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::RecordingHeaderReader; use crate::ObjectID; use crate::Result; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; @@ -21,27 +23,30 @@ pub struct UnitAction { pub params: ActionType, } -impl UnitAction { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for UnitAction { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let action_type = input.read_u16::()?; - Self::read_from_inner(&mut input, action_type, version) + Self::read_from_inner(input, action_type) } +} - // `dyn` because this is a recursive function; taking &mut from a `impl Read` here - // would cause infinite recursion in the types. - fn read_from_inner(mut input: &mut dyn Read, action_type: u16, version: f32) -> Result { +impl UnitAction { + fn read_from_inner( + input: &mut RecordingHeaderReader, + action_type: u16, + ) -> Result { // TODO this is different between AoC 1.0 and AoC 1.0c. This version check is a guess // and may not actually be when it changed. May have to become more specific in the // future! - let state = if version <= 11.76 { + let state = if input.version() <= 11.76 { input.read_u8()? as u32 } else { input.read_u32::()? }; let _target_object_pointer = input.read_u32::()?; let _target_object_pointer_2 = input.read_u32::()?; - let target_object_id = read_opt_u32(&mut input)?; - let target_object_id_2 = read_opt_u32(&mut input)?; + let target_object_id = read_opt_u32(input)?; + let target_object_id_2 = read_opt_u32(input)?; let target_position = ( input.read_f32::()?, input.read_f32::()?, @@ -49,13 +54,13 @@ impl UnitAction { ); let timer = input.read_f32::()?; let target_moved_state = input.read_u8()?; - let task_id = read_opt_u16(&mut input)?; + let task_id = read_opt_u16(input)?; let sub_action_value = input.read_u8()?; - let sub_actions = UnitAction::read_list_from(&mut input, version)?; - let sprite_id = read_opt_u16(&mut input)?; - let params = ActionType::read_from(&mut input, action_type)?; + let sub_actions = UnitAction::read_list_from(input)?; + let sprite_id = read_opt_u16(input)?; + let params = ActionType::read_from(input, action_type)?; - Ok(Self { + Ok(UnitAction { state, target_object_id, target_object_id_2, @@ -65,19 +70,19 @@ impl UnitAction { task_id, sub_action_value, sub_actions, - params, sprite_id, + params, }) } - pub fn read_list_from(mut input: impl Read, version: f32) -> Result> { + pub fn read_list_from(input: &mut RecordingHeaderReader) -> Result> { let mut list = vec![]; loop { let action_type = input.read_u16::()?; if action_type == 0 { return Ok(list); } - let action = Self::read_from_inner(&mut input, action_type, version)?; + let action = Self::read_from_inner(input, action_type)?; list.push(action); } } @@ -94,10 +99,16 @@ pub enum ActionType { Guard, Make(ActionMake), Artifact, + Unknown(u16), } impl ActionType { - pub fn read_from(input: impl Read, action_type: u16) -> Result { + pub fn read_from( + input: &mut RecordingHeaderReader, + action_type: u16, + ) -> Result { + println!("{} {}", input.position(), action_type); + let data = match action_type { 1 => Self::MoveTo(ActionMoveTo::read_from(input)?), 3 => Self::Enter(ActionEnter::read_from(input)?), @@ -119,13 +130,15 @@ pub struct ActionMoveTo { pub range: f32, } -impl ActionMoveTo { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ActionMoveTo { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let range = input.read_f32::()?; Ok(Self { range }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for ActionMoveTo { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.range)?; Ok(()) } @@ -136,13 +149,15 @@ pub struct ActionEnter { pub first_time: u32, } -impl ActionEnter { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ActionEnter { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let first_time = input.read_u32::()?; Ok(Self { first_time }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for ActionEnter { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u32::(self.first_time)?; Ok(()) } @@ -163,25 +178,25 @@ pub struct ActionAttack { last_target_position: (f32, f32, f32), } -impl ActionAttack { - pub fn read_from(mut input: impl Read) -> Result { - let mut props = Self::default(); - props.range = input.read_f32::()?; - props.min_range = input.read_f32::()?; - props.missile_id = input.read_u16::()?.into(); - props.frame_delay = input.read_u16::()?; - props.need_to_attack = input.read_u16::()?; - props.was_same_owner = input.read_u16::()?; - props.indirect_fire_flag = input.read_u8()?; - props.move_sprite_id = read_opt_u16(&mut input)?; - props.fight_sprite_id = read_opt_u16(&mut input)?; - props.wait_sprite_id = read_opt_u16(&mut input)?; - props.last_target_position = ( - input.read_f32::()?, - input.read_f32::()?, - input.read_f32::()?, - ); - Ok(props) +impl ReadableHeaderElement for ActionAttack { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + Ok(ActionAttack { + range: input.read_f32::()?, + min_range: input.read_f32::()?, + missile_id: input.read_u16::()?.into(), + frame_delay: input.read_u16::()?, + need_to_attack: input.read_u16::()?, + was_same_owner: input.read_u16::()?, + indirect_fire_flag: input.read_u8()?, + move_sprite_id: read_opt_u16(input)?, + fight_sprite_id: read_opt_u16(input)?, + wait_sprite_id: read_opt_u16(input)?, + last_target_position: ( + input.read_f32::()?, + input.read_f32::()?, + input.read_f32::()?, + ), + }) } } @@ -190,13 +205,15 @@ pub struct ActionMake { pub work_timer: f32, } -impl ActionMake { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ActionMake { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let work_timer = input.read_f32::()?; Ok(Self { work_timer }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for ActionMake { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.work_timer)?; Ok(()) } diff --git a/crates/genie-rec/src/unit_type.rs b/crates/genie-rec/src/unit_type.rs index 9ea9161..e67d563 100644 --- a/crates/genie-rec/src/unit_type.rs +++ b/crates/genie-rec/src/unit_type.rs @@ -1,7 +1,11 @@ +use crate::element::{ReadableHeaderElement, WritableHeaderElement}; +use crate::reader::RecordingHeaderReader; +use crate::GameVariant::DefinitiveEdition; use crate::Result; use arrayvec::ArrayVec; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; pub use genie_dat::unit_type::{AttributeCost, ParseUnitBaseClassError, UnitBaseClass}; +use genie_support::{read_opt_u16, ReadSkipExt}; pub use genie_support::{StringKey, UnitTypeID}; use std::convert::TryInto; use std::io::{Read, Write}; @@ -19,10 +23,11 @@ pub struct CompactUnitType { pub building: Option, } -impl CompactUnitType { - pub fn read_from(mut input: impl Read, version: f32) -> Result { +impl ReadableHeaderElement for CompactUnitType { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let unit_base_class = input.read_u8()?.try_into().unwrap(); - let static_ = StaticUnitAttributes::read_from(&mut input)?; + let static_ = StaticUnitAttributes::read_from(input)?; + let mut unit = Self { unit_base_class, static_, @@ -34,27 +39,29 @@ impl CompactUnitType { combat: None, building: None, }; + if unit_base_class >= UnitBaseClass::Animated { - unit.animated = Some(AnimatedUnitAttributes::read_from(&mut input)?); + unit.animated = Some(AnimatedUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Moving { - unit.moving = Some(MovingUnitAttributes::read_from(&mut input)?); + unit.moving = Some(MovingUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Action { - unit.action = Some(ActionUnitAttributes::read_from(&mut input)?); + unit.action = Some(ActionUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::BaseCombat { - unit.base_combat = Some(BaseCombatUnitAttributes::read_from(&mut input, version)?); + unit.base_combat = Some(BaseCombatUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Missile { - unit.missile = Some(MissileUnitAttributes::read_from(&mut input)?); + unit.missile = Some(MissileUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Combat { - unit.combat = Some(CombatUnitAttributes::read_from(&mut input)?); + unit.combat = Some(CombatUnitAttributes::read_from(input)?); } if unit_base_class >= UnitBaseClass::Building { - unit.building = Some(BuildingUnitAttributes::read_from(&mut input)?); + unit.building = Some(BuildingUnitAttributes::read_from(input)?); } + Ok(unit) } } @@ -82,14 +89,31 @@ pub struct StaticUnitAttributes { attribute_max_amount: u16, attribute_amount_held: f32, disabled: bool, + de: Option, +} +#[derive(Debug, Default, Clone)] +pub struct StaticUnitAttributesDeExtension { + name_id: StringKey, + creation_id: StringKey, + terrain_table: i16, + dead_unit: Option, + icon: Option, + blast_defense: u8, } -impl StaticUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { - let mut attrs = Self::default(); - attrs.id = input.read_u16::()?.into(); - attrs.copy_id = input.read_u16::()?.into(); - attrs.base_id = input.read_u16::()?.into(); +impl ReadableHeaderElement for StaticUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = StaticUnitAttributes { + id: input.read_u16::()?.into(), + copy_id: input.read_u16::()?.into(), + base_id: input.read_u16::()?.into(), + ..Default::default() + }; + + if input.variant() >= DefinitiveEdition { + // repeat of id?? + input.skip(2)?; + } attrs.unit_class = input.read_u16::()?; attrs.hotkey_id = input.read_u32::()?; attrs.available = input.read_u8()? != 0; @@ -119,6 +143,18 @@ impl StaticUnitAttributes { attrs.attribute_max_amount = input.read_u16::()?; attrs.attribute_amount_held = input.read_f32::()?; attrs.disabled = input.read_u8()? != 0; + + if input.variant() >= DefinitiveEdition { + attrs.de = Some(StaticUnitAttributesDeExtension { + name_id: input.read_u32::()?.into(), + creation_id: input.read_u32::()?.into(), + terrain_table: input.read_i16::()?, + dead_unit: read_opt_u16(input)?, + icon: read_opt_u16(input)?, + blast_defense: input.read_u8()?, + }); + } + Ok(attrs) } } @@ -128,13 +164,15 @@ pub struct AnimatedUnitAttributes { pub speed: f32, } -impl AnimatedUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for AnimatedUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let speed = input.read_f32::()?; Ok(Self { speed }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for AnimatedUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.speed)?; Ok(()) } @@ -145,13 +183,15 @@ pub struct MovingUnitAttributes { pub turn_speed: f32, } -impl MovingUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for MovingUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let turn_speed = input.read_f32::()?; Ok(Self { turn_speed }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for MovingUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.turn_speed)?; Ok(()) } @@ -163,8 +203,8 @@ pub struct ActionUnitAttributes { pub work_rate: f32, } -impl ActionUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for ActionUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let search_radius = input.read_f32::()?; let work_rate = input.read_f32::()?; Ok(Self { @@ -172,8 +212,10 @@ impl ActionUnitAttributes { work_rate, }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for ActionUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_f32::(self.search_radius)?; output.write_f32::(self.work_rate)?; Ok(()) @@ -186,8 +228,8 @@ pub struct HitType { pub amount: i16, } -impl HitType { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for HitType { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let hit_type = input.read_u16::()?; let amount = input.read_i16::()?; Ok(Self { hit_type, amount }) @@ -207,23 +249,35 @@ pub struct BaseCombatUnitAttributes { pub weapon_range_max_2: f32, pub area_of_effect: f32, pub weapon_range_min: f32, + pub de: Option, } -impl BaseCombatUnitAttributes { - pub fn read_from(mut input: impl Read, version: f32) -> Result { - let mut attrs = Self::default(); - attrs.base_armor = if version >= 11.52 { - input.read_u16::()? - } else { - input.read_u8()?.into() +#[derive(Debug, Default, Clone)] +pub struct BaseCombatUnitAttributesDeExtension { + pub frame_delay: i16, + pub blast_attack_level: u8, + pub shown_melee_armor: i16, + pub shown_attack: i16, + pub shown_range: f32, +} + +impl ReadableHeaderElement for BaseCombatUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { + let mut attrs = BaseCombatUnitAttributes { + base_armor: if input.version() >= 11.52 { + input.read_u16::()? + } else { + input.read_u8()?.into() + }, + ..Default::default() }; let num_attacks = input.read_u16::()?; for _ in 0..num_attacks { - attrs.attacks.push(HitType::read_from(&mut input)?); + attrs.attacks.push(HitType::read_from(input)?); } let num_armors = input.read_u16::()?; for _ in 0..num_armors { - attrs.armors.push(HitType::read_from(&mut input)?); + attrs.armors.push(HitType::read_from(input)?); } attrs.attack_speed = input.read_f32::()?; attrs.weapon_range_max = input.read_f32::()?; @@ -239,11 +293,18 @@ impl BaseCombatUnitAttributes { attrs.weapon_range_max_2 = input.read_f32::()?; attrs.area_of_effect = input.read_f32::()?; attrs.weapon_range_min = input.read_f32::()?; - Ok(attrs) - } - pub fn write_to(&self, _output: impl Write) -> Result<()> { - todo!() + if input.variant() >= DefinitiveEdition { + attrs.de = Some(BaseCombatUnitAttributesDeExtension { + frame_delay: input.read_i16::()?, + blast_attack_level: input.read_u8()?, + shown_melee_armor: input.read_i16::()?, + shown_attack: input.read_i16::()?, + shown_range: input.read_f32::()?, + }); + } + + Ok(attrs) } } @@ -252,13 +313,15 @@ pub struct MissileUnitAttributes { pub targeting_type: u8, } -impl MissileUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for MissileUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let targeting_type = input.read_u8()?; Ok(Self { targeting_type }) } +} - pub fn write_to(&self, mut output: impl Write) -> Result<()> { +impl WritableHeaderElement for MissileUnitAttributes { + fn write_to(&self, output: &mut W) -> Result<()> { output.write_u8(self.targeting_type)?; Ok(()) } @@ -280,17 +343,28 @@ pub struct CombatUnitAttributes { pub hero_flag: Option, pub volley_fire_amount: f32, pub max_attacks_in_volley: u8, + pub de: Option, } -impl CombatUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +#[derive(Debug, Default, Clone)] +pub struct CombatUnitAttributesDeExtension { + pub hero_flag: bool, + pub shown_pierce_armor: i16, + pub train_location: Option, + pub train_button: u8, + pub health_regen: f32, +} + +impl ReadableHeaderElement for CombatUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut attrs = Self::default(); for _ in 0..3 { - let attr = AttributeCost::read_from(&mut input)?; + let attr = AttributeCost::read_from(&mut *input)?; if attr.attribute_type >= 0 { attrs.costs.push(attr); } } + let create_time = input.read_u16::()?; // UserPatch data if create_time == u16::max_value() - 15 { @@ -313,11 +387,20 @@ impl CombatUnitAttributes { }; attrs.volley_fire_amount = input.read_f32::()?; attrs.max_attacks_in_volley = input.read_u8()?; - Ok(attrs) - } - pub fn write_to(&self, _output: impl Write) -> Result<()> { - todo!() + if input.variant() >= DefinitiveEdition { + let mut de = CombatUnitAttributesDeExtension::default(); + let hero_flag = input.read_u8()?; + attrs.hero_flag = Some(hero_flag); + de.hero_flag = hero_flag == 1; + de.train_location = read_opt_u16(input)?; + de.train_button = input.read_u8()?; + de.shown_pierce_armor = input.read_i16::()?; + de.health_regen = input.read_f32::()?; + attrs.de = Some(de); + } + + Ok(attrs) } } @@ -331,8 +414,8 @@ pub struct BuildingUnitAttributes { pub garrison_heal_rate: Option, } -impl BuildingUnitAttributes { - pub fn read_from(mut input: impl Read) -> Result { +impl ReadableHeaderElement for BuildingUnitAttributes { + fn read_from(input: &mut RecordingHeaderReader) -> Result { let mut attrs = Self::default(); let facet = input.read_i16::()?; // UserPatch data @@ -344,11 +427,14 @@ impl BuildingUnitAttributes { } else { attrs.facet = facet; } - Ok(attrs) - } - pub fn write_to(&self, _output: impl Write) -> Result<()> { - todo!() + if input.variant() >= DefinitiveEdition { + attrs.garrison_heal_rate = Some(input.read_f32::()?); + // every item I've found so far is 0 + input.skip(4)?; + } + + Ok(attrs) } } @@ -356,9 +442,10 @@ impl BuildingUnitAttributes { mod tests { use super::*; + #[allow(clippy::neg_cmp_op_on_partial_ord)] #[test] fn cmp_unit_base_class() { - assert!(UnitBaseClass::Static == UnitBaseClass::Static); + assert_eq!(UnitBaseClass::Static, UnitBaseClass::Static); assert!(UnitBaseClass::Static < UnitBaseClass::Animated); assert!(UnitBaseClass::Static < UnitBaseClass::Doppelganger); assert!(UnitBaseClass::Animated < UnitBaseClass::Doppelganger); diff --git a/crates/genie-rec/src/version.rs b/crates/genie-rec/src/version.rs new file mode 100644 index 0000000..247675a --- /dev/null +++ b/crates/genie-rec/src/version.rs @@ -0,0 +1,305 @@ +use crate::game_options::Age; +use crate::game_options::GameMode; +use crate::game_options::GameSpeed; +use std::cmp::Ordering; +use std::fmt; +use std::fmt::{Debug, Display}; +use std::io::Read; + +use crate::game_options::Difficulty; +use crate::game_options::MapSize; +use crate::game_options::MapType; +use crate::game_options::ResourceLevel; +use crate::game_options::Visibility; +use crate::DLCOptions; +/// the variant of AoE2 game +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum GameVariant { + /// A trial version, either AoC or AoE + Trial, + /// Age of Kings + AgeOfKings, + /// Age of Conquerors + AgeOfConquerors, + /// User Patch + UserPatch, + /// Forgotten Empires mod + ForgottenEmpires, + /// AoE2:HD release + HighDefinition, + /// AoE2:DE release + DefinitiveEdition, +} + +pub const TRIAL_VERSION: GameVersion = GameVersion(*b"TRL 9.3\0"); +pub const AGE_OF_KINGS_VERSION: GameVersion = GameVersion(*b"VER 9.3\0"); +pub const AGE_OF_CONQUERORS_VERSION: GameVersion = GameVersion(*b"VER 9.4\0"); +pub const FORGOTTEN_EMPIRES_VERSION: GameVersion = GameVersion(*b"VER 9.5\0"); + +// So last known AoC version is 11.76 +// Since all versions only have a precision of 2 +// We can save do + 0.01 +pub const HD_SAVE_VERSION: f32 = 11.77; + +pub const DE_SAVE_VERSION: f32 = 12.97; + +use GameVariant::*; + +impl GameVariant { + pub fn resolve_variant(version: &GameVersion, sub_version: f32) -> Option { + // taken from https://github.com/goto-bus-stop/recanalyst/blob/master/src/Analyzers/VersionAnalyzer.php + + Some(match *version { + // Either AOC or AOK trial, just return Trial :shrug: + TRIAL_VERSION => Trial, + AGE_OF_KINGS_VERSION => AgeOfKings, + AGE_OF_CONQUERORS_VERSION if sub_version >= DE_SAVE_VERSION => DefinitiveEdition, + AGE_OF_CONQUERORS_VERSION if sub_version >= HD_SAVE_VERSION => HighDefinition, + AGE_OF_CONQUERORS_VERSION => AgeOfConquerors, + FORGOTTEN_EMPIRES_VERSION => ForgottenEmpires, + // UserPatch uses VER 9.\0 where N is anything between 8 and F + GameVersion([b'V', b'E', b'R', b' ', b'9', b'.', b'8'..=b'F', b'\0']) => UserPatch, + _ => return None, + }) + } + + pub fn is_original(&self) -> bool { + matches!(self, Trial | AgeOfKings | AgeOfConquerors) + } + + pub fn is_mod(&self) -> bool { + matches!(self, ForgottenEmpires | UserPatch) + } + + pub fn is_update(&self) -> bool { + matches!(self, HighDefinition | DefinitiveEdition) + } +} + +/// A bit of a weird comparing check +/// It follows the hierarchy of what game is based on what +/// +/// Thus AgeOfConquerors is bigger than AgeOfKings and HighDefinition is bigger than AgeOfConquers etc. +/// +/// The confusing part is around HighDefinition and UserPatch +/// UserPatch is neither bigger, smaller or equal to the -new- editions created by MSFT and vice versa +impl PartialOrd for GameVariant { + fn partial_cmp(&self, other: &Self) -> Option { + // quick return for equal stuff :) + if other == self { + return Some(Ordering::Equal); + } + + // Try to not use Ord operators here, to make sure we don't fall in weird recursive traps + let is_mod = self.is_mod() || other.is_mod(); + let update = self.is_update() || other.is_update(); + let original = self.is_original() || other.is_original(); + + // Can't compare between user patch and hd and up + if is_mod && update { + return None; + } + + if original && (is_mod || update) { + return Some(if self.is_original() { + Ordering::Less + } else { + Ordering::Greater + }); + } + + // So this part is a bit confusing + // but basically we removed all comparisons that are between e.g. mod, update and original + // and we removed all comparisons that are equal + // so the only comparison left is within their own class + Some(match self { + // Trial is only compared to AoK and AoC, and is the first version, thus always less + Trial => Ordering::Less, + // AoK can only be greater if compared against trial + AgeOfKings if other == &Trial => Ordering::Greater, + AgeOfKings => Ordering::Less, + // AoC will always be greater + AgeOfConquerors => Ordering::Greater, + // Can we compare UP and FE??? + UserPatch | ForgottenEmpires => return None, + // HD can only be compared to DE, and vice versa + HighDefinition => Ordering::Less, + DefinitiveEdition => Ordering::Greater, + }) + } +} + +/// The game data version string. In practice, this does not really reflect the game version. +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub struct GameVersion([u8; 8]); + +impl From<&[u8; 8]> for GameVersion { + fn from(val: &[u8; 8]) -> Self { + GameVersion(*val) + } +} + +/// I am very lazy :) +impl From<&[u8; 7]> for GameVersion { + fn from(val: &[u8; 7]) -> Self { + let mut whole = [0; 8]; + whole[..7].copy_from_slice(val); + GameVersion(whole) + } +} + +impl Debug for GameVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", std::str::from_utf8(&self.0).unwrap()) + } +} + +impl Display for GameVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", std::str::from_utf8(&self.0).unwrap()) + } +} + +impl GameVersion { + /// Read the game version string from an input stream. + pub fn read_from(input: &mut R) -> crate::Result { + let mut game_version = [0; 8]; + input.read_exact(&mut game_version)?; + Ok(Self(game_version)) + } +} + +#[derive(Debug, Clone)] +pub struct HDGameOptions { + pub dlc_options: DLCOptions, + pub difficulty: Difficulty, + pub map_size: MapSize, + pub map_type: MapType, + pub visibility: Visibility, + pub starting_resources: ResourceLevel, + pub starting_age: Age, + pub ending_age: Age, + pub game_mode: GameMode, + // if version < 1001 + pub random_map_name: Option, + // if version < 1001 + pub scenario_name: Option, + pub game_speed: GameSpeed, + pub treaty_length: i32, + pub population_limit: i32, + pub num_players: i32, + pub victory_amount: i32, + pub trading_enabled: bool, + pub team_bonuses_enabled: bool, + pub randomize_positions_enabled: bool, + pub full_tech_tree_enabled: bool, + pub num_starting_units: i8, + pub teams_locked: bool, + pub speed_locked: bool, + pub multiplayer: bool, + pub cheats_enabled: bool, + pub record_game: bool, + pub animals_enabled: bool, + pub predators_enabled: bool, + // if version > 1.16 && version < 1002 + pub scenario_player_indices: Vec, +} + +#[cfg(test)] +mod test { + use crate::GameVariant::*; + use crate::*; + + #[test] + pub fn test_game_variant_resolution() { + assert_eq!( + Some(Trial), + GameVariant::resolve_variant(&TRIAL_VERSION, 0.0) + ); + + assert_eq!( + Some(AgeOfKings), + GameVariant::resolve_variant(&AGE_OF_KINGS_VERSION, 0.0) + ); + + assert_eq!( + Some(AgeOfConquerors), + GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, 0.0) + ); + + assert_eq!( + Some(HighDefinition), + GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, HD_SAVE_VERSION) + ); + + assert_eq!( + Some(DefinitiveEdition), + GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, DE_SAVE_VERSION) + ); + + assert_eq!( + Some(UserPatch), + GameVariant::resolve_variant(&b"VER 9.A".into(), 0.0), + ); + + assert_eq!( + Some(ForgottenEmpires), + GameVariant::resolve_variant(&b"VER 9.5".into(), 0.0), + ); + } + + #[allow(clippy::bool_assert_comparison)] + #[test] + pub fn test_game_variant_comparison() { + // Am I going add all cases here? WHO KNOWS + assert!(Trial < AgeOfKings); + assert!(AgeOfKings > Trial); + assert!(Trial < AgeOfConquerors); + assert!(AgeOfConquerors > Trial); + assert!(Trial < ForgottenEmpires); + assert!(ForgottenEmpires > Trial); + assert!(Trial < UserPatch); + assert!(UserPatch > Trial); + assert!(Trial < HighDefinition); + assert!(HighDefinition > Trial); + assert!(Trial < DefinitiveEdition); + assert!(DefinitiveEdition > Trial); + + assert!(AgeOfKings < AgeOfConquerors); + assert!(AgeOfConquerors > AgeOfKings); + assert!(AgeOfKings < ForgottenEmpires); + assert!(ForgottenEmpires > AgeOfKings); + assert!(AgeOfKings < UserPatch); + assert!(UserPatch > AgeOfKings); + assert!(AgeOfKings < HighDefinition); + assert!(HighDefinition > AgeOfKings); + assert!(AgeOfKings < DefinitiveEdition); + assert!(DefinitiveEdition > AgeOfKings); + + assert!(AgeOfConquerors < ForgottenEmpires); + assert!(ForgottenEmpires > AgeOfConquerors); + assert!(AgeOfConquerors < UserPatch); + assert!(UserPatch > AgeOfConquerors); + assert!(AgeOfConquerors < HighDefinition); + assert!(HighDefinition > AgeOfConquerors); + assert!(AgeOfConquerors < DefinitiveEdition); + assert!(DefinitiveEdition > AgeOfConquerors); + assert!(DefinitiveEdition >= AgeOfConquerors); + + assert_eq!(false, ForgottenEmpires < UserPatch); + assert_eq!(false, UserPatch > ForgottenEmpires); + assert_eq!(false, ForgottenEmpires < HighDefinition); + assert_eq!(false, HighDefinition > ForgottenEmpires); + assert_eq!(false, ForgottenEmpires < DefinitiveEdition); + assert_eq!(false, DefinitiveEdition > ForgottenEmpires); + + assert_eq!(false, UserPatch < HighDefinition); + assert_eq!(false, HighDefinition > UserPatch); + assert_eq!(false, UserPatch < DefinitiveEdition); + assert_eq!(false, DefinitiveEdition > UserPatch); + + assert!(HighDefinition < DefinitiveEdition); + assert!(DefinitiveEdition > HighDefinition); + // yes i was + } +} diff --git a/crates/genie-rec/test/AgeIIDE_Replay_90000059.aoe2record b/crates/genie-rec/test/AgeIIDE_Replay_90000059.aoe2record new file mode 100644 index 0000000..98711a4 Binary files /dev/null and b/crates/genie-rec/test/AgeIIDE_Replay_90000059.aoe2record differ diff --git a/crates/genie-rec/test/AgeIIDE_Replay_90889731.aoe2record b/crates/genie-rec/test/AgeIIDE_Replay_90889731.aoe2record new file mode 100644 index 0000000..7a9147c Binary files /dev/null and b/crates/genie-rec/test/AgeIIDE_Replay_90889731.aoe2record differ diff --git a/crates/genie-rec/test/SD-AgeIIDE_Replay_181966005.aoe2record b/crates/genie-rec/test/SD-AgeIIDE_Replay_181966005.aoe2record new file mode 100644 index 0000000..9153151 Binary files /dev/null and b/crates/genie-rec/test/SD-AgeIIDE_Replay_181966005.aoe2record differ diff --git a/crates/genie-rec/test/header.bin b/crates/genie-rec/test/header.bin new file mode 100644 index 0000000..87e4c77 Binary files /dev/null and b/crates/genie-rec/test/header.bin differ diff --git a/crates/genie-scx/Cargo.toml b/crates/genie-scx/Cargo.toml index b3f84c6..054d850 100644 --- a/crates/genie-scx/Cargo.toml +++ b/crates/genie-scx/Cargo.toml @@ -1,23 +1,28 @@ [package] name = "genie-scx" version = "4.0.0" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read and write Age of Empires I/II scenario files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-scx" +documentation = "https://docs.rs/genie-scx" +repository.workspace = true +readme = "./README.md" exclude = ["test/scenarios"] [dependencies] -byteorder = "1.3.4" -flate2 = "1.0.18" -genie-support = { version = "^1.0.0", path = "../genie-support", features = ["strings"] } -log = "0.4.11" +byteorder.workspace = true +flate2.workspace = true +genie-support = { version = "^2.0.0", path = "../genie-support", features = [ + "strings", +] } +log = "0.4.17" nohash-hasher = "0.2.0" -rgb = "0.8.25" -thiserror = "1.0.21" -num_enum = "0.5.1" +rgb.workspace = true +thiserror.workspace = true +num_enum.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-scx/LICENSE.md b/crates/genie-scx/LICENSE.md deleted file mode 100644 index 2fb2e74..0000000 --- a/crates/genie-scx/LICENSE.md +++ /dev/null @@ -1,675 +0,0 @@ -### GNU GENERAL PUBLIC LICENSE - -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -### Preamble - -The GNU General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom -to share and change all versions of a program--to make sure it remains -free software for all its users. We, the Free Software Foundation, use -the GNU General Public License for most of our software; it applies -also to any other work released this way by its authors. You can apply -it to your programs, too. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you -have certain responsibilities if you distribute copies of the -software, or if you modify it: responsibilities to respect the freedom -of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the -manufacturer can do so. This is fundamentally incompatible with the -aim of protecting users' freedom to change the software. The -systematic pattern of such abuse occurs in the area of products for -individuals to use, which is precisely where it is most unacceptable. -Therefore, we have designed this version of the GPL to prohibit the -practice for those products. If such problems arise substantially in -other domains, we stand ready to extend this provision to those -domains in future versions of the GPL, as needed to protect the -freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish -to avoid the special danger that patents applied to a free program -could make it effectively proprietary. To prevent this, the GPL -assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and -modification follow. - -### TERMS AND CONDITIONS - -#### 0. Definitions. - -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds -of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of -an exact copy. The resulting work is called a "modified version" of -the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user -through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to -the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -#### 1. Source Code. - -The "source code" for a work means the preferred form of the work for -making modifications to it. "Object code" means any non-source form of -a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can -regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same -work. - -#### 2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, -without conditions so long as your license otherwise remains in force. -You may convey covered works to others for the sole purpose of having -them make modifications exclusively for you, or provide you with -facilities for running those works, provided that you comply with the -terms of this License in conveying all material for which you do not -control copyright. Those thus making or running the covered works for -you must do so exclusively on your behalf, under your direction and -control, on terms that prohibit them from making any copies of your -copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes -it unnecessary. - -#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such -circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit -operation or modification of the work as a means of enforcing, against -the work's users, your or third parties' legal rights to forbid -circumvention of technological measures. - -#### 4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -#### 5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these -conditions: - -- a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. -- b) The work must carry prominent notices stating that it is - released under this License and any conditions added under - section 7. This requirement modifies the requirement in section 4 - to "keep intact all notices". -- c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. -- d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -#### 6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of -sections 4 and 5, provided that you also convey the machine-readable -Corresponding Source under the terms of this License, in one of these -ways: - -- a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. -- b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. -- c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. -- d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. -- e) Convey the object code using peer-to-peer transmission, - provided you inform other peers where the object code and - Corresponding Source of the work are being offered to the general - public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, -family, or household purposes, or (2) anything designed or sold for -incorporation into a dwelling. In determining whether a product is a -consumer product, doubtful cases shall be resolved in favor of -coverage. For a particular product received by a particular user, -"normally used" refers to a typical or common use of that class of -product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected -to use, the product. A product is a consumer product regardless of -whether the product has substantial commercial, industrial or -non-consumer uses, unless such uses represent the only significant -mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to -install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The -information must suffice to ensure that the continued functioning of -the modified object code is in no case prevented or interfered with -solely because modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or -updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or -installed. Access to a network may be denied when the modification -itself materially and adversely affects the operation of the network -or violates the rules and protocols for communication across the -network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -#### 7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders -of that material) supplement the terms of this License with terms: - -- a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or -- b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or -- c) Prohibiting misrepresentation of the origin of that material, - or requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or -- d) Limiting the use for publicity purposes of names of licensors - or authors of the material; or -- e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or -- f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions - of it) with contractual assumptions of liability to the recipient, - for any liability that these contractual assumptions directly - impose on those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; the -above requirements apply either way. - -#### 8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -#### 9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run -a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -#### 10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -#### 11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned -or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within the -scope of its coverage, prohibits the exercise of, or is conditioned on -the non-exercise of one or more of the rights that are specifically -granted under this License. You may not convey a covered work if you -are a party to an arrangement with a third party that is in the -business of distributing software, under which you make payment to the -third party based on the extent of your activity of conveying the -work, and under which the third party grants, to any of the parties -who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in -connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent -license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -#### 12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under -this License and any other pertinent obligations, then as a -consequence you may not convey it at all. For example, if you agree to -terms that obligate you to collect a royalty for further conveying -from those to whom you convey the Program, the only way you could -satisfy both those terms and this License would be to refrain entirely -from conveying the Program. - -#### 13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -#### 14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions -of the GNU General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in -detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies that a certain numbered version of the GNU General Public -License "or any later version" applies to it, you have the option of -following the terms and conditions either of that numbered version or -of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the GNU General Public -License, you may choose any version ever published by the Free -Software Foundation. - -If the Program specifies that a proxy can decide which future versions -of the GNU General Public License can be used, that proxy's public -statement of acceptance of a version permanently authorizes you to -choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -#### 15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT -WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. - -#### 16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR -CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT -NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR -LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -#### 17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -### How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these -terms. - -To do so, attach the following notices to the program. It is safest to -attach them to the start of each source file to most effectively state -the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper -mail. - -If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands \`show w' and \`show c' should show the -appropriate parts of the General Public License. Of course, your -program's commands might be different; for a GUI interface, you would -use an "about box". - -You should also get your employer (if you work as a programmer) or -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. For more information on this, and how to apply and follow -the GNU GPL, see . - -The GNU General Public License does not permit incorporating your -program into proprietary programs. If your program is a subroutine -library, you may consider it more useful to permit linking proprietary -applications with the library. If this is what you want to do, use the -GNU Lesser General Public License instead of this License. But first, -please read . diff --git a/crates/genie-scx/README.md b/crates/genie-scx/README.md index 1672683..8a64b08 100644 --- a/crates/genie-scx/README.md +++ b/crates/genie-scx/README.md @@ -1,10 +1,11 @@ -# genie-scx-rs +# genie-scx -Age of Empires 2 scenario reader, writer, and converter. - -## Usage +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--scx-blue?style=flat-square&color=blue)](https://docs.rs/genie-scx) +[![crates.io](https://img.shields.io/crates/v/genie-scx.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-scx) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) -See [docs.rs](https://docs.rs/genie-scx) for API documentation. +Age of Empires 2 scenario reader, writer, and converter. ## License diff --git a/crates/genie-scx/src/ai.rs b/crates/genie-scx/src/ai.rs index 6ec6cbc..ec2e12b 100644 --- a/crates/genie-scx/src/ai.rs +++ b/crates/genie-scx/src/ai.rs @@ -111,14 +111,13 @@ impl AIErrorInfo { pub fn write_to(&self, mut output: impl Write) -> Result<()> { // TODO support non UTF8 encoding let mut filename_bytes = [0; 257]; - (&mut filename_bytes[..self.filename.len()]).copy_from_slice(self.filename.as_bytes()); + filename_bytes[..self.filename.len()].copy_from_slice(self.filename.as_bytes()); output.write_all(&filename_bytes)?; output.write_i32::(self.line_number)?; let mut description_bytes = [0; 128]; - (&mut description_bytes[..self.description.len()]) - .copy_from_slice(self.description.as_bytes()); + description_bytes[..self.description.len()].copy_from_slice(self.description.as_bytes()); output.write_all(&description_bytes)?; output.write_u32::(self.error_code.into())?; @@ -185,7 +184,7 @@ impl AIInfo { } pub fn write_to(&self, mut output: impl Write) -> Result<()> { - output.write_u32::(if self.files.is_empty() { 0 } else { 1 })?; + output.write_u32::(u32::from(!self.files.is_empty()))?; if let Some(error) = &self.error { output.write_u32::(1)?; diff --git a/crates/genie-scx/src/bitmap.rs b/crates/genie-scx/src/bitmap.rs index a994364..b1088a9 100644 --- a/crates/genie-scx/src/bitmap.rs +++ b/crates/genie-scx/src/bitmap.rs @@ -25,18 +25,21 @@ pub struct BitmapInfo { impl BitmapInfo { /// Read a bitmap header info structure from a byte stream. pub fn read_from(mut input: impl Read) -> Result { - let mut bitmap = Self::default(); - bitmap.size = input.read_u32::()?; - bitmap.width = input.read_i32::()?; - bitmap.height = input.read_i32::()?; - bitmap.planes = input.read_u16::()?; - bitmap.bit_count = input.read_u16::()?; - bitmap.compression = input.read_u32::()?; - bitmap.size_image = input.read_u32::()?; - bitmap.xpels_per_meter = input.read_i32::()?; - bitmap.ypels_per_meter = input.read_i32::()?; - bitmap.clr_used = input.read_u32::()?; - bitmap.clr_important = input.read_u32::()?; + let mut bitmap = BitmapInfo { + size: input.read_u32::()?, + width: input.read_i32::()?, + height: input.read_i32::()?, + planes: input.read_u16::()?, + bit_count: input.read_u16::()?, + compression: input.read_u32::()?, + size_image: input.read_u32::()?, + xpels_per_meter: input.read_i32::()?, + ypels_per_meter: input.read_i32::()?, + clr_used: input.read_u32::()?, + clr_important: input.read_u32::()?, + ..Default::default() + }; + for _ in 0..256 { let r = input.read_u8()?; let g = input.read_u8()?; diff --git a/crates/genie-scx/src/convert/mod.rs b/crates/genie-scx/src/convert/mod.rs index ba2f075..5b37dfe 100644 --- a/crates/genie-scx/src/convert/mod.rs +++ b/crates/genie-scx/src/convert/mod.rs @@ -30,13 +30,9 @@ pub enum ConvertError { /// use genie_scx::convert::AutoToWK; /// AutoToWK::default().convert(&mut scenario)? /// ``` -pub struct AutoToWK {} -impl Default for AutoToWK { - fn default() -> Self { - AutoToWK {} - } -} +#[derive(Debug, Default)] +pub struct AutoToWK {} /// Check if a scenario object is likely a WololoKingdoms one. fn is_wk_object(object: &ScenarioObject) -> bool { diff --git a/crates/genie-scx/src/format.rs b/crates/genie-scx/src/format.rs index 098ba29..e4624de 100644 --- a/crates/genie-scx/src/format.rs +++ b/crates/genie-scx/src/format.rs @@ -14,7 +14,8 @@ 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, write_opt_str, write_str, ReadStringsExt, StringKey, UnitTypeID, + f32_eq, read_opt_u32, write_opt_str, write_str, ReadSkipExt, ReadStringsExt, StringKey, + UnitTypeID, }; use std::convert::{TryFrom, TryInto}; use std::io::{self, Read, Write}; @@ -136,6 +137,7 @@ pub(crate) struct RGEScen { pregame_cinematic: Option, victory_cinematic: Option, loss_cinematic: Option, + #[allow(dead_code)] mission_bmp: Option, player_build_lists: Vec>, player_city_plans: Vec>, @@ -148,6 +150,7 @@ impl RGEScen { pub fn read_from(mut input: impl Read) -> Result { let version = input.read_f32::()?; log::debug!("RGEScen version {}", version); + dbg!(version, version.to_le_bytes()); let mut player_names = vec![None; 16]; if version > 1.13 { for name in player_names.iter_mut() { @@ -196,7 +199,6 @@ impl RGEScen { // File name may be empty for embedded scenario data inside recorded games. let name = input.read_u16_length_prefixed_str()?.unwrap_or_default(); - let ( description_string_table, hints_string_table, @@ -259,6 +261,16 @@ impl RGEScen { None }; + if version >= 1.41 { + // 6 zeroes? + input.skip(6)?; + // what is this data?, just repeating 0xFEFF_FFFF + input.skip(24)?; + let _description = input.read_u16_length_prefixed_str()?; + // ?? + input.skip(32)?; + } + let mut player_build_lists = vec![None; 16]; for build_list in player_build_lists.iter_mut() { *build_list = input.read_u16_length_prefixed_str()?; @@ -368,7 +380,7 @@ impl RGEScen { } if version >= 1.07 { - output.write_u8(if self.victory_conquest { 1 } else { 0 })?; + output.write_u8(u8::from(self.victory_conquest))?; } // RGE_Timeline @@ -540,16 +552,18 @@ pub struct TribeScen { /// /// According to [AoE2ScenarioParser][]. /// [AoE2ScenarioParser]: https://github.com/KSneijders/AoE2ScenarioParser/blob/8e3abd422164961aa5c7857350475088790804f8/AoE2ScenarioParser/pieces/options.py + #[allow(dead_code)] combat_mode: i32, /// (What exactly?) /// /// According to [AoE2ScenarioParser][]. /// [AoE2ScenarioParser]: https://github.com/KSneijders/AoE2ScenarioParser/blob/8e3abd422164961aa5c7857350475088790804f8/AoE2ScenarioParser/pieces/options.py + #[allow(dead_code)] naval_mode: i32, /// Whether "All Techs" is enabled. all_techs: bool, /// The starting age per player. - player_start_ages: Vec, + player_start_ages: Vec, /// The initial camera location. view: (i32, i32), /// The map type. @@ -613,7 +627,6 @@ impl TribeScen { let victory = VictoryInfo::read_from(&mut input)?; let victory_all_flag = input.read_i32::()? != 0; - let mp_victory_type = if version >= 1.13 { input.read_i32::()? } else { @@ -756,10 +769,10 @@ impl TribeScen { (0, false) }; - let mut player_start_ages = vec![StartingAge::Default; 16]; + let mut player_start_ages = vec![AgeIdentifier::Default; 16]; if version > 1.05 { for start_age in player_start_ages.iter_mut() { - *start_age = StartingAge::try_from(input.read_i32::()?, version)?; + *start_age = AgeIdentifier::try_from(input.read_i32::()?, version)?; } } @@ -891,7 +904,7 @@ impl TribeScen { } self.victory.write_to(&mut output)?; - output.write_i32::(if self.victory_all_flag { 1 } else { 0 })?; + output.write_i32::(i32::from(self.victory_all_flag))?; if version >= 1.13 { output.write_i32::(self.mp_victory_type)?; @@ -923,12 +936,12 @@ impl TribeScen { } 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_i8(i8::from(self.teams_locked))?; + output.write_i8(i8::from(self.can_change_teams))?; + output.write_i8(i8::from(self.random_start_locations))?; output.write_u8(self.max_teams)?; } else if f32_eq!(version, 1.23) { - output.write_i32::(if self.teams_locked { 1 } else { 0 })?; + output.write_i32::(i32::from(self.teams_locked))?; } if version >= 1.28 { @@ -1042,7 +1055,7 @@ impl TribeScen { } if version >= 1.12 { output.write_i32::(0)?; - output.write_i32::(if self.all_techs { 1 } else { 0 })?; + output.write_i32::(i32::from(self.all_techs))?; } if version > 1.05 { @@ -1081,10 +1094,10 @@ impl TribeScen { 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 })?; + output.write_u8(u8::from(self.collide_and_correct))?; } if version >= 1.37 { - output.write_u8(if self.villager_force_drop { 1 } else { 0 })?; + output.write_u8(u8::from(self.villager_force_drop))?; } Ok(()) diff --git a/crates/genie-scx/src/header.rs b/crates/genie-scx/src/header.rs index 6843d17..a1f2ee2 100644 --- a/crates/genie-scx/src/header.rs +++ b/crates/genie-scx/src/header.rs @@ -101,7 +101,7 @@ impl SCXHeader { 0 }; let description = if format_version == *b"3.13" { - input.read_hd_style_str()? + input.read_tlv_str()? } else { input.read_u32_length_prefixed_str()? }; @@ -169,7 +169,7 @@ impl SCXHeader { } intermediate.write_all(&description_bytes)?; - intermediate.write_u32::(if self.any_sp_victory { 1 } else { 0 })?; + intermediate.write_u32::(u32::from(self.any_sp_victory))?; intermediate.write_u32::(self.active_player_count)?; if version > 2 && format_version != *b"3.13" { diff --git a/crates/genie-scx/src/lib.rs b/crates/genie-scx/src/lib.rs index 4b908e5..64ca12c 100644 --- a/crates/genie-scx/src/lib.rs +++ b/crates/genie-scx/src/lib.rs @@ -82,7 +82,7 @@ pub enum Error { ParseDLCPackageError(#[from] ParseDLCPackageError), /// The given ID is not a known starting age in AoE1 or AoE2. #[error(transparent)] - ParseStartingAgeError(#[from] ParseStartingAgeError), + ParseStartingAgeError(#[from] ParseAgeIdentifierError), /// The given ID is not a known error code. #[error(transparent)] ParseAIErrorCodeError(#[from] num_enum::TryFromPrimitiveError), @@ -214,8 +214,7 @@ impl Scenario { self.format .player_objects .iter() - .map(|list| list.iter()) - .flatten() + .flat_map(|list| list.iter()) } /// Iterate mutably over all the objects placed in the scenario. @@ -224,8 +223,7 @@ impl Scenario { self.format .player_objects .iter_mut() - .map(|list| list.iter_mut()) - .flatten() + .flat_map(|list| list.iter_mut()) } pub fn world_players(&self) -> &[WorldPlayerData] { diff --git a/crates/genie-scx/src/map.rs b/crates/genie-scx/src/map.rs index cc29d6b..c63f296 100644 --- a/crates/genie-scx/src/map.rs +++ b/crates/genie-scx/src/map.rs @@ -151,7 +151,7 @@ impl Map { output.write_u32::(version)?; } if version >= 2 { - output.write_u8(if self.render_waves { 0 } else { 1 })?; + output.write_u8(u8::from(!self.render_waves))?; } output.write_u32::(self.width)?; diff --git a/crates/genie-scx/src/player.rs b/crates/genie-scx/src/player.rs index 0b71ea1..3bf9148 100644 --- a/crates/genie-scx/src/player.rs +++ b/crates/genie-scx/src/player.rs @@ -183,7 +183,7 @@ impl ScenarioPlayerData { output.write_i16::(self.location.1)?; if version > 1.0 { - output.write_u8(if self.allied_victory { 1 } else { 0 })?; + output.write_u8(u8::from(self.allied_victory))?; }; output.write_i16::(self.relations.len() as i16)?; diff --git a/crates/genie-scx/src/triggers.rs b/crates/genie-scx/src/triggers.rs index c95c675..ac2e45e 100644 --- a/crates/genie-scx/src/triggers.rs +++ b/crates/genie-scx/src/triggers.rs @@ -581,8 +581,8 @@ impl Trigger { objective_order, start_time, description, - short_description, short_description_id, + short_description, display_short_description, short_description_state, mute_objective, @@ -597,19 +597,19 @@ impl Trigger { /// Write this trigger condition to an output stream, with the given trigger system version. pub fn write_to(&self, mut output: impl Write, version: f64) -> Result<()> { - output.write_i32::(if self.enabled { 1 } else { 0 })?; - output.write_i8(if self.looping { 1 } else { 0 })?; + output.write_i32::(i32::from(self.enabled))?; + output.write_i8(i8::from(self.looping))?; output.write_i32::(self.name_id)?; - output.write_i8(if self.is_objective { 1 } else { 0 })?; + output.write_i8(i8::from(self.is_objective))?; output.write_i32::(self.objective_order)?; if version >= 1.8 { - output.write_u8(if self.make_header { 1 } else { 0 })?; + output.write_u8(u8::from(self.make_header))?; write_opt_string_key(&mut output, &self.short_description_id)?; - output.write_u8(if self.display_short_description { 1 } else { 0 })?; + output.write_u8(u8::from(self.display_short_description))?; output.write_u8(self.short_description_state)?; output.write_u32::(self.start_time)?; - output.write_u8(if self.mute_objective { 1 } else { 0 })?; + output.write_u8(u8::from(self.mute_objective))?; } else { output.write_u32::(self.start_time)?; } @@ -793,7 +793,7 @@ impl TriggerSystem { output.write_u32::(num_custom_names as u32)?; for (index, name) in custom_names { output.write_u32::(index as u32)?; - write_i32_str(&mut output, &name)?; + write_i32_str(&mut output, name)?; } } Ok(()) diff --git a/crates/genie-scx/src/types.rs b/crates/genie-scx/src/types.rs index da32e8e..f22b061 100644 --- a/crates/genie-scx/src/types.rs +++ b/crates/genie-scx/src/types.rs @@ -211,54 +211,60 @@ fn expected_range(version: f32) -> &'static str { /// Could not parse a starting age because given number refers to an unknown age. #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("invalid starting age {} (must be {})", .found, expected_range(*.version))] -pub struct ParseStartingAgeError { +pub struct ParseAgeIdentifierError { version: f32, found: i32, } /// The starting age. #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StartingAge { +pub enum AgeIdentifier { /// Use the game default. Default = -1, - /// Start in the Dark Age with Nomad resources. + /// Start/End in the Dark Age with Nomad resources. Nomad = -2, - /// Start in the Dark Age. + /// Start/End in the Dark Age. DarkAge = 0, - /// Start in the Feudal Age. + /// Start/End in the Feudal Age. FeudalAge = 1, - /// Start in the Castle Age. + /// Start/End in the Castle Age. CastleAge = 2, - /// Start in the Imperial Age. + /// Start/End in the Imperial Age. ImperialAge = 3, - /// Start in the Imperial Age with all technologies researched. + /// Start/End in the Imperial Age with all technologies researched. PostImperialAge = 4, } -impl StartingAge { - /// Convert a starting age number to the appropriate enum for a particular +impl Default for AgeIdentifier { + fn default() -> Self { + AgeIdentifier::Default + } +} + +impl AgeIdentifier { + /// Convert a start/end age number to the appropriate enum for a particular /// data version. - pub fn try_from(n: i32, version: f32) -> Result { + pub fn try_from(n: i32, version: f32) -> Result { if version < 1.25 { match n { - -1 => Ok(StartingAge::Default), - 0 => Ok(StartingAge::DarkAge), - 1 => Ok(StartingAge::FeudalAge), - 2 => Ok(StartingAge::CastleAge), - 3 => Ok(StartingAge::ImperialAge), - 4 => Ok(StartingAge::PostImperialAge), - _ => Err(ParseStartingAgeError { version, found: n }), + -1 => Ok(AgeIdentifier::Default), + 0 => Ok(AgeIdentifier::DarkAge), + 1 => Ok(AgeIdentifier::FeudalAge), + 2 => Ok(AgeIdentifier::CastleAge), + 3 => Ok(AgeIdentifier::ImperialAge), + 4 => Ok(AgeIdentifier::PostImperialAge), + _ => Err(ParseAgeIdentifierError { version, found: n }), } } else { match n { - -1 | 0 => Ok(StartingAge::Default), - 1 => Ok(StartingAge::Nomad), - 2 => Ok(StartingAge::DarkAge), - 3 => Ok(StartingAge::FeudalAge), - 4 => Ok(StartingAge::CastleAge), - 5 => Ok(StartingAge::ImperialAge), - 6 => Ok(StartingAge::PostImperialAge), - _ => Err(ParseStartingAgeError { version, found: n }), + -1 | 0 => Ok(AgeIdentifier::Default), + 1 => Ok(AgeIdentifier::Nomad), + 2 => Ok(AgeIdentifier::DarkAge), + 3 => Ok(AgeIdentifier::FeudalAge), + 4 => Ok(AgeIdentifier::CastleAge), + 5 => Ok(AgeIdentifier::ImperialAge), + 6 => Ok(AgeIdentifier::PostImperialAge), // can be DM start as well + _ => Err(ParseAgeIdentifierError { version, found: n }), } } } @@ -267,22 +273,22 @@ impl StartingAge { pub fn to_i32(self, version: f32) -> i32 { if version < 1.25 { match self { - StartingAge::Default => -1, - StartingAge::Nomad | StartingAge::DarkAge => 0, - StartingAge::FeudalAge => 1, - StartingAge::CastleAge => 2, - StartingAge::ImperialAge => 3, - StartingAge::PostImperialAge => 4, + AgeIdentifier::Default => -1, + AgeIdentifier::Nomad | AgeIdentifier::DarkAge => 0, + AgeIdentifier::FeudalAge => 1, + AgeIdentifier::CastleAge => 2, + AgeIdentifier::ImperialAge => 3, + AgeIdentifier::PostImperialAge => 4, } } else { match self { - StartingAge::Default => 0, - StartingAge::Nomad => 1, - StartingAge::DarkAge => 2, - StartingAge::FeudalAge => 3, - StartingAge::CastleAge => 4, - StartingAge::ImperialAge => 5, - StartingAge::PostImperialAge => 6, + AgeIdentifier::Default => 0, + AgeIdentifier::Nomad => 1, + AgeIdentifier::DarkAge => 2, + AgeIdentifier::FeudalAge => 3, + AgeIdentifier::CastleAge => 4, + AgeIdentifier::ImperialAge => 5, + AgeIdentifier::PostImperialAge => 6, // can be DM start as well } } } @@ -471,10 +477,7 @@ impl VersionBundle { /// Returns whether this version is (likely) for an AoK scenario. pub fn is_aok(&self) -> bool { - match self.format.as_bytes() { - b"1.18" | b"1.19" | b"1.20" => true, - _ => false, - } + matches!(self.format.as_bytes(), b"1.18" | b"1.19" | b"1.20") } /// Returns whether this version is (likely) for an AoC scenario. diff --git a/crates/genie-scx/src/victory.rs b/crates/genie-scx/src/victory.rs index 87f390c..3b639f9 100644 --- a/crates/genie-scx/src/victory.rs +++ b/crates/genie-scx/src/victory.rs @@ -61,7 +61,7 @@ impl LegacyVictoryInfo { /// Write old-tyle victory settings to an output stream. pub fn write_to(&self, mut output: impl Write) -> Result<()> { output.write_i32::(self.object_type)?; - output.write_i32::(if self.all_flag { 1 } else { 0 })?; + output.write_i32::(i32::from(self.all_flag))?; output.write_i32::(self.player_id)?; output.write_i32::(self.dest_object_id)?; output.write_f32::(self.area.0)?; @@ -263,7 +263,8 @@ impl VictoryConditions { #[deprecated = "Use VictoryConditions::read_from instead"] #[doc(hidden)] pub fn from(input: &mut R, has_version: bool) -> Result { - Ok(Self::read_from(input, has_version)?) + let result = Self::read_from(input, has_version)?; + Ok(result) } /// Read victory conditions from an input stream. @@ -373,7 +374,7 @@ impl VictoryInfo { } pub fn write_to(&self, mut output: impl Write) -> Result<()> { - output.write_i32::(if self.conquest { 1 } else { 0 })?; + output.write_i32::(i32::from(self.conquest))?; output.write_i32::(self.ruins)?; output.write_i32::(self.relics)?; output.write_i32::(self.discoveries)?; diff --git a/crates/genie-support/Cargo.toml b/crates/genie-support/Cargo.toml index df42183..e7dc983 100644 --- a/crates/genie-support/Cargo.toml +++ b/crates/genie-support/Cargo.toml @@ -1,20 +1,23 @@ [package] name = "genie-support" -version = "1.0.0" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +version = "2.0.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Support library for genie-* crates" -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-support" +documentation = "https://docs.rs/genie-support" +repository.workspace = true +readme = "./README.md" [dependencies] -byteorder = "1.3.4" -encoding_rs = { version = "0.8.24", optional = true } -thiserror = "1.0.21" +byteorder.workspace = true +encoding_rs = { version = "0.8.31", optional = true } +thiserror.workspace = true [features] strings = ["encoding_rs"] [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/genie-support/README.md b/crates/genie-support/README.md index 97b52cb..feac182 100644 --- a/crates/genie-support/README.md +++ b/crates/genie-support/README.md @@ -1,5 +1,10 @@ # genie-support +[![docs.rs](https://img.shields.io/badge/docs.rs-genie--support-blue?style=flat-square&color=blue)](https://docs.rs/genie-support) +[![crates.io](https://img.shields.io/crates/v/genie-support.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-support) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) + Support library for `genie-*` crates. ## License diff --git a/crates/genie-support/src/lib.rs b/crates/genie-support/src/lib.rs index 1f4296d..8063ab6 100644 --- a/crates/genie-support/src/lib.rs +++ b/crates/genie-support/src/lib.rs @@ -13,6 +13,7 @@ mod map_into; mod read; #[cfg(feature = "strings")] mod strings; +mod versions; pub use ids::*; pub use macros::*; @@ -20,3 +21,4 @@ pub use map_into::*; pub use read::*; #[cfg(feature = "strings")] pub use strings::*; +pub use versions::*; diff --git a/crates/genie-support/src/read.rs b/crates/genie-support/src/read.rs index 8dba1f9..a888883 100644 --- a/crates/genie-support/src/read.rs +++ b/crates/genie-support/src/read.rs @@ -16,7 +16,7 @@ use std::io::{self, Error, ErrorKind, Read, Result}; /// assert_eq!(read_opt_u16(&mut zero).unwrap(), Some(0)); /// ``` #[inline] -pub fn read_opt_u16(mut input: R) -> Result> +pub fn read_opt_u16(input: &mut R) -> Result> where T: TryFrom, T::Error: std::error::Error + Send + Sync + 'static, @@ -46,7 +46,7 @@ where /// assert_eq!(read_opt_u32(&mut one).unwrap(), Some(1)); /// ``` #[inline] -pub fn read_opt_u32(mut input: R) -> Result> +pub fn read_opt_u32(input: &mut R) -> Result> where T: TryFrom, T::Error: std::error::Error + Send + Sync + 'static, @@ -76,3 +76,27 @@ impl ReadSkipExt for T { Ok(()) } } + +/// Very simple struct that tracks the position inside a `Read` +pub struct Tracker { + inner: T, + position: u64, +} + +impl Tracker { + pub fn new(inner: T) -> Self { + Tracker { inner, position: 0 } + } + + pub fn position(&self) -> u64 { + self.position + } +} + +impl Read for Tracker { + fn read(&mut self, buf: &mut [u8]) -> Result { + let read = self.inner.read(buf)?; + self.position += read as u64; + Ok(read) + } +} diff --git a/crates/genie-support/src/strings.rs b/crates/genie-support/src/strings.rs index 4e047f9..a49646c 100644 --- a/crates/genie-support/src/strings.rs +++ b/crates/genie-support/src/strings.rs @@ -79,7 +79,7 @@ pub fn write_opt_str( option: &Option, ) -> Result<(), WriteStringError> { if let Some(ref string) = option { - write_str(output, &string) + write_str(output, string) } else { output.write_i16::(0)?; Ok(()) @@ -94,7 +94,7 @@ pub fn write_opt_i32_str( option: &Option, ) -> Result<(), WriteStringError> { if let Some(ref string) = option { - write_i32_str(output, &string) + write_i32_str(output, string) } else { output.write_i32::(0)?; Ok(()) @@ -107,7 +107,7 @@ fn decode_str(bytes: &[u8]) -> Result { return Ok("".to_string()); } - let (decoded, _enc, failed) = WINDOWS_1252.decode(&bytes); + let (decoded, _enc, failed) = WINDOWS_1252.decode(bytes); if failed { Err(DecodeStringError) } else { @@ -115,6 +115,20 @@ fn decode_str(bytes: &[u8]) -> Result { } } +/// Read and decode NUL delimited string from buffer +pub fn read_str>(bytes: T) -> Result, ReadStringError> { + let bytes = bytes.as_ref(); + let end = bytes + .iter() + .position(|&byte| byte == 0) + .unwrap_or(bytes.len()); + if end == 0 { + Ok(None) + } else { + Ok(Some(decode_str(&bytes[..end])?)) + } +} + /// Functions to read various kinds of strings from input streams. /// Extension trait for reading strings in several common formats used by AoE2. pub trait ReadStringsExt: Read { @@ -123,14 +137,7 @@ pub trait ReadStringsExt: Read { if length > 0 { let mut bytes = vec![0; length as usize]; self.read_exact(&mut bytes)?; - if let Some(end) = bytes.iter().position(|&byte| byte == 0) { - bytes.truncate(end); - } - if bytes.is_empty() { - Ok(None) - } else { - Ok(Some(decode_str(&bytes)?)) - } + read_str(bytes) } else { Ok(None) } @@ -154,19 +161,27 @@ pub trait ReadStringsExt: Read { } } - /// Read an HD Edition style string. + /// Read TLV (Type-Length-Value) string. /// /// Reads a 'signature' value, then the `length` as an u16 value, then reads an optionally /// null-terminated WINDOWS-1252-encoded string of that length in bytes. - fn read_hd_style_str(&mut self) -> Result, ReadStringError> { - let open = self.read_u16::()?; + fn read_tlv_str(&mut self) -> Result, ReadStringError> { + let start = self.read_u16::()?; + // Check that this actually is the start of a string - if open != 0x0A60 { + #[cfg(debug_assertions)] + assert_eq!(start, 0x0A60, "This doesn't seem to be the beginning of a TLV (Type–length–value)! Actual value: {start:#X}"); + + #[cfg(not(debug_assertions))] + if !start.eq(&0x0A60) { return Err(DecodeStringError.into()); } + let len = self.read_u16::()? as usize; + let mut bytes = vec![0; len]; self.read_exact(&mut bytes[0..len])?; + Ok(Some(decode_str(&bytes)?)) } } diff --git a/crates/genie-support/src/versions.rs b/crates/genie-support/src/versions.rs new file mode 100644 index 0000000..582a66d --- /dev/null +++ b/crates/genie-support/src/versions.rs @@ -0,0 +1 @@ +pub const DE_VERSION: f32 = 12.97; diff --git a/crates/jascpal/Cargo.toml b/crates/jascpal/Cargo.toml index e7b2ba2..f776f34 100644 --- a/crates/jascpal/Cargo.toml +++ b/crates/jascpal/Cargo.toml @@ -1,17 +1,21 @@ [package] name = "jascpal" version = "0.1.1" -authors = ["Renée Kooi "] -edition = "2018" -license = "GPL-3.0" +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true description = "Read and write JASC palette files." -homepage = "https://github.com/SiegeEngineers/genie-rs" -repository = "https://github.com/SiegeEngineers/genie-rs" +homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/jascpal" +documentation = "https://docs.rs/jascpal" +repository.workspace = true +readme = "./README.md" + [dependencies] -nom = { version = "6.0.0", default-features = false, features = ["std"] } -rgb = "0.8.25" -thiserror = "1.0.21" +nom = { version = "7.1.1", default-features = false, features = ["std"] } +rgb.workspace = true +thiserror.workspace = true [dev-dependencies] -anyhow = "1.0.33" +anyhow.workspace = true diff --git a/crates/jascpal/README.md b/crates/jascpal/README.md new file mode 100644 index 0000000..1674208 --- /dev/null +++ b/crates/jascpal/README.md @@ -0,0 +1,12 @@ +# jascpal + +[![docs.rs](https://img.shields.io/badge/docs.rs-jascpal-blue?style=flat-square&color=blue)](https://docs.rs/jascpal) +[![crates.io](https://img.shields.io/crates/v/jascpal.svg?style=flat-square&color=orange)](https://crates.io/crates/jascpal) +[![GitHub license](https://img.shields.io/github/license/SiegeEngineers/genie-rs?style=flat-square&color=darkred)](https://github.com/SiegeEngineers/genie-rs/blob/default/LICENSE.md) +![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) + +Read and write JASC palette files. + +## License + +[GPL-3.0](../../LICENSE.md) diff --git a/examples/convertscx.rs b/examples/convert_scx.rs similarity index 94% rename from examples/convertscx.rs rename to examples/convert_scx.rs index 985839d..58689d3 100644 --- a/examples/convertscx.rs +++ b/examples/convert_scx.rs @@ -1,3 +1,6 @@ +extern crate genie; +extern crate structopt; + use genie::scx::{convert::AutoToWK, VersionBundle}; use genie::Scenario; use std::{fs::File, path::PathBuf}; @@ -26,7 +29,7 @@ fn main() -> anyhow::Result<()> { version, } = Cli::from_args(); let version_arg = version; - let version = match version_arg.as_ref().map(|s| &**s) { + let version = match version_arg.as_deref() { Some("aoe") => VersionBundle::aoe(), Some("ror") => VersionBundle::ror(), Some("aoc") => VersionBundle::aoc(), diff --git a/examples/displayaochotkeys.rs b/examples/display_aoc_hotkeys.rs similarity index 92% rename from examples/displayaochotkeys.rs rename to examples/display_aoc_hotkeys.rs index de1169e..1887303 100644 --- a/examples/displayaochotkeys.rs +++ b/examples/display_aoc_hotkeys.rs @@ -6,6 +6,9 @@ // Example language file path: // D:\SteamLibrary\steamapps\common\Age2HD\resources\en\strings\key-value\key-value-strings-utf8.txt +extern crate genie; +extern crate structopt; + use genie::hki::{self, HotkeyInfo}; use genie::lang::LangFileType; use std::fs::File; @@ -42,6 +45,6 @@ fn main() -> anyhow::Result<()> { let info = HotkeyInfo::from(&mut f_hki)?; let aoc_him = hki::default_him(); - println!("{}", info.to_string_lang(&lang_file, &aoc_him)); + println!("{}", info.get_string_from_lang(&lang_file, &aoc_him)); Ok(()) } diff --git a/examples/displaycivs.rs b/examples/display_civs.rs similarity index 93% rename from examples/displaycivs.rs rename to examples/display_civs.rs index 6797986..b5a429e 100644 --- a/examples/displaycivs.rs +++ b/examples/display_civs.rs @@ -1,5 +1,8 @@ //! Displays civilizations from a specified dat file. +extern crate genie; +extern crate structopt; + use genie::DatFile; use std::fs::File; use std::path::PathBuf; diff --git a/examples/displayhotkey.rs b/examples/display_hotkey.rs similarity index 96% rename from examples/displayhotkey.rs rename to examples/display_hotkey.rs index 021f622..8d33344 100644 --- a/examples/displayhotkey.rs +++ b/examples/display_hotkey.rs @@ -1,5 +1,8 @@ //! Displays a hotkey to `stdout`. +extern crate genie; +extern crate structopt; + use genie::hki::HotkeyInfo; use std::fs::File; use std::path::PathBuf; diff --git a/examples/displaylang.rs b/examples/display_lang.rs similarity index 95% rename from examples/displaylang.rs rename to examples/display_lang.rs index 691eb39..cc9bb47 100644 --- a/examples/displaylang.rs +++ b/examples/display_lang.rs @@ -1,5 +1,8 @@ //! Displays the key value pairs in a language file. +extern crate genie; +extern crate structopt; + use genie::lang::LangFileType; use std::fs::File; use std::path::PathBuf; diff --git a/examples/extractcpx.rs b/examples/extract_cpx.rs similarity index 95% rename from examples/extractcpx.rs rename to examples/extract_cpx.rs index 3181ac0..80ef866 100644 --- a/examples/extractcpx.rs +++ b/examples/extract_cpx.rs @@ -1,3 +1,6 @@ +extern crate genie; +extern crate structopt; + use genie::Campaign; use std::{cmp, fs::File, path::PathBuf}; use structopt::StructOpt; @@ -43,10 +46,10 @@ fn list(args: List) { .map(|entry| entry.filename.to_string()) .collect::>(); - for i in 0..campaign.len() { + (0..campaign.len()).for_each(|i| { let bytes = campaign.by_index_raw(i).expect("missing scenario data"); println!("- {} ({})", names[i], format_bytes(bytes.len() as u32)); - } + }); } fn extract(args: Extract) { @@ -62,11 +65,11 @@ fn extract(args: Extract) { .map(|entry| entry.filename.to_string()) .collect::>(); - for i in 0..campaign.len() { + (0..campaign.len()).for_each(|i| { let bytes = campaign.by_index_raw(i).expect("missing scenario data"); println!("{}", names[i]); std::fs::write(dir.join(&names[i]), bytes).expect("failed to write"); - } + }); } /// Derived from https://github.com/banyan/rust-pretty-bytes/blob/master/src/converter.rs diff --git a/examples/extractdrs.rs b/examples/extract_drs.rs similarity index 94% rename from examples/extractdrs.rs rename to examples/extract_drs.rs index 19486db..6019450 100644 --- a/examples/extractdrs.rs +++ b/examples/extract_drs.rs @@ -1,3 +1,7 @@ +extern crate genie; +extern crate genie_drs; +extern crate structopt; + use genie_drs::{DRSReader, DRSWriter, ReserveDirectoryStrategy}; use std::collections::HashSet; use std::fs::{create_dir_all, File}; @@ -94,13 +98,10 @@ fn get(args: Get) -> anyhow::Result<()> { let drs = DRSReader::new(&mut file)?; for table in drs.tables() { - match table.get_resource(args.resource_id) { - Some(ref resource) => { - let buf = drs.read_resource(&mut file, table.resource_type, resource.id)?; - stdout().write_all(&buf)?; - return Ok(()); - } - None => (), + if let Some(resource) = table.get_resource(args.resource_id) { + let buf = drs.read_resource(&mut file, table.resource_type, resource.id)?; + stdout().write_all(&buf)?; + return Ok(()); } } @@ -192,7 +193,7 @@ fn add(args: Add) -> anyhow::Result<()> { for (i, path) in args.file.iter().enumerate() { let mut res_type = [0x20; 4]; let slice = args.table[i].as_bytes(); - (&mut res_type[0..slice.len()]).copy_from_slice(slice); + res_type[0..slice.len()].copy_from_slice(slice); res_type.reverse(); drs_write.add(res_type, args.id[i], File::open(path)?)?; } diff --git a/examples/inspectscx.rs b/examples/inspect_scx.rs similarity index 97% rename from examples/inspectscx.rs rename to examples/inspect_scx.rs index 1ee8d39..012dc8f 100644 --- a/examples/inspectscx.rs +++ b/examples/inspect_scx.rs @@ -1,3 +1,6 @@ +extern crate genie; +extern crate simplelog; + use genie::Scenario; use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode}; use std::fs::File; diff --git a/examples/recactions.rs b/examples/print_rec_actions.rs similarity index 91% rename from examples/recactions.rs rename to examples/print_rec_actions.rs index 6c68ecb..27ccd3b 100644 --- a/examples/recactions.rs +++ b/examples/print_rec_actions.rs @@ -1,3 +1,6 @@ +extern crate genie; +extern crate structopt; + use genie::RecordedGame; use std::fs::File; use std::path::PathBuf; diff --git a/examples/sethotkey.rs b/examples/set_hotkey.rs similarity index 96% rename from examples/sethotkey.rs rename to examples/set_hotkey.rs index 7f2f5b0..89c21e6 100644 --- a/examples/sethotkey.rs +++ b/examples/set_hotkey.rs @@ -1,5 +1,9 @@ //! Sets a hotkey in a hotkey file. +extern crate genie; +extern crate genie_hki; +extern crate structopt; + use genie::hki::HotkeyInfo; use std::fs::File; use std::path::PathBuf; diff --git a/examples/wolololang.rs b/examples/wololo_lang.rs similarity index 96% rename from examples/wolololang.rs rename to examples/wololo_lang.rs index 380a189..a95e240 100644 --- a/examples/wolololang.rs +++ b/examples/wololo_lang.rs @@ -4,6 +4,9 @@ //! The order in which string keys are written to the output file is currently //! unspecified. +extern crate genie; +extern crate structopt; + use genie::lang::LangFileType::KeyValue; use std::fs::File; use std::path::PathBuf; diff --git a/src/lib.rs b/src/lib.rs index 0dbb132..fd621fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,15 @@ #![warn(unused)] #![allow(missing_docs)] +pub extern crate genie_cpx; +pub extern crate genie_dat; +pub extern crate genie_drs; +pub extern crate genie_hki; +pub extern crate genie_lang; +pub extern crate genie_rec; +pub extern crate genie_scx; +pub extern crate jascpal; + pub use genie_cpx as cpx; pub use genie_dat as dat; pub use genie_drs as drs;