├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── crates ├── genie-cpx │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── read.rs │ │ └── write.rs │ └── test │ │ └── campaigns │ │ ├── 10 The First Punic War.aoecpn │ │ ├── AIImprovementsTest.aoe2campaign │ │ ├── Armies at War A Combat Showcase.cpn │ │ ├── DER FALL VON SACSAHUAMAN - TEIL I.cpx │ │ ├── Rise of Egypt Learning Campaign.cpn │ │ ├── acam1.aoe2campaign │ │ └── rcam3.aoe2campaign ├── genie-dat │ ├── Cargo.toml │ ├── README.md │ ├── fixtures │ │ ├── age-of-chivalry.dat │ │ ├── aoc1.0c.dat │ │ ├── aok.dat │ │ └── hd.dat │ └── src │ │ ├── civ.rs │ │ ├── color_table.rs │ │ ├── lib.rs │ │ ├── random_map.rs │ │ ├── sound.rs │ │ ├── sprite.rs │ │ ├── task.rs │ │ ├── tech.rs │ │ ├── tech_tree.rs │ │ ├── terrain.rs │ │ └── unit_type.rs ├── genie-drs │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── read.rs │ │ └── write.rs │ └── test.drs ├── genie-hki │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── lib.rs │ └── test │ │ └── files │ │ ├── aoc1.hki │ │ ├── aoc2.hki │ │ ├── aoc3.hki │ │ ├── hd0.hki │ │ ├── hd1.hki │ │ └── wk.hki ├── genie-lang │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── lib.rs │ └── test │ │ └── dlls │ │ └── language_x1_p1.dll ├── genie-rec │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── actions.rs │ │ ├── ai.rs │ │ ├── element.rs │ │ ├── header.rs │ │ ├── lib.rs │ │ ├── map.rs │ │ ├── player.rs │ │ ├── reader.rs │ │ ├── string_table.rs │ │ ├── unit.rs │ │ ├── unit_action.rs │ │ ├── unit_type.rs │ │ └── version.rs │ └── test │ │ ├── aok.mgl │ │ ├── missyou_finally_vs_11.mgx │ │ └── rec.20181208-195117.mgz ├── genie-scx │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── ai.rs │ │ ├── bitmap.rs │ │ ├── convert │ │ │ ├── aoc_to_wk.rs │ │ │ ├── hd_to_wk.rs │ │ │ └── mod.rs │ │ ├── format.rs │ │ ├── header.rs │ │ ├── lib.rs │ │ ├── map.rs │ │ ├── player.rs │ │ ├── triggers.rs │ │ ├── types.rs │ │ └── victory.rs │ └── test │ │ └── scenarios │ │ ├── The Destruction of Rome.scn │ │ ├── A New Emporer.scn │ │ ├── Age of Heroes b1-3-5.scx │ │ ├── Bronze Age Art of War.scn │ │ ├── CAMELOT.SCN │ │ ├── CEASAR.scn │ │ ├── Corlis.aoescn │ │ ├── Dawn of a New Age.scn │ │ ├── El advenimiento de los hunos_.scx │ │ ├── Hotkey Trainer Buildings.aoe2scenario │ │ ├── Jeremiah Johnson (Update).scx │ │ ├── Year_of_the_Pig.aoe2scenario │ │ ├── layertest.aoe2scenario │ │ └── real_world_amazon.scx ├── genie-support │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── ids.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── map_into.rs │ │ ├── read.rs │ │ └── strings.rs └── jascpal │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── examples ├── convertscx.rs ├── displayaochotkeys.rs ├── displaycivs.rs ├── displayhotkey.rs ├── displaylang.rs ├── extractcpx.rs ├── extractdrs.rs ├── inspectscx.rs ├── recactions.rs ├── sethotkey.rs └── wolololang.rs └── src └── lib.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [windows-latest, macos-latest, ubuntu-latest] 26 | runs-on: ${{matrix.os}} 27 | steps: 28 | - uses: actions/checkout@v1 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | override: true 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | args: --all 38 | 39 | fmt: 40 | name: Rustfmt 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v1 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: stable 48 | override: true 49 | - run: rustup component add rustfmt 50 | - uses: actions-rs/cargo@v1 51 | with: 52 | command: fmt 53 | args: --all -- --check 54 | 55 | clippy: 56 | name: Clippy 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v1 60 | - uses: actions-rs/toolchain@v1 61 | with: 62 | profile: minimal 63 | toolchain: stable 64 | override: true 65 | - run: rustup component add clippy 66 | - uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | # args: -- -D warnings 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # genie-rs change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 0.5.0 8 | * **(breaking)** scx: fix Age of Empires 2: Definitive Edition tile data types. `MapTile.layered_terrain` now contains a u16 instead of a u8. 9 | * **(breaking)** scx: read versioned map data from Age of Empires 2: Definitive Edition. 10 | * **(breaking)** cpx: update genie-scx to v4.0.0. 11 | * cpx: support reading and writing Age of Empires 2: Definitive Edition campaign files. (#22) 12 | * rec: fix small action buffer optimisation. 13 | 14 | ## 0.4.0 15 | * **(breaking)** scx: support Age of Empires 2: Definitive Edition scenario files. (#28) 16 | * **(breaking)** scx: change `DataStruct::from(&mut Read)` methods to `DataStruct::read_from(impl Read)`. (#28) 17 | * **(breaking)** cpx: update genie-scx to v3.0.0. 18 | * cpx: support reading and writing AoE1: Definitive Edition campaign files. (#18) 19 | * dat: Add a `.dat` file reader with support for The Conquerors and the HD Edition. It has some writing support but makes no guarantees yet. 20 | * drs: make `ResourceType` act more like a `&str`. (#15) 21 | * lang: disable unused `pelite` features for leaner DLL reading. 22 | * rec: Add a recorded game file reader with support for Age of Kings and The Conquerors. (#8) 23 | * scx: support writing embedded AI information and triggers. (#17, #28) 24 | * Use `thiserror` for custom error types. (#27) 25 | 26 | ## 0.3.0 27 | * **(breaking)** genie: Raise minimum language version requirement to Rust 1.34, for the `TryFrom` trait. 28 | * **(breaking)** scx: Add descriptive error types. 29 | * **(breaking)** cpx: Add descriptive error types. 30 | * **(breaking)** drs: Add descriptive error types. 31 | * **(breaking)** hki: Add non-destructive update functions for binding hotkeys. (@twestura in #3) 32 | * **(breaking)** lang: Overhaul APIs. (@twestura in #3) 33 | * **(breaking)** pal: Replace `chariot_palette` with custom jascpal crate, adding support for writing palette files. 34 | * drs: Add a DRS file writer. 35 | * cpx: Detect and convert non-UTF8 encodings. 36 | * drs: find resources faster using binary search. (#6) 37 | 38 | ## 0.2.0 39 | * Add a cpx file writer. 40 | * Import genie-drs, for reading .DRS files. 41 | * Add read/write support for .ini and HD Edition key-value language files, and read support for .dll language files. 42 | 43 | ## 0.1.0 44 | * Initial release. 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at renee@kooi.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie" 3 | version = "0.5.0" 4 | description = "Libraries for reading/writing Age of Empires II data files" 5 | documentation = "https://docs.rs/genie/" 6 | homepage = "https://github.com/SiegeEngineers/genie-rs" 7 | readme = "./README.md" 8 | 9 | [workspace] 10 | members = [ 11 | "crates/genie-cpx", 12 | "crates/genie-dat", 13 | "crates/genie-drs", 14 | "crates/genie-hki", 15 | "crates/genie-lang", 16 | "crates/genie-rec", 17 | "crates/genie-scx", 18 | "crates/genie-support", 19 | "crates/jascpal", 20 | ] 21 | 22 | [workspace.package] 23 | authors = ["Renée Kooi "] 24 | edition = "2021" 25 | rust-version = "1.64.0" 26 | license = "GPL-3.0" 27 | repository = "https://github.com/SiegeEngineers/genie-rs" 28 | 29 | [workspace.dependencies] 30 | structopt = "0.3.26" 31 | anyhow = "1.0.65" 32 | simplelog = "0.12.0" 33 | thiserror = "1.0.36" 34 | byteorder = "1.4.3" 35 | flate2 = { version = "1.0.24", features = [ 36 | "rust_backend", 37 | ], default-features = false } 38 | encoding_rs = "0.8.31" 39 | encoding_rs_io = "0.1.7" 40 | rgb = "0.8.34" 41 | num_enum = "0.5.7" 42 | arrayvec = "0.7.2" 43 | 44 | [dependencies] 45 | genie-cpx = { version = "0.5.0", path = "crates/genie-cpx" } 46 | genie-dat = { version = "0.1.0", path = "crates/genie-dat" } 47 | genie-drs = { version = "0.2.1", path = "crates/genie-drs" } 48 | genie-hki = { version = "0.2.1", path = "crates/genie-hki" } 49 | genie-lang = { version = "0.2.1", path = "crates/genie-lang" } 50 | genie-rec = { version = "0.1.1", path = "crates/genie-rec" } 51 | genie-scx = { version = "4.0.0", path = "crates/genie-scx" } 52 | jascpal = { version = "0.1.1", path = "crates/jascpal" } 53 | 54 | [dev-dependencies] 55 | structopt.workspace = true 56 | anyhow.workspace = true 57 | simplelog.workspace = true 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # genie-rs 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie-blue?style=flat-square)](https://docs.rs/genie/) 4 | [![crates.io](https://img.shields.io/crates/v/genie.svg?style=flat-square)](https://crates.io/crates/genie) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Rust libraries for reading/writing various Age of Empires I/II files. 9 | 10 | ## Example Programs 11 | 12 | ```bash 13 | # Extract scenario files from a campaign to the working directory. 14 | cargo run --example extractcpx ~/path/to/campaign.cpx 15 | 16 | # Show the scenario files in a campaign file. 17 | cargo run --example extractcpx ~/path/to/campaign.cpx -l 18 | 19 | # Convert an HD Edition (+expansions) scenario to WololoKingdoms. 20 | cargo run --example convertscx ~/path/to/input.aoe2scenario ~/path/to/output.scx wk 21 | 22 | # Display contents of a language file. 23 | cargo run --example displaylang ~/path/to/input/language.dll dll 24 | cargo run --example displaylang ~/path/to/input/language.ini ini 25 | cargo run --example displaylang ~/path/to/input/key-value-strings.txt key-value 26 | 27 | # Convert HD Edition key-value.txt language files to language.ini files for Voobly or aoc-language-ini 28 | cargo run --example wolololang ~/path/to/input/key-value-strings.txt ~/path/to/output/language.ini 29 | ``` 30 | 31 | ## License 32 | 33 | [GPL-3.0](./LICENSE.md) 34 | -------------------------------------------------------------------------------- /crates/genie-cpx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-cpx" 3 | version = "0.5.0" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read and write Age of Empires I/II campaign files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-cpx" 10 | documentation = "https://docs.rs/genie-cpx" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | exclude = ["test/campaigns"] 14 | 15 | [dependencies] 16 | byteorder.workspace = true 17 | chardet = "0.2.4" 18 | encoding_rs.workspace = true 19 | genie-scx = { version = "4.0.0", path = "../genie-scx" } 20 | thiserror.workspace = true 21 | 22 | [dev-dependencies] 23 | anyhow.workspace = true 24 | -------------------------------------------------------------------------------- /crates/genie-cpx/README.md: -------------------------------------------------------------------------------- 1 | # genie-cpx 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--cpx-blue?style=flat-square&color=blue)](https://docs.rs/genie-cpx/) 4 | [![crates.io](https://img.shields.io/crates/v/genie-cpx.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-cpx) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read and write Age of Empires I/II campaign files. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-cpx/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Campaign files store multiple scenario files in one easily distributable chunk. 2 | //! 3 | //! genie-cpx can read and write campaign files using the Campaign and CampaignWriter structs, 4 | //! respectively. 5 | 6 | #![deny(future_incompatible)] 7 | #![deny(nonstandard_style)] 8 | #![deny(rust_2018_idioms)] 9 | #![deny(unsafe_code)] 10 | #![warn(unused)] 11 | #![allow(missing_docs)] 12 | 13 | use std::io::{Read, Seek, Write}; 14 | 15 | mod read; 16 | mod write; 17 | 18 | pub use read::{Campaign, ReadCampaignError}; 19 | pub use write::{CampaignWriter, WriteCampaignError}; 20 | 21 | /// Version identifier for the campaign file format. 22 | /// 23 | /// Use one of the AOE_AOK or AOE1_DE constants rather than writing them out manually. 24 | pub type CPXVersion = [u8; 4]; 25 | 26 | /// Version identifier for AoE1, AoE2, and AoE2: HD campaign files. 27 | pub const AOE_AOK: CPXVersion = *b"1.00"; 28 | /// Version identifier for AoE1: Definitive Edition campaign files. 29 | pub const AOE1_DE: CPXVersion = *b"1.10"; 30 | /// Version identifier for AoE2: Definitive Edition campaign files. 31 | pub const AOE2_DE: CPXVersion = *b"2.00"; 32 | 33 | /// Campaign header. 34 | #[derive(Debug, Clone)] 35 | pub(crate) struct CampaignHeader { 36 | /// File format version. 37 | pub(crate) version: CPXVersion, 38 | /// Name of the campaign. 39 | pub(crate) name: String, 40 | /// Amount of scenario files in this campaign. 41 | pub(crate) num_scenarios: usize, 42 | } 43 | 44 | impl CampaignHeader { 45 | pub(crate) fn new(name: &str) -> Self { 46 | Self { 47 | version: AOE_AOK, 48 | name: name.to_string(), 49 | num_scenarios: 0, 50 | } 51 | } 52 | } 53 | 54 | /// Data about a scenario in the campaign file. 55 | #[derive(Debug, Clone)] 56 | pub struct ScenarioMeta { 57 | /// Size in bytes of the scenario file. 58 | pub size: usize, 59 | /// Offset in bytes of the scenario file within the campaign file. 60 | pub(crate) offset: usize, 61 | /// Name of the scenario. 62 | pub name: String, 63 | /// File name of the scenario. 64 | pub filename: String, 65 | } 66 | 67 | impl Campaign 68 | where 69 | R: Read + Seek, 70 | { 71 | /// Write the scenario file to an output stream, using the same version as when reading it. 72 | pub fn write_to(&mut self, output: &mut W) -> Result<(), WriteCampaignError> { 73 | self.write_to_version(output, self.version()) 74 | } 75 | 76 | /// Write the scenario file to an output stream with the given version. 77 | pub fn write_to_version( 78 | &mut self, 79 | output: &mut W, 80 | version: CPXVersion, 81 | ) -> Result<(), WriteCampaignError> { 82 | let mut writer = CampaignWriter::new(self.name(), output).version(version); 83 | 84 | for i in 0..self.len() { 85 | let bytes = self 86 | .by_index_raw(i) 87 | .map_err(|_| WriteCampaignError::NotFoundError(i))?; 88 | match (self.get_name(i), self.get_filename(i)) { 89 | (Some(name), Some(filename)) => { 90 | writer.add_raw(name, filename, bytes); 91 | } 92 | _ => return Err(WriteCampaignError::NotFoundError(i)), 93 | } 94 | } 95 | 96 | let _output = writer.flush()?; 97 | 98 | Ok(()) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use std::fs::File; 106 | use std::io::Cursor; 107 | 108 | #[test] 109 | fn rebuild_cpx() -> anyhow::Result<()> { 110 | let instream = File::open("./test/campaigns/Armies at War A Combat Showcase.cpn")?; 111 | let mut outstream = vec![]; 112 | let mut incpx = Campaign::from(instream)?; 113 | incpx.write_to(&mut outstream)?; 114 | 115 | let mut written_cpx = Campaign::from(Cursor::new(outstream))?; 116 | assert_eq!(written_cpx.name(), incpx.name()); 117 | assert_eq!(written_cpx.len(), incpx.len()); 118 | assert_eq!(written_cpx.by_index_raw(0)?, incpx.by_index_raw(0)?); 119 | Ok(()) 120 | } 121 | 122 | #[test] 123 | fn rebuild_cpn_de() -> anyhow::Result<()> { 124 | let instream = File::open("./test/campaigns/10 The First Punic War.aoecpn")?; 125 | let mut outstream = vec![]; 126 | let mut incpx = Campaign::from(instream)?; 127 | incpx.write_to(&mut outstream)?; 128 | 129 | let mut written_cpx = Campaign::from(Cursor::new(outstream))?; 130 | assert_eq!(written_cpx.name(), incpx.name()); 131 | assert_eq!(written_cpx.len(), incpx.len()); 132 | assert_eq!(written_cpx.version(), incpx.version()); 133 | assert_eq!(written_cpx.get_name(0), incpx.get_name(0)); 134 | assert_eq!(written_cpx.get_filename(0), incpx.get_filename(0)); 135 | assert_eq!(written_cpx.by_index_raw(0)?, incpx.by_index_raw(0)?); 136 | Ok(()) 137 | } 138 | 139 | #[test] 140 | fn rebuild_campaign_de2() -> anyhow::Result<()> { 141 | let instream = File::open("./test/campaigns/AIImprovementsTest.aoe2campaign")?; 142 | let mut outstream = vec![]; 143 | let mut incpx = Campaign::from(instream)?; 144 | incpx.write_to(&mut outstream)?; 145 | 146 | let mut written_cpx = Campaign::from(Cursor::new(outstream))?; 147 | assert_eq!(written_cpx.name(), incpx.name()); 148 | assert_eq!(written_cpx.len(), incpx.len()); 149 | assert_eq!(written_cpx.version(), incpx.version()); 150 | assert_eq!(written_cpx.get_name(0), incpx.get_name(0)); 151 | assert_eq!(written_cpx.get_filename(0), incpx.get_filename(0)); 152 | assert_eq!(written_cpx.by_index_raw(0)?, incpx.by_index_raw(0)?); 153 | Ok(()) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/10 The First Punic War.aoecpn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/10 The First Punic War.aoecpn -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/AIImprovementsTest.aoe2campaign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/AIImprovementsTest.aoe2campaign -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/Armies at War A Combat Showcase.cpn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/Armies at War A Combat Showcase.cpn -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/DER FALL VON SACSAHUAMAN - TEIL I.cpx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/DER FALL VON SACSAHUAMAN - TEIL I.cpx -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/Rise of Egypt Learning Campaign.cpn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/Rise of Egypt Learning Campaign.cpn -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/acam1.aoe2campaign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/acam1.aoe2campaign -------------------------------------------------------------------------------- /crates/genie-cpx/test/campaigns/rcam3.aoe2campaign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-cpx/test/campaigns/rcam3.aoe2campaign -------------------------------------------------------------------------------- /crates/genie-dat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-dat" 3 | version = "0.1.0" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read and write Age of Empires I/II data files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-dat" 10 | documentation = "https://docs.rs/genie-dat" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | 14 | [dependencies] 15 | arrayvec.workspace = true 16 | byteorder.workspace = true 17 | encoding_rs.workspace = true 18 | flate2.workspace = true 19 | genie-support = { version = "^1.0.0", path = "../genie-support" } 20 | jascpal = { version = "^0.1.0", path = "../jascpal" } 21 | thiserror.workspace = true 22 | 23 | [dev-dependencies] 24 | anyhow.workspace = true 25 | -------------------------------------------------------------------------------- /crates/genie-dat/README.md: -------------------------------------------------------------------------------- 1 | # genie-dat 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--dat-blue?style=flat-square&color=blue)](https://docs.rs/genie-dat/) 4 | [![crates.io](https://img.shields.io/crates/v/genie-dat.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-dat) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read and write Age of Empires I/II data files. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-dat/fixtures/age-of-chivalry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-dat/fixtures/age-of-chivalry.dat -------------------------------------------------------------------------------- /crates/genie-dat/fixtures/aoc1.0c.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-dat/fixtures/aoc1.0c.dat -------------------------------------------------------------------------------- /crates/genie-dat/fixtures/aok.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-dat/fixtures/aok.dat -------------------------------------------------------------------------------- /crates/genie-dat/fixtures/hd.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-dat/fixtures/hd.dat -------------------------------------------------------------------------------- /crates/genie-dat/src/civ.rs: -------------------------------------------------------------------------------- 1 | //! Types related to civilizations. 2 | 3 | use crate::unit_type::{UnitType, UnitTypeID}; 4 | use crate::GameVersion; 5 | use arrayvec::ArrayString; 6 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 7 | use encoding_rs::WINDOWS_1252; 8 | use genie_support::{fallible_try_from, infallible_try_into, read_opt_u16}; 9 | use std::convert::TryInto; 10 | use std::io::{Read, Result, Write}; 11 | 12 | /// An ID identifying a civilization 13 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 14 | pub struct CivilizationID(u8); 15 | 16 | impl From for CivilizationID { 17 | fn from(n: u8) -> Self { 18 | CivilizationID(n) 19 | } 20 | } 21 | 22 | impl From for u8 { 23 | fn from(n: CivilizationID) -> Self { 24 | n.0 25 | } 26 | } 27 | 28 | impl From for u16 { 29 | fn from(n: CivilizationID) -> Self { 30 | n.0.into() 31 | } 32 | } 33 | 34 | impl From for u32 { 35 | fn from(n: CivilizationID) -> Self { 36 | n.0.into() 37 | } 38 | } 39 | 40 | impl From for usize { 41 | fn from(n: CivilizationID) -> Self { 42 | n.0.into() 43 | } 44 | } 45 | 46 | infallible_try_into!(CivilizationID, i16); 47 | infallible_try_into!(CivilizationID, i32); 48 | fallible_try_from!(CivilizationID, i8); 49 | fallible_try_from!(CivilizationID, i16); 50 | fallible_try_from!(CivilizationID, u16); 51 | fallible_try_from!(CivilizationID, i32); 52 | fallible_try_from!(CivilizationID, u32); 53 | 54 | type CivName = ArrayString<20>; 55 | 56 | /// Information about a civilization. 57 | #[derive(Debug, Default, Clone)] 58 | pub struct Civilization { 59 | name: CivName, 60 | attributes: Vec, 61 | civ_effect: u16, 62 | bonus_effect: Option, 63 | culture: u8, 64 | unit_types: Vec>, 65 | } 66 | 67 | impl Civilization { 68 | /// Get the name of this civilization. 69 | pub fn name(&self) -> &str { 70 | self.name.as_str() 71 | } 72 | 73 | /// Read civilization data from an input stream. 74 | pub fn read_from(mut input: impl Read, version: GameVersion) -> Result { 75 | let mut civ = Self::default(); 76 | let mut bytes = [0; 20]; 77 | input.read_exact(&mut bytes)?; 78 | let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; 79 | let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); 80 | civ.name = CivName::from(&name).unwrap(); 81 | let num_attributes = input.read_u16::()?; 82 | civ.civ_effect = input.read_u16::()?; 83 | civ.bonus_effect = read_opt_u16(&mut input)?; 84 | 85 | civ.attributes.reserve(num_attributes as usize); 86 | for _ in 0..num_attributes { 87 | civ.attributes.push(input.read_f32::()?); 88 | } 89 | 90 | civ.culture = input.read_u8()?; 91 | 92 | let num_unit_types = input.read_u16::()?; 93 | let have_unit_types = { 94 | let mut list = vec![]; 95 | for _ in 0..num_unit_types { 96 | list.push(input.read_u32::()? != 0); 97 | } 98 | list 99 | }; 100 | for do_read in have_unit_types { 101 | if !do_read { 102 | civ.unit_types.push(None); 103 | continue; 104 | } 105 | civ.unit_types 106 | .push(Some(UnitType::read_from(&mut input, version.as_f32())?)); 107 | } 108 | 109 | Ok(civ) 110 | } 111 | 112 | /// Write civilization data to an output stream. 113 | pub fn write_to(&self, mut output: impl Write, version: GameVersion) -> Result<()> { 114 | let mut name = [0; 20]; 115 | (name[..self.name.len()]).copy_from_slice(self.name.as_bytes()); 116 | output.write_all(&name)?; 117 | output.write_u16::(self.attributes.len().try_into().unwrap())?; 118 | output.write_u16::(self.civ_effect)?; 119 | output.write_u16::(self.bonus_effect.unwrap_or(0xFFFF))?; 120 | for v in self.attributes.iter() { 121 | output.write_f32::(*v)?; 122 | } 123 | output.write_u8(self.culture)?; 124 | 125 | output.write_u16::(self.unit_types.len().try_into().unwrap())?; 126 | for opt in &self.unit_types { 127 | output.write_u32::(match opt { 128 | Some(_) => 1, 129 | None => 0, 130 | })?; 131 | } 132 | for unit_type in self.unit_types.iter().flatten() { 133 | unit_type.write_to(&mut output, version.as_f32())?; 134 | } 135 | Ok(()) 136 | } 137 | 138 | /// Get a unit type by its ID. 139 | pub fn get_unit_type(&self, id: impl Into) -> Option<&UnitType> { 140 | let id: UnitTypeID = id.into(); 141 | self.unit_types 142 | .get(usize::from(id)) 143 | .and_then(Option::as_ref) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/genie-dat/src/color_table.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 2 | pub use jascpal::PaletteIndex; 3 | use std::convert::TryInto; 4 | use std::io::{Read, Result, Write}; 5 | 6 | /// Player colour data. 7 | #[derive(Debug, Clone)] 8 | pub struct ColorTable { 9 | pub id: i32, 10 | /// Base palette index for this player colour. 11 | pub base: PaletteIndex, 12 | /// The palette index to use for unit outlines when they are obscured by buildings or trees. 13 | pub unit_outline_color: PaletteIndex, 14 | pub unit_selection_colors: (PaletteIndex, PaletteIndex), 15 | /// Palette indices for this colour on the minimap. 16 | pub minimap_colors: (PaletteIndex, PaletteIndex, PaletteIndex), 17 | /// Color table to use for this player colour in the in-game statistics in the bottom right. 18 | pub statistics_text_color: i32, 19 | } 20 | 21 | impl ColorTable { 22 | /// Read a colour table from an input stream. 23 | pub fn read_from(input: &mut R) -> Result { 24 | let id = input.read_i32::()?; 25 | let base = input.read_i32::()?.try_into().unwrap(); 26 | let unit_outline_color = input.read_i32::()?.try_into().unwrap(); 27 | let unit_selection_colors = ( 28 | input.read_i32::()?.try_into().unwrap(), 29 | input.read_i32::()?.try_into().unwrap(), 30 | ); 31 | let minimap_colors = ( 32 | input.read_i32::()?.try_into().unwrap(), 33 | input.read_i32::()?.try_into().unwrap(), 34 | input.read_i32::()?.try_into().unwrap(), 35 | ); 36 | let statistics_text_color = input.read_i32::()?; 37 | 38 | Ok(Self { 39 | id, 40 | base, 41 | unit_outline_color, 42 | unit_selection_colors, 43 | minimap_colors, 44 | statistics_text_color, 45 | }) 46 | } 47 | 48 | /// Write this colour table to an output stream. 49 | pub fn write_to(&self, output: &mut W) -> Result<()> { 50 | output.write_i32::(self.id)?; 51 | output.write_i32::(self.base.try_into().unwrap())?; 52 | output.write_i32::(self.unit_outline_color.try_into().unwrap())?; 53 | output.write_i32::(self.unit_selection_colors.0.try_into().unwrap())?; 54 | output.write_i32::(self.unit_selection_colors.1.try_into().unwrap())?; 55 | output.write_i32::(self.minimap_colors.0.try_into().unwrap())?; 56 | output.write_i32::(self.minimap_colors.1.try_into().unwrap())?; 57 | output.write_i32::(self.minimap_colors.2.try_into().unwrap())?; 58 | output.write_i32::(self.statistics_text_color)?; 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/genie-dat/src/sound.rs: -------------------------------------------------------------------------------- 1 | use crate::FileVersion; 2 | use arrayvec::ArrayString; 3 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 4 | use genie_support::{fallible_try_from, fallible_try_into, infallible_try_into}; 5 | use std::convert::TryInto; 6 | use std::io::{Read, Result, Write}; 7 | 8 | /// An ID identifying a sound. 9 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 10 | pub struct SoundID(u16); 11 | impl From for SoundID { 12 | fn from(n: u16) -> Self { 13 | SoundID(n) 14 | } 15 | } 16 | 17 | impl From for u16 { 18 | fn from(n: SoundID) -> Self { 19 | n.0 20 | } 21 | } 22 | 23 | impl From for usize { 24 | fn from(n: SoundID) -> Self { 25 | n.0.into() 26 | } 27 | } 28 | 29 | fallible_try_into!(SoundID, i16); 30 | infallible_try_into!(SoundID, i32); 31 | infallible_try_into!(SoundID, u32); 32 | fallible_try_from!(SoundID, i16); 33 | fallible_try_from!(SoundID, i32); 34 | fallible_try_from!(SoundID, u32); 35 | 36 | /// A "conceptual" sound, consisting of one or a group of sound files. 37 | /// 38 | /// Items can be picked depending on the player's civilization, and depending on the probabilities 39 | /// for each file. 40 | #[derive(Debug, Default, Clone)] 41 | pub struct Sound { 42 | /// Unique ID for this sound. 43 | pub id: SoundID, 44 | /// TODO document. 45 | pub play_delay: i16, 46 | /// TODO document. 47 | pub cache_time: i32, 48 | /// List of sound files in this sound. 49 | pub items: Vec, 50 | } 51 | 52 | /// A single sound file. 53 | #[derive(Debug, Default, Clone)] 54 | pub struct SoundItem { 55 | /// Internal file name for this sound file. 56 | pub filename: ArrayString<13>, 57 | /// DRS file ID for this sound file. 58 | pub resource_id: i32, 59 | /// The probability out of 100% that this file will be used for any given playback. 60 | pub probability: i16, 61 | /// Use this file for this civilization ID only. 62 | pub civilization: Option, 63 | /// File icon set (TODO what does this do?) 64 | pub icon_set: Option, 65 | } 66 | 67 | impl SoundItem { 68 | /// Read this sound item from an input stream. 69 | pub fn read_from(input: &mut R, _version: FileVersion) -> Result { 70 | let mut item = SoundItem::default(); 71 | // TODO actually use this 72 | let mut filename = [0u8; 13]; 73 | input.read_exact(&mut filename)?; 74 | item.resource_id = input.read_i32::()?; 75 | item.probability = input.read_i16::()?; 76 | // AoK only 77 | item.civilization = Some(input.read_i16::()?); 78 | item.icon_set = Some(input.read_i16::()?); 79 | 80 | Ok(item) 81 | } 82 | 83 | /// Write this sound item to an input stream. 84 | pub fn write_to(&self, output: &mut W, _version: FileVersion) -> Result<()> { 85 | output.write_all(&[0; 13])?; 86 | output.write_i32::(self.resource_id)?; 87 | output.write_i16::(self.probability)?; 88 | // AoK only, must both be set 89 | assert!(self.civilization.is_some()); 90 | assert!(self.icon_set.is_some()); 91 | output.write_i16::(self.civilization.unwrap())?; 92 | output.write_i16::(self.icon_set.unwrap())?; 93 | Ok(()) 94 | } 95 | } 96 | 97 | impl Sound { 98 | /// Read this sound from an input stream. 99 | pub fn read_from(input: &mut R, version: FileVersion) -> Result { 100 | let mut sound = Sound { 101 | id: input.read_u16::()?.into(), 102 | play_delay: input.read_i16::()?, 103 | ..Default::default() 104 | }; 105 | let num_items = input.read_u16::()?; 106 | sound.cache_time = input.read_i32::()?; 107 | if version.is_de2() { 108 | let _total_probability = input.read_u16::()?; 109 | } 110 | for _ in 0..num_items { 111 | sound.items.push(SoundItem::read_from(input, version)?); 112 | } 113 | Ok(sound) 114 | } 115 | 116 | /// Write this sound to an input stream. 117 | pub fn write_to(&self, output: &mut W, version: FileVersion) -> Result<()> { 118 | output.write_u16::(self.id.into())?; 119 | output.write_i16::(self.play_delay)?; 120 | output.write_u16::(self.len().try_into().unwrap())?; 121 | output.write_i32::(self.cache_time)?; 122 | for item in &self.items { 123 | item.write_to(output, version)?; 124 | } 125 | Ok(()) 126 | } 127 | 128 | /// Get the number of sound files that are part of this "conceptual" sound. 129 | pub fn len(&self) -> usize { 130 | self.items.len() 131 | } 132 | 133 | /// Returns true if there are no sound files. 134 | pub fn is_empty(&self) -> bool { 135 | self.items.is_empty() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /crates/genie-dat/src/task.rs: -------------------------------------------------------------------------------- 1 | use crate::sound::SoundID; 2 | use crate::sprite::SpriteID; 3 | use crate::unit_type::UnitTypeID; 4 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 5 | use genie_support::read_opt_u16; 6 | use std::convert::TryInto; 7 | use std::io::{Read, Result, Write}; 8 | use std::ops::Deref; 9 | 10 | #[derive(Debug, Default, Clone)] 11 | pub struct TaskList(Vec); 12 | 13 | #[derive(Debug, Default, Clone)] 14 | pub struct Task { 15 | id: u16, 16 | is_default: bool, 17 | action_type: u16, 18 | object_class: i16, 19 | object_id: Option, 20 | terrain_id: i16, 21 | attribute_types: (i16, i16, i16, i16), 22 | work_values: (f32, f32), 23 | work_range: f32, 24 | auto_search_targets: bool, 25 | search_wait_time: f32, 26 | enable_targeting: bool, 27 | combat_level: u8, 28 | work_flags: (u16, u16), 29 | owner_type: u8, 30 | holding_attribute: u8, 31 | state_building: u8, 32 | move_sprite: Option, 33 | work_sprite: Option, 34 | work_sprite2: Option, 35 | carry_sprite: Option, 36 | work_sound: Option, 37 | work_sound2: Option, 38 | } 39 | 40 | impl Deref for TaskList { 41 | type Target = Vec; 42 | fn deref(&self) -> &Self::Target { 43 | &self.0 44 | } 45 | } 46 | 47 | impl TaskList { 48 | pub fn read_from(mut input: impl Read) -> Result { 49 | let num_tasks = input.read_u16::()?; 50 | let mut tasks = vec![]; 51 | for _ in 0..num_tasks { 52 | let task_type = input.read_u16::()?; 53 | assert_eq!(task_type, 1); 54 | tasks.push(Task::read_from(&mut input)?); 55 | } 56 | 57 | Ok(Self(tasks)) 58 | } 59 | 60 | pub fn write_to(&self, output: &mut W) -> Result<()> { 61 | output.write_u16::(self.len().try_into().unwrap())?; 62 | for task in self.iter() { 63 | output.write_u16::(1)?; 64 | task.write_to(output)?; 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl Task { 71 | pub fn read_from(mut input: impl Read) -> Result { 72 | Ok(Task { 73 | id: input.read_u16::()?, 74 | is_default: input.read_u8()? != 0, 75 | action_type: input.read_u16::()?, 76 | object_class: input.read_i16::()?, 77 | object_id: read_opt_u16(&mut input)?, 78 | terrain_id: input.read_i16::()?, 79 | attribute_types: ( 80 | input.read_i16::()?, 81 | input.read_i16::()?, 82 | input.read_i16::()?, 83 | input.read_i16::()?, 84 | ), 85 | work_values: (input.read_f32::()?, input.read_f32::()?), 86 | work_range: input.read_f32::()?, 87 | auto_search_targets: input.read_u8()? != 0, 88 | search_wait_time: input.read_f32::()?, 89 | enable_targeting: input.read_u8()? != 0, 90 | combat_level: input.read_u8()?, 91 | work_flags: (input.read_u16::()?, input.read_u16::()?), 92 | owner_type: input.read_u8()?, 93 | holding_attribute: input.read_u8()?, 94 | state_building: input.read_u8()?, 95 | move_sprite: read_opt_u16(&mut input)?, 96 | work_sprite: read_opt_u16(&mut input)?, 97 | work_sprite2: read_opt_u16(&mut input)?, 98 | carry_sprite: read_opt_u16(&mut input)?, 99 | work_sound: read_opt_u16(&mut input)?, 100 | work_sound2: read_opt_u16(&mut input)?, 101 | }) 102 | } 103 | 104 | pub fn write_to(&self, output: &mut W) -> Result<()> { 105 | output.write_u16::(self.id)?; 106 | output.write_u8(u8::from(self.is_default))?; 107 | output.write_u16::(self.action_type)?; 108 | output.write_i16::(self.object_class)?; 109 | output.write_i16::( 110 | self.object_id 111 | .map(|id| id.try_into().unwrap()) 112 | .unwrap_or(-1), 113 | )?; 114 | output.write_i16::(self.terrain_id)?; 115 | output.write_i16::(self.attribute_types.0)?; 116 | output.write_i16::(self.attribute_types.1)?; 117 | output.write_i16::(self.attribute_types.2)?; 118 | output.write_i16::(self.attribute_types.3)?; 119 | output.write_f32::(self.work_values.0)?; 120 | output.write_f32::(self.work_values.1)?; 121 | output.write_f32::(self.work_range)?; 122 | output.write_u8(u8::from(self.auto_search_targets))?; 123 | output.write_f32::(self.search_wait_time)?; 124 | output.write_u8(u8::from(self.enable_targeting))?; 125 | output.write_u8(self.combat_level)?; 126 | output.write_u16::(self.work_flags.0)?; 127 | output.write_u16::(self.work_flags.1)?; 128 | output.write_u8(self.owner_type)?; 129 | output.write_u8(self.holding_attribute)?; 130 | output.write_u8(self.state_building)?; 131 | output.write_i16::( 132 | self.move_sprite 133 | .map(|id| id.try_into().unwrap()) 134 | .unwrap_or(-1), 135 | )?; 136 | output.write_i16::( 137 | self.work_sprite 138 | .map(|id| id.try_into().unwrap()) 139 | .unwrap_or(-1), 140 | )?; 141 | output.write_i16::( 142 | self.work_sprite2 143 | .map(|id| id.try_into().unwrap()) 144 | .unwrap_or(-1), 145 | )?; 146 | output.write_i16::( 147 | self.carry_sprite 148 | .map(|id| id.try_into().unwrap()) 149 | .unwrap_or(-1), 150 | )?; 151 | output.write_i16::( 152 | self.work_sound 153 | .map(|id| id.try_into().unwrap()) 154 | .unwrap_or(-1), 155 | )?; 156 | output.write_i16::( 157 | self.work_sound2 158 | .map(|id| id.try_into().unwrap()) 159 | .unwrap_or(-1), 160 | )?; 161 | Ok(()) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /crates/genie-dat/src/tech.rs: -------------------------------------------------------------------------------- 1 | use crate::civ::CivilizationID; 2 | use crate::unit_type::UnitTypeID; 3 | use arrayvec::{ArrayString, ArrayVec}; 4 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 5 | use encoding_rs::WINDOWS_1252; 6 | pub use genie_support::TechID; 7 | use genie_support::{read_opt_u16, read_opt_u32, MapInto, StringKey}; 8 | use std::io::{Read, Result, Write}; 9 | 10 | /// An effect command specifies an attribute change when a tech effect is triggered. 11 | #[derive(Debug, Default, Clone)] 12 | pub struct EffectCommand { 13 | /// The command. 14 | pub command_type: u8, 15 | /// Command-dependent parameters. 16 | pub params: (i16, i16, i16, f32), 17 | } 18 | 19 | type TechEffectName = ArrayString<31>; 20 | 21 | /// A tech effect is a group of attribute changes that are applied when the effect is triggered. 22 | #[derive(Debug, Default, Clone)] 23 | pub struct TechEffect { 24 | /// Name for the effect. 25 | name: TechEffectName, 26 | /// Attribute commands to execute when this effect is triggered. 27 | pub commands: Vec, 28 | } 29 | 30 | #[derive(Debug, Default, Clone, Copy)] 31 | pub struct TechEffectRef { 32 | pub effect_type: u16, 33 | pub amount: u16, 34 | pub enabled: bool, 35 | } 36 | 37 | #[derive(Debug, Default, Clone)] 38 | pub struct Tech { 39 | required_techs: ArrayVec, 40 | effects: ArrayVec, 41 | civilization_id: Option, 42 | full_tech_mode: u16, 43 | location: Option, 44 | language_dll_name: Option, 45 | language_dll_description: Option, 46 | time: u16, 47 | time2: u16, 48 | type_: u16, 49 | icon_id: Option, 50 | button_id: u8, 51 | language_dll_help: Option, 52 | help_page_id: u32, 53 | hotkey: Option, 54 | name: String, 55 | } 56 | 57 | impl EffectCommand { 58 | pub fn read_from(input: &mut R) -> Result { 59 | let command_type = input.read_u8()?; 60 | let params = ( 61 | input.read_i16::()?, 62 | input.read_i16::()?, 63 | input.read_i16::()?, 64 | input.read_f32::()?, 65 | ); 66 | Ok(EffectCommand { 67 | command_type, 68 | params, 69 | }) 70 | } 71 | 72 | pub fn write_to(&self, output: &mut W) -> Result<()> { 73 | output.write_u8(self.command_type)?; 74 | output.write_i16::(self.params.0)?; 75 | output.write_i16::(self.params.1)?; 76 | output.write_i16::(self.params.2)?; 77 | output.write_f32::(self.params.3)?; 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl TechEffect { 83 | /// Get the name of this effect. 84 | pub fn name(&self) -> &str { 85 | self.name.as_str() 86 | } 87 | 88 | /// Set the name of this effect. 89 | /// 90 | /// # Panics 91 | /// This function panics if `name` requires more than 31 bytes of storage. 92 | pub fn set_name(&mut self, name: &str) { 93 | self.name = TechEffectName::from(name).unwrap(); 94 | } 95 | 96 | pub fn read_from(input: &mut R) -> Result { 97 | let mut effect = Self::default(); 98 | let mut bytes = [0; 31]; 99 | input.read_exact(&mut bytes)?; 100 | let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; 101 | let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); 102 | effect.name = TechEffectName::from(&name).unwrap(); 103 | 104 | let num_commands = input.read_u16::()?; 105 | for _ in 0..num_commands { 106 | effect.commands.push(EffectCommand::read_from(input)?); 107 | } 108 | 109 | Ok(effect) 110 | } 111 | 112 | pub fn write_to(&self, output: &mut W) -> Result<()> { 113 | let mut buffer = [0; 31]; 114 | buffer[..self.name.len()].copy_from_slice(self.name.as_bytes()); 115 | output.write_all(&buffer)?; 116 | 117 | output.write_u16::(self.commands.len() as u16)?; 118 | for effect in &self.commands { 119 | effect.write_to(output)?; 120 | } 121 | Ok(()) 122 | } 123 | } 124 | 125 | impl TechEffectRef { 126 | pub fn read_from(input: &mut R) -> Result { 127 | Ok(Self { 128 | effect_type: input.read_u16::()?, 129 | amount: input.read_u16::()?, 130 | enabled: input.read_u8()? != 0, 131 | }) 132 | } 133 | 134 | pub fn write_to(self, output: &mut W) -> Result<()> { 135 | output.write_u16::(self.effect_type)?; 136 | output.write_u16::(self.amount)?; 137 | output.write_u8(u8::from(self.enabled))?; 138 | Ok(()) 139 | } 140 | } 141 | 142 | impl Tech { 143 | /// Get the name of this tech. 144 | pub fn name(&self) -> &str { 145 | self.name.as_str() 146 | } 147 | 148 | pub fn read_from(mut input: impl Read) -> Result { 149 | let mut tech = Self::default(); 150 | for _ in 0..6 { 151 | // 4 on some versions 152 | if let Some(tech_id) = read_opt_u16(&mut input)? { 153 | tech.required_techs.push(tech_id); 154 | } 155 | } 156 | for _ in 0..3 { 157 | let effect = TechEffectRef::read_from(&mut input)?; 158 | if effect.effect_type != 0xFFFF { 159 | tech.effects.push(effect); 160 | } 161 | } 162 | let _num_required_techs = input.read_u16::()?; 163 | tech.civilization_id = read_opt_u16(&mut input)?; 164 | tech.full_tech_mode = input.read_u16::()?; 165 | tech.location = read_opt_u16(&mut input)?; 166 | tech.language_dll_name = read_opt_u16(&mut input)?; 167 | tech.language_dll_description = read_opt_u16(&mut input)?; 168 | tech.time = input.read_u16::()?; 169 | tech.time2 = input.read_u16::()?; 170 | tech.type_ = input.read_u16::()?; 171 | tech.icon_id = read_opt_u16(&mut input)?; 172 | tech.button_id = input.read_u8()?; 173 | tech.language_dll_help = read_opt_u32(&mut input)?; 174 | tech.help_page_id = input.read_u32::()?; 175 | tech.hotkey = read_opt_u32(&mut input)?; 176 | tech.name = { 177 | let name_len = input.read_u16::()?; 178 | let mut bytes = vec![0; name_len as usize]; 179 | input.read_exact(&mut bytes)?; 180 | let bytes = &bytes[..bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len())]; 181 | let (name, _encoding, _failed) = WINDOWS_1252.decode(bytes); 182 | name.to_string() 183 | }; 184 | Ok(tech) 185 | } 186 | 187 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 188 | for i in 0..6 { 189 | match self.required_techs.get(i) { 190 | Some(&id) => output.write_u16::(id.into())?, 191 | None => output.write_i16::(-1)?, 192 | } 193 | } 194 | for i in 0..3 { 195 | match self.effects.get(i) { 196 | Some(effect) => effect.write_to(&mut output)?, 197 | None => TechEffectRef { 198 | effect_type: 0xFFFF, 199 | amount: 0, 200 | enabled: false, 201 | } 202 | .write_to(&mut output)?, 203 | } 204 | } 205 | output.write_u16::(self.required_techs.len() as u16)?; 206 | output.write_u16::(self.civilization_id.map_into().unwrap_or(0xFFFF))?; 207 | output.write_u16::(self.full_tech_mode)?; 208 | output.write_u16::(self.location.map_into().unwrap_or(0xFFFF))?; 209 | output.write_u16::(match self.language_dll_name { 210 | Some(StringKey::Num(id)) => id as u16, 211 | Some(_) => unreachable!("cannot use named string keys in dat files"), 212 | None => 0xFFFF, 213 | })?; 214 | output.write_u16::(match self.language_dll_description { 215 | Some(StringKey::Num(id)) => id as u16, 216 | Some(_) => unreachable!("cannot use named string keys in dat files"), 217 | None => 0xFFFF, 218 | })?; 219 | output.write_u16::(self.time)?; 220 | output.write_u16::(self.time2)?; 221 | output.write_u16::(self.type_)?; 222 | output.write_u16::(self.icon_id.map_into().unwrap_or(0xFFFF))?; 223 | output.write_u8(self.button_id)?; 224 | output.write_u32::(match self.language_dll_help { 225 | Some(StringKey::Num(id)) => id, 226 | Some(_) => unreachable!("cannot use named string keys in dat files"), 227 | None => 0xFFFF_FFFF, 228 | })?; 229 | output.write_u32::(self.help_page_id)?; 230 | output.write_u32::(self.hotkey.map_into().unwrap_or(0xFFFF_FFFF))?; 231 | let (encoded, _encoding, _failed) = WINDOWS_1252.encode(&self.name); 232 | output.write_u16::(encoded.len() as u16)?; 233 | output.write_all(encoded.as_ref())?; 234 | Ok(()) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /crates/genie-drs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-drs" 3 | version = "0.2.1" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license = "GPL-3.0-or-later" 8 | license-file = "./LICENSE.md" 9 | description = "Read .drs archive files from the Genie Engine, used in Age of Empires 1/2 and SWGB" 10 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-drs" 11 | documentation = "https://docs.rs/genie-drs" 12 | repository.workspace = true 13 | readme = "./README.md" 14 | exclude = ["*.drs"] 15 | 16 | [dependencies] 17 | byteorder.workspace = true 18 | sorted-vec = "0.8.0" 19 | thiserror.workspace = true 20 | 21 | [dev-dependencies] 22 | anyhow.workspace = true 23 | -------------------------------------------------------------------------------- /crates/genie-drs/README.md: -------------------------------------------------------------------------------- 1 | # genie-drs 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--drs-blue?style=flat-square&color=blue)](https://docs.rs/genie-drs/) 4 | [![crates.io](https://img.shields.io/crates/v/genie-drs.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-drs) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read .drs archive files from the Genie Engine, used in Age of Empires 1/2 and SWGB 9 | 10 | ## About DRS 11 | 12 | .drs is the resource archive file format for the Genie Engine, used by Age of Empires 1/2 and 13 | Star Wars: Galactic Battlegrounds. .drs files contain tables, each of which contain resources 14 | of a single type. Resources are identified by a numeric identifier. 15 | 16 | This crate only supports reading files right now. 17 | 18 | ## Install 19 | 20 | Add to Cargo.toml: 21 | 22 | ```toml 23 | [dependencies] 24 | genie-drs = "^0.2.1" 25 | ``` 26 | 27 | ## Example 28 | 29 | ```rust 30 | use std::fs::File; 31 | use genie_drs::DRSReader; 32 | 33 | let mut file = File::open("test.drs")?; 34 | let drs = DRSReader::new(&mut file)?; 35 | 36 | for table in drs.tables() { 37 | for resource in table.resources() { 38 | let content = drs.read_resource(&mut file, table.resource_type, resource.id)?; 39 | println!("{}: {:?}", resource.id, std::str::from_utf8(&content)?); 40 | } 41 | } 42 | ``` 43 | 44 | ## Wishlist 45 | 46 | - An API that doesn't require passing in the file handle manually 47 | - A [file mapping](https://en.wikipedia.org/wiki/Memory-mapped_file) counterpart for the `read_resource` API, using [memmap](https://crates.io/crates/memmap) probably. 48 | 49 | ## License 50 | 51 | [GPL-3.0 or later](./LICENSE.md) 52 | -------------------------------------------------------------------------------- /crates/genie-drs/src/read.rs: -------------------------------------------------------------------------------- 1 | use super::{DRSHeader, DRSResource, DRSTable, DRSTableIterator, ResourceType}; 2 | use std::io::{Error, ErrorKind, Read, Seek, SeekFrom}; 3 | 4 | /// A DRS archive reader. 5 | #[derive(Debug)] 6 | pub struct DRSReader { 7 | header: Option, 8 | tables: Vec, 9 | } 10 | 11 | impl DRSReader { 12 | /// Create a new DRS archive reader for the given handle. 13 | /// The handle must be `Read`able and `Seek`able. 14 | pub fn new(handle: &mut R) -> Result 15 | where 16 | R: Read + Seek, 17 | { 18 | let mut drs = DRSReader { 19 | header: None, 20 | tables: vec![], 21 | }; 22 | drs.read_header(handle)?; 23 | drs.read_tables(handle)?; 24 | drs.read_dictionary(handle)?; 25 | Ok(drs) 26 | } 27 | 28 | /// Read the DRS archive header. 29 | fn read_header(&mut self, handle: &mut R) -> Result<(), Error> { 30 | self.header = Some(DRSHeader::from(handle)?); 31 | Ok(()) 32 | } 33 | 34 | /// Read the list of tables. 35 | fn read_tables(&mut self, handle: &mut R) -> Result<(), Error> { 36 | match self.header { 37 | Some(ref header) => { 38 | for _ in 0..header.num_resource_types { 39 | let table = DRSTable::from(handle)?; 40 | self.tables.push(table); 41 | } 42 | } 43 | None => panic!("must read header first"), 44 | }; 45 | Ok(()) 46 | } 47 | 48 | /// Read the list of resources. 49 | fn read_dictionary(&mut self, handle: &mut R) -> Result<(), Error> { 50 | for table in &mut self.tables { 51 | table.read_resources(handle)?; 52 | } 53 | Ok(()) 54 | } 55 | 56 | /// Get the table for the given resource type. 57 | pub fn get_table(&self, resource_type: ResourceType) -> Option<&DRSTable> { 58 | self.tables 59 | .iter() 60 | .find(|table| table.resource_type == resource_type) 61 | } 62 | 63 | /// Get a resource of a given type and ID. 64 | pub fn get_resource(&self, resource_type: ResourceType, id: u32) -> Option<&DRSResource> { 65 | self.get_table(resource_type) 66 | .and_then(|table| table.get_resource(id)) 67 | } 68 | 69 | /// Get the type of a resource with the given ID. 70 | pub fn get_resource_type(&self, id: u32) -> Option { 71 | self.tables 72 | .iter() 73 | .find(|table| table.get_resource(id).is_some()) 74 | .map(|table| table.resource_type) 75 | } 76 | 77 | /// Get a `Read`er for the given resource. 78 | /// 79 | /// It shares the file handle that is given, so make sure to use the return value before 80 | /// calling this method again. 81 | pub fn get_resource_reader( 82 | &self, 83 | mut handle: R, 84 | resource_type: ResourceType, 85 | id: u32, 86 | ) -> Result { 87 | let &DRSResource { size, offset, .. } = self 88 | .get_resource(resource_type, id) 89 | .ok_or_else(|| Error::new(ErrorKind::NotFound, "Resource not found in this archive"))?; 90 | 91 | handle.seek(SeekFrom::Start(u64::from(offset)))?; 92 | 93 | Ok(handle.take(u64::from(size))) 94 | } 95 | 96 | /// Read a file from the DRS archive. 97 | pub fn read_resource( 98 | &self, 99 | handle: &mut R, 100 | resource_type: ResourceType, 101 | id: u32, 102 | ) -> Result, Error> { 103 | let mut buf = vec![]; 104 | 105 | self.get_resource_reader(handle, resource_type, id)? 106 | .read_to_end(&mut buf)?; 107 | 108 | Ok(buf.into_boxed_slice()) 109 | } 110 | 111 | /// Iterate over the tables in this DRS archive. 112 | #[inline] 113 | pub fn tables(&self) -> DRSTableIterator<'_> { 114 | self.tables.iter() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crates/genie-drs/test.drs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-drs/test.drs -------------------------------------------------------------------------------- /crates/genie-hki/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-hki" 3 | version = "0.2.1" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read Age of Empires I/II hotkey files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-hki" 10 | documentation = "https://docs.rs/genie-hki" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | exclude = ["test/files"] 14 | 15 | [dependencies] 16 | byteorder.workspace = true 17 | flate2.workspace = true 18 | genie-lang = { version = "^0.2.0", path = "../genie-lang" } 19 | thiserror.workspace = true 20 | 21 | [dev-dependencies] 22 | anyhow.workspace = true 23 | -------------------------------------------------------------------------------- /crates/genie-hki/README.md: -------------------------------------------------------------------------------- 1 | # genie-hki 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--hki-blue?style=flat-square&color=blue)](https://docs.rs/genie-hki/) 4 | [![crates.io](https://img.shields.io/crates/v/genie-hki.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-hki) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read Age of Empires 2 hotkey files. 9 | 10 | ## Hotkeys Descriptions in Language Files 11 | 12 | Hotkeys have a keycode that the game translates into a string for displaying in 13 | the hotkey menu. 14 | Some of these strings are contained in the language file. 15 | Other keys are turned into strings using the [Windows API `GetKeyNameTextA`](https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getkeynametexta). 16 | 17 | The following list summarizes the strings that are displayed for each keycode in the hotkey menu for HD. 18 | Unlisted keycodes are blank or whitespace in the hotkey menu. 19 | 20 | * 0 -> ??? 21 | * 3 -> Scroll Lock 22 | * 8 -> Backspace 23 | * 9 -> Tab 24 | * 12 -> Num 5 25 | * 13 -> Enter 26 | * 16 -> Shift 27 | * 17 -> Ctrl 28 | * 18 -> Alt 29 | * 20 -> Caps Lock 30 | * 27 -> Esc 31 | * 32 -> Space 32 | * 33 -> Page Up 33 | * 34 -> Page Down 34 | * 35 -> End 35 | * 36 -> Home 36 | * 37 -> Left 37 | * 38 -> Up 38 | * 39 -> Right 39 | * 40 -> Down 40 | * 44 -> Sys Req 41 | * 45 -> Insert 42 | * 46 -> Delete 43 | * 47 -> ?UnknownKey? 44 | * 48 -> 0 45 | * 49 -> 1 46 | * ... 47 | * 57 -> 9 48 | * 65 -> A 49 | * 66 -> B 50 | * ... 51 | * 90 -> Z 52 | * 91, 92, 93, 95 -> ?UnknownKey? 53 | * 96 -> Num 0 54 | * 97 -> Num 1 55 | * ... (including another Num 5) 56 | * 105 -> Num 9 57 | * 106 -> Num * 58 | * 107 -> Num + 59 | * 109 -> Num - 60 | * 110 -> Num Del 61 | * 111 -> Num / 62 | * 112 -> F1 63 | * ... 64 | * 120 -> F9 (Note 121 is blank, not F10) 65 | * 122 -> F11 66 | * ... 67 | * 135 -> F24 68 | * 144 -> Pause 69 | * 145 -> Scroll Lock 70 | * 160 -> Shift 71 | * 161 -> Shift 72 | * 162 -> Ctrl 73 | * 163 -> Ctrl 74 | * 164 -> Alt 75 | * 165 -> Alt 76 | * 166 -> ?UnknownKey? 77 | * ... 78 | * 171 -> ?UnknownKey? 79 | * 172 -> M 80 | * 173 -> D 81 | * 174 -> C 82 | * 175 -> B 83 | * 176 -> P 84 | * 177 -> Q 85 | * 178 -> J 86 | * 179 -> G 87 | * 180 -> ?UnknownKey? 88 | * 181 -> ?UnknownKey? 89 | * 182 -> ?UnknownKey? 90 | * 183 -> F 91 | * 186 -> ; 92 | * 187 -> = 93 | * 188 -> , 94 | * 189 -> - 95 | * 190 -> . 96 | * 191 -> / 97 | * 192 -> ` 98 | * 193 -> ?UnknownKey? 99 | * 194 -> F15 (again) 100 | * 220 -> \ 101 | * 221 -> ] 102 | * 222 -> ' 103 | * 226 -> \ (again) 104 | * 233 -> ?UnknownKey? 105 | * 234 -> ?UnknownKey? 106 | * 235 -> ?UnknownKey? 107 | * 237 -> ?UnknownKey? 108 | * 238 -> ?UnknownKey? 109 | * 241 -> ?UnknownKey? 110 | * 243 -> ?UnknownKey? 111 | * 245 -> ?UnknownKey? 112 | * 249 -> ?UnknownKey? 113 | * 251 -> Extra Button 2 114 | * 252 -> Extra Button 1 115 | * 253 -> Middle Button 116 | * 254 -> Wheel Down 117 | * 255 -> Wheel Up 118 | 119 | ## License 120 | 121 | [GPL-3.0](../../LICENSE.md) 122 | -------------------------------------------------------------------------------- /crates/genie-hki/test/files/aoc1.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/aoc1.hki -------------------------------------------------------------------------------- /crates/genie-hki/test/files/aoc2.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/aoc2.hki -------------------------------------------------------------------------------- /crates/genie-hki/test/files/aoc3.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/aoc3.hki -------------------------------------------------------------------------------- /crates/genie-hki/test/files/hd0.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/hd0.hki -------------------------------------------------------------------------------- /crates/genie-hki/test/files/hd1.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/hd1.hki -------------------------------------------------------------------------------- /crates/genie-hki/test/files/wk.hki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-hki/test/files/wk.hki -------------------------------------------------------------------------------- /crates/genie-lang/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-lang" 3 | version = "0.2.1" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read different types of language resource files from Age of Empires 1 and 2." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-lang" 10 | documentation = "https://docs.rs/genie-lang" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | exclude = ["test/dlls"] 14 | 15 | [dependencies] 16 | byteorder.workspace = true 17 | encoding_rs.workspace = true 18 | encoding_rs_io.workspace = true 19 | genie-support = { version = "1.0.0", path = "../genie-support" } 20 | pelite = { version = "0.10.0", default-features = false, features = ["mmap"] } 21 | thiserror.workspace = true 22 | 23 | [dev-dependencies] 24 | anyhow.workspace = true 25 | -------------------------------------------------------------------------------- /crates/genie-lang/README.md: -------------------------------------------------------------------------------- 1 | # genie-lang 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--lang-blue?style=flat-square&color=blue)](https://docs.rs/genie-lang/) 4 | [![crates.io](https://img.shields.io/crates/v/genie-lang.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-lang) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read different types of language resource files from Age of Empires 1 and 2. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-lang/test/dlls/language_x1_p1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-lang/test/dlls/language_x1_p1.dll -------------------------------------------------------------------------------- /crates/genie-rec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-rec" 3 | version = "0.1.1" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read Age of Empires I/II recorded game files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-rec" 10 | documentation = "https://docs.rs/genie-rec" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | 14 | [dependencies] 15 | arrayvec.workspace = true 16 | byteorder.workspace = true 17 | flate2.workspace = true 18 | genie-dat = { version = "0.1.0", path = "../genie-dat" } 19 | genie-scx = { version = "4.0.0", path = "../genie-scx" } 20 | genie-support = { version = "1.0.0", path = "../genie-support", features = [ 21 | "strings", 22 | ] } 23 | thiserror.workspace = true 24 | 25 | [dev-dependencies] 26 | anyhow.workspace = true 27 | -------------------------------------------------------------------------------- /crates/genie-rec/README.md: -------------------------------------------------------------------------------- 1 | # genie-rec 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--rec-blue?style=flat-square&color=blue)](https://docs.rs/genie-rec) 4 | [![crates.io](https://img.shields.io/crates/v/genie-rec.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-rec) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Age of Empires 2 recorded game file reader (incomplete). 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-rec/src/element.rs: -------------------------------------------------------------------------------- 1 | use crate::reader::RecordingHeaderReader; 2 | use std::io::{Read, Write}; 3 | 4 | pub trait OptionalReadableElement: Sized { 5 | fn read_from(input: &mut RecordingHeaderReader) -> crate::Result>; 6 | } 7 | 8 | impl ReadableElement> for T { 9 | fn read_from(input: &mut RecordingHeaderReader) -> crate::Result> { 10 | OptionalReadableElement::read_from(input) 11 | } 12 | } 13 | 14 | pub trait ReadableElement: Sized { 15 | fn read_from(input: &mut RecordingHeaderReader) -> crate::Result; 16 | } 17 | 18 | pub trait ReadableHeaderElement: Sized { 19 | fn read_from(input: &mut RecordingHeaderReader) -> crate::Result; 20 | } 21 | 22 | impl ReadableElement for T { 23 | fn read_from(input: &mut RecordingHeaderReader) -> crate::Result { 24 | ReadableHeaderElement::read_from(input) 25 | } 26 | } 27 | 28 | pub trait WritableElement { 29 | fn write_to(element: &T, output: &mut W) -> crate::Result<()>; 30 | } 31 | 32 | pub trait WritableHeaderElement { 33 | fn write_to(&self, output: &mut W) -> crate::Result<()> { 34 | // we need to use `output` otherwise we'll get warnings that it's not used 35 | // prefixing it would make any traits auto completed also be prefixed with _ and that's annoying 36 | let _ = output; 37 | unimplemented!() 38 | } 39 | } 40 | 41 | impl WritableElement for T { 42 | fn write_to(element: &T, output: &mut W) -> crate::Result<()> { 43 | WritableHeaderElement::write_to(element, output) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/genie-rec/src/map.rs: -------------------------------------------------------------------------------- 1 | use crate::element::{ReadableHeaderElement, WritableHeaderElement}; 2 | use crate::reader::RecordingHeaderReader; 3 | use crate::Result; 4 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 5 | use genie_support::ReadSkipExt; 6 | use std::convert::TryInto; 7 | use std::io::{Read, Write}; 8 | 9 | /// Data about a map tile. 10 | #[derive(Debug, Default, Clone)] 11 | pub struct Tile { 12 | /// The terrain type of this tile. 13 | pub terrain: u8, 14 | /// The elevation level of this tile. 15 | pub elevation: u8, 16 | /// The original terrain type of this tile, if it was later replaced, for example by placing a 17 | /// Farm. UserPatch 1.5 only. 18 | pub original_terrain: Option, 19 | } 20 | 21 | impl ReadableHeaderElement for Tile { 22 | /// Read a tile from an input stream. 23 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 24 | let terrain = input.read_u8()?; 25 | let (terrain, elevation, original_terrain) = if terrain == 0xFF { 26 | (input.read_u8()?, input.read_u8()?, Some(input.read_u8()?)) 27 | } else { 28 | (terrain, input.read_u8()?, None) 29 | }; 30 | Ok(Tile { 31 | terrain, 32 | elevation, 33 | original_terrain, 34 | }) 35 | } 36 | } 37 | 38 | impl WritableHeaderElement for Tile { 39 | /// Write a tile to an output stream. 40 | fn write_to(&self, output: &mut W) -> Result<()> { 41 | match self.original_terrain { 42 | Some(t) => { 43 | output.write_u8(0xFF)?; 44 | output.write_u8(self.terrain)?; 45 | output.write_u8(self.elevation)?; 46 | output.write_u8(t)?; 47 | } 48 | None => { 49 | output.write_u8(self.terrain)?; 50 | output.write_u8(self.elevation)?; 51 | } 52 | } 53 | Ok(()) 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct MapZone { 59 | /// Zone information—this is a Vec<> of a fixed size, and can only be accessed as a slice 60 | /// through the `info()` accessors to prevent modifications to the size. 61 | info: Vec, 62 | tiles: Vec, 63 | pub zone_map: Vec, 64 | pub passability_rules: Vec, 65 | pub num_zones: u32, 66 | } 67 | 68 | impl Default for MapZone { 69 | fn default() -> Self { 70 | Self { 71 | info: vec![0; 255], 72 | tiles: vec![0; 255], 73 | zone_map: Default::default(), 74 | passability_rules: Default::default(), 75 | num_zones: Default::default(), 76 | } 77 | } 78 | } 79 | 80 | impl MapZone { 81 | pub fn info(&self) -> &[i8] { 82 | assert_eq!(self.info.len(), 255); 83 | &self.info 84 | } 85 | 86 | pub fn info_mut(&mut self) -> &mut [i8] { 87 | assert_eq!(self.info.len(), 255); 88 | &mut self.info 89 | } 90 | 91 | pub fn tiles(&self) -> &[i32] { 92 | assert_eq!(self.tiles.len(), 255); 93 | &self.tiles 94 | } 95 | 96 | pub fn tiles_mut(&mut self) -> &mut [i32] { 97 | assert_eq!(self.tiles.len(), 255); 98 | &mut self.tiles 99 | } 100 | } 101 | 102 | impl ReadableHeaderElement for MapZone { 103 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 104 | let mut zone = Self::default(); 105 | input.read_i8_into(&mut zone.info)?; 106 | input.read_i32_into::(&mut zone.tiles)?; 107 | zone.zone_map = vec![0; input.tile_count()]; 108 | input.read_i8_into(&mut zone.zone_map)?; 109 | 110 | let num_rules = input.read_u32::()?; 111 | zone.passability_rules = vec![0.0; num_rules as usize]; 112 | input.read_f32_into::(&mut zone.passability_rules)?; 113 | 114 | zone.num_zones = input.read_u32::()?; 115 | Ok(zone) 116 | } 117 | } 118 | 119 | impl WritableHeaderElement for MapZone { 120 | fn write_to(&self, output: &mut W) -> Result<()> { 121 | for val in &self.info { 122 | output.write_i8(*val)?; 123 | } 124 | for val in &self.tiles { 125 | output.write_i32::(*val)?; 126 | } 127 | for val in &self.zone_map { 128 | output.write_i8(*val)?; 129 | } 130 | output.write_u32::(self.passability_rules.len().try_into().unwrap())?; 131 | for val in &self.passability_rules { 132 | output.write_f32::(*val)?; 133 | } 134 | output.write_u32::(self.num_zones)?; 135 | Ok(()) 136 | } 137 | } 138 | 139 | /// 140 | #[derive(Debug, Default, Clone)] 141 | pub struct VisibilityMap { 142 | /// Width of the visibility map. 143 | pub width: u32, 144 | /// Height of the visibility map. 145 | pub height: u32, 146 | /// Visibility flags for each tile. 147 | pub visibility: Vec, 148 | } 149 | 150 | impl ReadableHeaderElement for VisibilityMap { 151 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 152 | let width = input.read_u32::()?; 153 | let height = input.read_u32::()?; 154 | let mut visibility = vec![0; (width * height).try_into().unwrap()]; 155 | input.read_u32_into::(&mut visibility)?; 156 | Ok(Self { 157 | width, 158 | height, 159 | visibility, 160 | }) 161 | } 162 | } 163 | 164 | impl WritableHeaderElement for VisibilityMap { 165 | fn write_to(&self, output: &mut W) -> Result<()> { 166 | output.write_u32::(self.width)?; 167 | output.write_u32::(self.height)?; 168 | for value in &self.visibility { 169 | output.write_u32::(*value)?; 170 | } 171 | Ok(()) 172 | } 173 | } 174 | 175 | /// Information about the map being played. 176 | #[derive(Debug, Default, Clone)] 177 | pub struct Map { 178 | /// Width of the map. 179 | pub width: u32, 180 | /// Height of the map. 181 | pub height: u32, 182 | /// Map zones. 183 | pub zones: Vec, 184 | /// Is the "All Visible" flag set? 185 | pub all_visible: bool, 186 | /// Is fog of war enabled? 187 | pub fog_of_war: bool, 188 | /// The tiles in this map, containing terrain and elevation data. 189 | pub tiles: Vec, 190 | /// The visibility map, containing line of sight data for each player. 191 | pub visibility: VisibilityMap, 192 | } 193 | 194 | impl ReadableHeaderElement for Map { 195 | /// Read map data from an input stream. 196 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 197 | let mut map = Map { 198 | width: input.read_u32::()?, 199 | height: input.read_u32::()?, 200 | ..Default::default() 201 | }; 202 | 203 | input.set_map_size(map.width, map.height); 204 | let num_zones = input.read_u32::()?; 205 | map.zones = Vec::with_capacity(num_zones.try_into().unwrap()); 206 | for _ in 0..num_zones { 207 | map.zones.push(MapZone::read_from(input)?); 208 | } 209 | map.all_visible = input.read_u8()? != 0; 210 | map.fog_of_war = input.read_u8()? != 0; 211 | map.tiles = Vec::with_capacity((map.width * map.height).try_into().unwrap()); 212 | for _ in 0..(map.width * map.height) { 213 | map.tiles.push(Tile::read_from(input)?); 214 | } 215 | 216 | { 217 | let data_count = input.read_u32::()?; 218 | let _capacity = input.read_u32::()?; 219 | input.skip(u64::from(data_count) * 4)?; 220 | for _ in 0..data_count { 221 | let count = input.read_u32::()?; 222 | input.skip(u64::from(count) * 8)?; 223 | } 224 | }; 225 | 226 | map.visibility = VisibilityMap::read_from(input)?; 227 | 228 | Ok(map) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /crates/genie-rec/src/string_table.rs: -------------------------------------------------------------------------------- 1 | use crate::element::{ReadableHeaderElement, WritableHeaderElement}; 2 | use crate::reader::RecordingHeaderReader; 3 | use crate::Result; 4 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 5 | use std::io::{Read, Write}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct StringTable { 9 | max_strings: u16, 10 | strings: Vec, 11 | } 12 | 13 | impl StringTable { 14 | pub fn new(max_strings: u16) -> Self { 15 | StringTable { 16 | max_strings, 17 | strings: vec![], 18 | } 19 | } 20 | 21 | pub fn max_strings(&self) -> u16 { 22 | self.max_strings 23 | } 24 | 25 | pub fn num_strings(&self) -> u16 { 26 | let len = self.strings.len(); 27 | assert!(len < u16::max_value() as usize); 28 | len as u16 29 | } 30 | 31 | pub fn strings(&self) -> &Vec { 32 | &self.strings 33 | } 34 | } 35 | 36 | impl ReadableHeaderElement for StringTable { 37 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 38 | let max_strings = input.read_u16::()?; 39 | let num_strings = input.read_u16::()?; 40 | let _ptr = input.read_u32::()?; 41 | 42 | let mut strings = Vec::with_capacity(max_strings as usize); 43 | for _ in 0..num_strings { 44 | let length = input.read_u32::()?; 45 | let mut bytes = vec![0; length as usize]; 46 | input.read_exact(&mut bytes)?; 47 | strings.push(String::from_utf8(bytes).unwrap()); 48 | } 49 | 50 | Ok(StringTable { 51 | max_strings, 52 | strings, 53 | }) 54 | } 55 | } 56 | 57 | impl WritableHeaderElement for StringTable { 58 | fn write_to(&self, handle: &mut W) -> Result<()> { 59 | handle.write_u16::(self.max_strings)?; 60 | handle.write_u16::(self.num_strings())?; 61 | handle.write_u32::(0)?; 62 | 63 | for string in &self.strings { 64 | let len = string.len(); 65 | assert!(len < u32::max_value() as usize); 66 | handle.write_u32::(len as u32)?; 67 | handle.write_all(string.as_bytes())?; 68 | } 69 | 70 | Ok(()) 71 | } 72 | } 73 | 74 | impl IntoIterator for StringTable { 75 | type Item = String; 76 | type IntoIter = ::std::vec::IntoIter; 77 | 78 | fn into_iter(self) -> Self::IntoIter { 79 | self.strings.into_iter() 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | #[test] 86 | fn read_strings() { 87 | assert_eq!(2 + 2, 4); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/genie-rec/src/unit_action.rs: -------------------------------------------------------------------------------- 1 | use crate::element::{ReadableHeaderElement, WritableHeaderElement}; 2 | use crate::reader::RecordingHeaderReader; 3 | use crate::ObjectID; 4 | use crate::Result; 5 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 6 | pub use genie_dat::sprite::SpriteID; 7 | pub use genie_support::UnitTypeID; 8 | use genie_support::{read_opt_u16, read_opt_u32}; 9 | use std::io::{Read, Write}; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct UnitAction { 13 | pub state: u32, 14 | pub target_object_id: Option, 15 | pub target_object_id_2: Option, 16 | pub target_position: (f32, f32, f32), 17 | pub timer: f32, 18 | pub target_moved_state: u8, 19 | pub task_id: Option, 20 | pub sub_action_value: u8, 21 | pub sub_actions: Vec, 22 | pub sprite_id: Option, 23 | pub params: ActionType, 24 | } 25 | 26 | impl ReadableHeaderElement for UnitAction { 27 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 28 | let action_type = input.read_u16::()?; 29 | Self::read_from_inner(input, action_type) 30 | } 31 | } 32 | 33 | impl UnitAction { 34 | fn read_from_inner( 35 | input: &mut RecordingHeaderReader, 36 | action_type: u16, 37 | ) -> Result { 38 | // TODO this is different between AoC 1.0 and AoC 1.0c. This version check is a guess 39 | // and may not actually be when it changed. May have to become more specific in the 40 | // future! 41 | let state = if input.version() <= 11.76 { 42 | input.read_u8()? as u32 43 | } else { 44 | input.read_u32::()? 45 | }; 46 | let _target_object_pointer = input.read_u32::()?; 47 | let _target_object_pointer_2 = input.read_u32::()?; 48 | let target_object_id = read_opt_u32(input)?; 49 | let target_object_id_2 = read_opt_u32(input)?; 50 | let target_position = ( 51 | input.read_f32::()?, 52 | input.read_f32::()?, 53 | input.read_f32::()?, 54 | ); 55 | let timer = input.read_f32::()?; 56 | let target_moved_state = input.read_u8()?; 57 | let task_id = read_opt_u16(input)?; 58 | let sub_action_value = input.read_u8()?; 59 | let sub_actions = UnitAction::read_list_from(input)?; 60 | let sprite_id = read_opt_u16(input)?; 61 | let params = ActionType::read_from(input, action_type)?; 62 | 63 | Ok(UnitAction { 64 | state, 65 | target_object_id, 66 | target_object_id_2, 67 | target_position, 68 | timer, 69 | target_moved_state, 70 | task_id, 71 | sub_action_value, 72 | sub_actions, 73 | sprite_id, 74 | params, 75 | }) 76 | } 77 | 78 | pub fn read_list_from(input: &mut RecordingHeaderReader) -> Result> { 79 | let mut list = vec![]; 80 | loop { 81 | let action_type = input.read_u16::()?; 82 | if action_type == 0 { 83 | return Ok(list); 84 | } 85 | let action = Self::read_from_inner(input, action_type)?; 86 | list.push(action); 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone)] 92 | pub enum ActionType { 93 | MoveTo(ActionMoveTo), 94 | Enter(ActionEnter), 95 | Explore, 96 | Attack(ActionAttack), 97 | Bird, 98 | Transport, 99 | Guard, 100 | Make(ActionMake), 101 | Artifact, 102 | } 103 | 104 | impl ActionType { 105 | pub fn read_from( 106 | input: &mut RecordingHeaderReader, 107 | action_type: u16, 108 | ) -> Result { 109 | let data = match action_type { 110 | 1 => Self::MoveTo(ActionMoveTo::read_from(input)?), 111 | 3 => Self::Enter(ActionEnter::read_from(input)?), 112 | 4 => Self::Explore, 113 | 9 => Self::Attack(ActionAttack::read_from(input)?), 114 | 10 => Self::Bird, 115 | 12 => Self::Transport, 116 | 13 => Self::Guard, 117 | 21 => Self::Make(ActionMake::read_from(input)?), 118 | 107 => Self::Artifact, 119 | _ => unimplemented!("action type {} not yet implemented", action_type), 120 | }; 121 | Ok(data) 122 | } 123 | } 124 | 125 | #[derive(Debug, Default, Clone)] 126 | pub struct ActionMoveTo { 127 | pub range: f32, 128 | } 129 | 130 | impl ReadableHeaderElement for ActionMoveTo { 131 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 132 | let range = input.read_f32::()?; 133 | Ok(Self { range }) 134 | } 135 | } 136 | 137 | impl WritableHeaderElement for ActionMoveTo { 138 | fn write_to(&self, output: &mut W) -> Result<()> { 139 | output.write_f32::(self.range)?; 140 | Ok(()) 141 | } 142 | } 143 | 144 | #[derive(Debug, Default, Clone)] 145 | pub struct ActionEnter { 146 | pub first_time: u32, 147 | } 148 | 149 | impl ReadableHeaderElement for ActionEnter { 150 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 151 | let first_time = input.read_u32::()?; 152 | Ok(Self { first_time }) 153 | } 154 | } 155 | 156 | impl WritableHeaderElement for ActionEnter { 157 | fn write_to(&self, output: &mut W) -> Result<()> { 158 | output.write_u32::(self.first_time)?; 159 | Ok(()) 160 | } 161 | } 162 | 163 | #[allow(dead_code)] 164 | #[derive(Debug, Default, Clone)] 165 | pub struct ActionAttack { 166 | range: f32, 167 | min_range: f32, 168 | missile_id: UnitTypeID, 169 | frame_delay: u16, 170 | need_to_attack: u16, 171 | was_same_owner: u16, 172 | indirect_fire_flag: u8, 173 | move_sprite_id: Option, 174 | fight_sprite_id: Option, 175 | wait_sprite_id: Option, 176 | last_target_position: (f32, f32, f32), 177 | } 178 | 179 | impl ReadableHeaderElement for ActionAttack { 180 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 181 | Ok(ActionAttack { 182 | range: input.read_f32::()?, 183 | min_range: input.read_f32::()?, 184 | missile_id: input.read_u16::()?.into(), 185 | frame_delay: input.read_u16::()?, 186 | need_to_attack: input.read_u16::()?, 187 | was_same_owner: input.read_u16::()?, 188 | indirect_fire_flag: input.read_u8()?, 189 | move_sprite_id: read_opt_u16(input)?, 190 | fight_sprite_id: read_opt_u16(input)?, 191 | wait_sprite_id: read_opt_u16(input)?, 192 | last_target_position: ( 193 | input.read_f32::()?, 194 | input.read_f32::()?, 195 | input.read_f32::()?, 196 | ), 197 | }) 198 | } 199 | } 200 | 201 | #[derive(Debug, Default, Clone)] 202 | pub struct ActionMake { 203 | pub work_timer: f32, 204 | } 205 | 206 | impl ReadableHeaderElement for ActionMake { 207 | fn read_from(input: &mut RecordingHeaderReader) -> Result { 208 | let work_timer = input.read_f32::()?; 209 | Ok(Self { work_timer }) 210 | } 211 | } 212 | 213 | impl WritableHeaderElement for ActionMake { 214 | fn write_to(&self, output: &mut W) -> Result<()> { 215 | output.write_f32::(self.work_timer)?; 216 | Ok(()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /crates/genie-rec/src/version.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::fmt; 3 | use std::fmt::{Debug, Display}; 4 | use std::io::Read; 5 | 6 | /// the variant of AoE2 game 7 | #[derive(Copy, Clone, Eq, PartialEq, Debug)] 8 | pub enum GameVariant { 9 | /// A trial version, either AoC or AoE 10 | Trial, 11 | /// Age of Kings 12 | AgeOfKings, 13 | /// Age of Conquerors 14 | AgeOfConquerors, 15 | /// User Patch 16 | UserPatch, 17 | /// Forgotten Empires mod 18 | ForgottenEmpires, 19 | /// AoE2:HD release 20 | HighDefinition, 21 | /// AoE2:DE release 22 | DefinitiveEdition, 23 | } 24 | 25 | pub const TRIAL_VERSION: GameVersion = GameVersion(*b"TRL 9.3\0"); 26 | pub const AGE_OF_KINGS_VERSION: GameVersion = GameVersion(*b"VER 9.3\0"); 27 | pub const AGE_OF_CONQUERORS_VERSION: GameVersion = GameVersion(*b"VER 9.4\0"); 28 | pub const FORGOTTEN_EMPIRES_VERSION: GameVersion = GameVersion(*b"VER 9.5\0"); 29 | 30 | // So last known AoC version is 11.76 31 | // Since all versions only have a precision of 2 32 | // We can save do + 0.01 33 | pub const HD_SAVE_VERSION: f32 = 11.77; 34 | 35 | pub const DE_SAVE_VERSION: f32 = 12.97; 36 | 37 | use GameVariant::*; 38 | 39 | impl GameVariant { 40 | pub fn resolve_variant(version: &GameVersion, sub_version: f32) -> Option { 41 | // taken from https://github.com/goto-bus-stop/recanalyst/blob/master/src/Analyzers/VersionAnalyzer.php 42 | 43 | Some(match *version { 44 | // Either AOC or AOK trial, just return Trial :shrug: 45 | TRIAL_VERSION => Trial, 46 | AGE_OF_KINGS_VERSION => AgeOfKings, 47 | AGE_OF_CONQUERORS_VERSION if sub_version >= DE_SAVE_VERSION => DefinitiveEdition, 48 | AGE_OF_CONQUERORS_VERSION if sub_version >= HD_SAVE_VERSION => HighDefinition, 49 | AGE_OF_CONQUERORS_VERSION => AgeOfConquerors, 50 | FORGOTTEN_EMPIRES_VERSION => ForgottenEmpires, 51 | // UserPatch uses VER 9.\0 where N is anything between 8 and F 52 | GameVersion([b'V', b'E', b'R', b' ', b'9', b'.', b'8'..=b'F', b'\0']) => UserPatch, 53 | _ => return None, 54 | }) 55 | } 56 | 57 | pub fn is_original(&self) -> bool { 58 | matches!(self, Trial | AgeOfKings | AgeOfConquerors) 59 | } 60 | 61 | pub fn is_mod(&self) -> bool { 62 | matches!(self, ForgottenEmpires | UserPatch) 63 | } 64 | 65 | pub fn is_update(&self) -> bool { 66 | matches!(self, HighDefinition | DefinitiveEdition) 67 | } 68 | } 69 | 70 | /// A bit of a weird comparing check 71 | /// It follows the hierarchy of what game is based on what 72 | /// 73 | /// Thus AgeOfConquerors is bigger than AgeOfKings and HighDefinition is bigger than AgeOfConquers etc. 74 | /// 75 | /// The confusing part is around HighDefinition and UserPatch 76 | /// UserPatch is neither bigger, smaller or equal to the -new- editions created by MSFT and vice versa 77 | impl PartialOrd for GameVariant { 78 | fn partial_cmp(&self, other: &Self) -> Option { 79 | // quick return for equal stuff :) 80 | if other == self { 81 | return Some(Ordering::Equal); 82 | } 83 | 84 | // Try to not use Ord operators here, to make sure we don't fall in weird recursive traps 85 | let is_mod = self.is_mod() || other.is_mod(); 86 | let update = self.is_update() || other.is_update(); 87 | let original = self.is_original() || other.is_original(); 88 | 89 | // Can't compare between user patch and hd and up 90 | if is_mod && update { 91 | return None; 92 | } 93 | 94 | if original && (is_mod || update) { 95 | return Some(if self.is_original() { 96 | Ordering::Less 97 | } else { 98 | Ordering::Greater 99 | }); 100 | } 101 | 102 | // So this part is a bit confusing 103 | // but basically we removed all comparisons that are between e.g. mod, update and original 104 | // and we removed all comparisons that are equal 105 | // so the only comparison left is within their own class 106 | Some(match self { 107 | // Trial is only compared to AoK and AoC, and is the first version, thus always less 108 | Trial => Ordering::Less, 109 | // AoK can only be greater if compared against trial 110 | AgeOfKings if other == &Trial => Ordering::Greater, 111 | AgeOfKings => Ordering::Less, 112 | // AoC will always be greater 113 | AgeOfConquerors => Ordering::Greater, 114 | // Can we compare UP and FE??? 115 | UserPatch | ForgottenEmpires => return None, 116 | // HD can only be compared to DE, and vice versa 117 | HighDefinition => Ordering::Less, 118 | DefinitiveEdition => Ordering::Greater, 119 | }) 120 | } 121 | } 122 | 123 | /// The game data version string. In practice, this does not really reflect the game version. 124 | #[derive(Clone, Copy, PartialEq, Eq, Default)] 125 | pub struct GameVersion([u8; 8]); 126 | 127 | impl From<&[u8; 8]> for GameVersion { 128 | fn from(val: &[u8; 8]) -> Self { 129 | GameVersion(*val) 130 | } 131 | } 132 | 133 | /// I am very lazy :) 134 | impl From<&[u8; 7]> for GameVersion { 135 | fn from(val: &[u8; 7]) -> Self { 136 | let mut whole = [0; 8]; 137 | whole[..7].copy_from_slice(val); 138 | GameVersion(whole) 139 | } 140 | } 141 | 142 | impl Debug for GameVersion { 143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 144 | write!(f, "{:?}", std::str::from_utf8(&self.0).unwrap()) 145 | } 146 | } 147 | 148 | impl Display for GameVersion { 149 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 150 | write!(f, "{}", std::str::from_utf8(&self.0).unwrap()) 151 | } 152 | } 153 | 154 | impl GameVersion { 155 | /// Read the game version string from an input stream. 156 | pub fn read_from(input: &mut R) -> crate::Result { 157 | let mut game_version = [0; 8]; 158 | input.read_exact(&mut game_version)?; 159 | Ok(Self(game_version)) 160 | } 161 | } 162 | 163 | #[cfg(test)] 164 | mod test { 165 | use crate::GameVariant::*; 166 | use crate::*; 167 | 168 | #[test] 169 | pub fn test_game_variant_resolution() { 170 | assert_eq!( 171 | Some(Trial), 172 | GameVariant::resolve_variant(&TRIAL_VERSION, 0.0) 173 | ); 174 | 175 | assert_eq!( 176 | Some(AgeOfKings), 177 | GameVariant::resolve_variant(&AGE_OF_KINGS_VERSION, 0.0) 178 | ); 179 | 180 | assert_eq!( 181 | Some(AgeOfConquerors), 182 | GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, 0.0) 183 | ); 184 | 185 | assert_eq!( 186 | Some(HighDefinition), 187 | GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, HD_SAVE_VERSION) 188 | ); 189 | 190 | assert_eq!( 191 | Some(DefinitiveEdition), 192 | GameVariant::resolve_variant(&AGE_OF_CONQUERORS_VERSION, DE_SAVE_VERSION) 193 | ); 194 | 195 | assert_eq!( 196 | Some(UserPatch), 197 | GameVariant::resolve_variant(&b"VER 9.A".into(), 0.0), 198 | ); 199 | 200 | assert_eq!( 201 | Some(ForgottenEmpires), 202 | GameVariant::resolve_variant(&b"VER 9.5".into(), 0.0), 203 | ); 204 | } 205 | 206 | #[allow(clippy::bool_assert_comparison)] 207 | #[test] 208 | pub fn test_game_variant_comparison() { 209 | // Am I going add all cases here? WHO KNOWS 210 | assert!(Trial < AgeOfKings); 211 | assert!(AgeOfKings > Trial); 212 | assert!(Trial < AgeOfConquerors); 213 | assert!(AgeOfConquerors > Trial); 214 | assert!(Trial < ForgottenEmpires); 215 | assert!(ForgottenEmpires > Trial); 216 | assert!(Trial < UserPatch); 217 | assert!(UserPatch > Trial); 218 | assert!(Trial < HighDefinition); 219 | assert!(HighDefinition > Trial); 220 | assert!(Trial < DefinitiveEdition); 221 | assert!(DefinitiveEdition > Trial); 222 | 223 | assert!(AgeOfKings < AgeOfConquerors); 224 | assert!(AgeOfConquerors > AgeOfKings); 225 | assert!(AgeOfKings < ForgottenEmpires); 226 | assert!(ForgottenEmpires > AgeOfKings); 227 | assert!(AgeOfKings < UserPatch); 228 | assert!(UserPatch > AgeOfKings); 229 | assert!(AgeOfKings < HighDefinition); 230 | assert!(HighDefinition > AgeOfKings); 231 | assert!(AgeOfKings < DefinitiveEdition); 232 | assert!(DefinitiveEdition > AgeOfKings); 233 | 234 | assert!(AgeOfConquerors < ForgottenEmpires); 235 | assert!(ForgottenEmpires > AgeOfConquerors); 236 | assert!(AgeOfConquerors < UserPatch); 237 | assert!(UserPatch > AgeOfConquerors); 238 | assert!(AgeOfConquerors < HighDefinition); 239 | assert!(HighDefinition > AgeOfConquerors); 240 | assert!(AgeOfConquerors < DefinitiveEdition); 241 | assert!(DefinitiveEdition > AgeOfConquerors); 242 | 243 | assert_eq!(false, ForgottenEmpires < UserPatch); 244 | assert_eq!(false, UserPatch > ForgottenEmpires); 245 | assert_eq!(false, ForgottenEmpires < HighDefinition); 246 | assert_eq!(false, HighDefinition > ForgottenEmpires); 247 | assert_eq!(false, ForgottenEmpires < DefinitiveEdition); 248 | assert_eq!(false, DefinitiveEdition > ForgottenEmpires); 249 | 250 | assert_eq!(false, UserPatch < HighDefinition); 251 | assert_eq!(false, HighDefinition > UserPatch); 252 | assert_eq!(false, UserPatch < DefinitiveEdition); 253 | assert_eq!(false, DefinitiveEdition > UserPatch); 254 | 255 | assert!(HighDefinition < DefinitiveEdition); 256 | assert!(DefinitiveEdition > HighDefinition); 257 | // yes i was 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /crates/genie-rec/test/aok.mgl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-rec/test/aok.mgl -------------------------------------------------------------------------------- /crates/genie-rec/test/missyou_finally_vs_11.mgx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-rec/test/missyou_finally_vs_11.mgx -------------------------------------------------------------------------------- /crates/genie-rec/test/rec.20181208-195117.mgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-rec/test/rec.20181208-195117.mgz -------------------------------------------------------------------------------- /crates/genie-scx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-scx" 3 | version = "4.0.0" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read and write Age of Empires I/II scenario files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-scx" 10 | documentation = "https://docs.rs/genie-scx" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | exclude = ["test/scenarios"] 14 | 15 | [dependencies] 16 | byteorder.workspace = true 17 | flate2.workspace = true 18 | genie-support = { version = "^1.0.0", path = "../genie-support", features = [ 19 | "strings", 20 | ] } 21 | log = "0.4.17" 22 | nohash-hasher = "0.2.0" 23 | rgb.workspace = true 24 | thiserror.workspace = true 25 | num_enum.workspace = true 26 | 27 | [dev-dependencies] 28 | anyhow.workspace = true 29 | -------------------------------------------------------------------------------- /crates/genie-scx/README.md: -------------------------------------------------------------------------------- 1 | # genie-scx 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--scx-blue?style=flat-square&color=blue)](https://docs.rs/genie-scx) 4 | [![crates.io](https://img.shields.io/crates/v/genie-scx.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-scx) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Age of Empires 2 scenario reader, writer, and converter. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-scx/src/ai.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 3 | use genie_support::{write_i32_str, DecodeStringError, ReadStringError, ReadStringsExt}; 4 | use std::convert::TryFrom; 5 | use std::io::{Read, Write}; 6 | 7 | #[derive( 8 | Debug, Clone, Copy, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive, 9 | )] 10 | #[repr(u32)] 11 | pub enum AIErrorCode { 12 | /// 13 | ConstantAlreadyDefined = 0, 14 | /// 15 | FileOpenFailed = 1, 16 | /// 17 | FileReadFailed = 2, 18 | /// 19 | InvalidIdentifier = 3, 20 | /// 21 | InvalidKeyword = 4, 22 | /// 23 | InvalidPreprocessorDirective = 5, 24 | /// 25 | ListFull = 6, 26 | /// 27 | MissingArrow = 7, 28 | /// 29 | MissingClosingParenthesis = 8, 30 | /// 31 | MissingClosingQuote = 9, 32 | /// 33 | MissingEndIf = 10, 34 | /// 35 | MissingFileName = 11, 36 | /// 37 | MissingIdentifier = 12, 38 | /// 39 | MissingKeyword = 13, 40 | /// 41 | MissingLHS = 14, 42 | /// 43 | MissingOpeningParenthesis = 15, 44 | /// 45 | MissingPreprocessorSymbol = 16, 46 | /// 47 | MissingRHS = 17, 48 | /// 49 | NoRules = 18, 50 | /// 51 | PreprocessorNestingTooDeep = 19, 52 | /// 53 | RuleTooLong = 20, 54 | /// 55 | StringTableFull = 21, 56 | /// 57 | UndocumentedError = 22, 58 | /// 59 | UnexpectedElse = 23, 60 | /// 61 | UnexpectedEndIf = 24, 62 | /// 63 | UnexpectedError = 25, 64 | /// 65 | UnexpectedEOF = 26, 66 | } 67 | 68 | #[derive(Debug, Clone)] 69 | pub struct AIErrorInfo { 70 | filename: String, 71 | line_number: i32, 72 | description: String, 73 | error_code: AIErrorCode, 74 | } 75 | 76 | fn parse_bytes(bytes: &[u8]) -> std::result::Result { 77 | let mut bytes = bytes.to_vec(); 78 | if let Some(end) = bytes.iter().position(|&byte| byte == 0) { 79 | bytes.truncate(end); 80 | } 81 | if bytes.is_empty() { 82 | Ok("".to_string()) 83 | } else { 84 | String::from_utf8(bytes).map_err(|_| ReadStringError::DecodeStringError(DecodeStringError)) 85 | } 86 | } 87 | 88 | impl AIErrorInfo { 89 | /// Read AI error information from an input stream. 90 | pub fn read_from(mut input: impl Read) -> Result { 91 | // TODO support non UTF8 encoding 92 | let mut filename_bytes = [0; 257]; 93 | input.read_exact(&mut filename_bytes)?; 94 | let line_number = input.read_i32::()?; 95 | let mut description_bytes = [0; 128]; 96 | input.read_exact(&mut description_bytes)?; 97 | let error_code = AIErrorCode::try_from(input.read_u32::()?)?; 98 | 99 | let filename = parse_bytes(&filename_bytes)?; 100 | let description = parse_bytes(&description_bytes)?; 101 | 102 | Ok(AIErrorInfo { 103 | filename, 104 | line_number, 105 | description, 106 | error_code, 107 | }) 108 | } 109 | 110 | /// Write AI error information to an output stream. 111 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 112 | // TODO support non UTF8 encoding 113 | let mut filename_bytes = [0; 257]; 114 | filename_bytes[..self.filename.len()].copy_from_slice(self.filename.as_bytes()); 115 | output.write_all(&filename_bytes)?; 116 | 117 | output.write_i32::(self.line_number)?; 118 | 119 | let mut description_bytes = [0; 128]; 120 | description_bytes[..self.description.len()].copy_from_slice(self.description.as_bytes()); 121 | output.write_all(&description_bytes)?; 122 | 123 | output.write_u32::(self.error_code.into())?; 124 | 125 | Ok(()) 126 | } 127 | } 128 | 129 | #[derive(Debug, Clone)] 130 | pub struct AIFile { 131 | filename: String, 132 | content: String, 133 | } 134 | 135 | impl AIFile { 136 | /// Read an embedded AI file from an input stream. 137 | pub fn read_from(mut input: impl Read) -> Result { 138 | let filename = input 139 | .read_u32_length_prefixed_str()? 140 | .expect("missing ai file name"); 141 | let content = input 142 | .read_u32_length_prefixed_str()? 143 | .expect("empty ai file?"); 144 | 145 | Ok(Self { filename, content }) 146 | } 147 | 148 | /// Write this embedded AI file to an output stream. 149 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 150 | write_i32_str(&mut output, &self.filename)?; 151 | write_i32_str(&mut output, &self.content)?; 152 | Ok(()) 153 | } 154 | } 155 | 156 | #[derive(Debug, Default, Clone)] 157 | pub struct AIInfo { 158 | error: Option, 159 | files: Vec, 160 | } 161 | 162 | impl AIInfo { 163 | pub fn read_from(mut input: impl Read) -> Result> { 164 | let has_ai_files = input.read_u32::()? != 0; 165 | let has_error = input.read_u32::()? != 0; 166 | 167 | if !has_error && !has_ai_files { 168 | return Ok(None); 169 | } 170 | 171 | let error = if has_error { 172 | Some(AIErrorInfo::read_from(&mut input)?) 173 | } else { 174 | None 175 | }; 176 | 177 | let num_ai_files = input.read_u32::()?; 178 | let mut files = vec![]; 179 | for _ in 0..num_ai_files { 180 | files.push(AIFile::read_from(&mut input)?); 181 | } 182 | 183 | Ok(Some(Self { error, files })) 184 | } 185 | 186 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 187 | output.write_u32::(u32::from(!self.files.is_empty()))?; 188 | 189 | if let Some(error) = &self.error { 190 | output.write_u32::(1)?; 191 | error.write_to(&mut output)?; 192 | } else { 193 | output.write_u32::(0)?; 194 | } 195 | 196 | if !self.files.is_empty() { 197 | output.write_u32::(self.files.len() as u32)?; 198 | for file in &self.files { 199 | file.write_to(&mut output)?; 200 | } 201 | } 202 | 203 | Ok(()) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /crates/genie-scx/src/bitmap.rs: -------------------------------------------------------------------------------- 1 | //! Handles bitmap files embedded in the scenario file. 2 | 3 | use crate::Result; 4 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 5 | use rgb::RGBA8; 6 | use std::io::{Read, Write}; 7 | 8 | /// Bitmap header info. 9 | #[derive(Debug, Default, Clone)] 10 | pub struct BitmapInfo { 11 | size: u32, 12 | width: i32, 13 | height: i32, 14 | planes: u16, 15 | bit_count: u16, 16 | compression: u32, 17 | size_image: u32, 18 | xpels_per_meter: i32, 19 | ypels_per_meter: i32, 20 | clr_used: u32, 21 | clr_important: u32, 22 | colors: Vec, 23 | } 24 | 25 | impl BitmapInfo { 26 | /// Read a bitmap header info structure from a byte stream. 27 | pub fn read_from(mut input: impl Read) -> Result { 28 | let mut bitmap = BitmapInfo { 29 | size: input.read_u32::()?, 30 | width: input.read_i32::()?, 31 | height: input.read_i32::()?, 32 | planes: input.read_u16::()?, 33 | bit_count: input.read_u16::()?, 34 | compression: input.read_u32::()?, 35 | size_image: input.read_u32::()?, 36 | xpels_per_meter: input.read_i32::()?, 37 | ypels_per_meter: input.read_i32::()?, 38 | clr_used: input.read_u32::()?, 39 | clr_important: input.read_u32::()?, 40 | ..Default::default() 41 | }; 42 | 43 | for _ in 0..256 { 44 | let r = input.read_u8()?; 45 | let g = input.read_u8()?; 46 | let b = input.read_u8()?; 47 | let a = input.read_u8()?; 48 | bitmap.colors.push(RGBA8 { r, g, b, a }); 49 | } 50 | 51 | Ok(bitmap) 52 | } 53 | 54 | #[allow(unused)] 55 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 56 | assert_eq!(self.colors.len(), 256); 57 | 58 | output.write_u32::(self.size)?; 59 | output.write_i32::(self.width)?; 60 | output.write_i32::(self.height)?; 61 | output.write_u16::(self.planes)?; 62 | output.write_u16::(self.bit_count)?; 63 | output.write_u32::(self.compression)?; 64 | output.write_u32::(self.size_image)?; 65 | output.write_i32::(self.xpels_per_meter)?; 66 | output.write_i32::(self.ypels_per_meter)?; 67 | output.write_u32::(self.clr_used)?; 68 | output.write_u32::(self.clr_important)?; 69 | for color in &self.colors { 70 | output.write_u8(color.r)?; 71 | output.write_u8(color.g)?; 72 | output.write_u8(color.b)?; 73 | output.write_u8(color.a)?; 74 | } 75 | 76 | Ok(()) 77 | } 78 | } 79 | 80 | /// A Genie-style bitmap file: a typical BMP with some metadata. 81 | #[derive(Debug)] 82 | pub struct Bitmap { 83 | own_memory: u32, 84 | width: u32, 85 | height: u32, 86 | orientation: u16, 87 | info: BitmapInfo, 88 | pixels: Vec, 89 | } 90 | 91 | impl Bitmap { 92 | pub fn read_from(mut input: impl Read) -> Result> { 93 | let own_memory = input.read_u32::()?; 94 | let width = input.read_u32::()?; 95 | let height = input.read_u32::()?; 96 | let orientation = input.read_u16::()?; 97 | 98 | if width > 0 && height > 0 { 99 | let info = BitmapInfo::read_from(&mut input)?; 100 | let aligned_width = height * ((width + 3) & !3); 101 | let mut pixels = vec![0u8; aligned_width as usize]; 102 | input.read_exact(&mut pixels)?; 103 | Ok(Some(Bitmap { 104 | own_memory, 105 | width, 106 | height, 107 | orientation, 108 | info, 109 | pixels, 110 | })) 111 | } else { 112 | Ok(None) 113 | } 114 | } 115 | 116 | #[allow(unused)] 117 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 118 | output.write_u32::(self.own_memory)?; 119 | output.write_u32::(self.width)?; 120 | output.write_u32::(self.height)?; 121 | output.write_u16::(self.orientation)?; 122 | self.info.write_to(&mut output)?; 123 | output.write_all(&self.pixels)?; 124 | Ok(()) 125 | } 126 | 127 | #[allow(unused)] 128 | pub fn write_empty(mut output: impl Write) -> Result<()> { 129 | output.write_u32::(0)?; 130 | output.write_u32::(0)?; 131 | output.write_u32::(0)?; 132 | output.write_u16::(0)?; 133 | Ok(()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/genie-scx/src/convert/aoc_to_wk.rs: -------------------------------------------------------------------------------- 1 | use super::ConvertError; 2 | use crate::{Scenario, ScenarioObject, Tile, Trigger, UnitTypeID}; 3 | use nohash_hasher::IntMap; 4 | 5 | pub struct AoCToWK { 6 | object_ids_map: IntMap, 7 | terrain_ids_map: IntMap, 8 | } 9 | 10 | impl Default for AoCToWK { 11 | fn default() -> Self { 12 | let object_ids_map = [ 13 | // NOTE: These are special to make the tech tree work 14 | (1103, 529), // Fire Galley, Fire Ship 15 | (529, 1103), // Fire Ship, Fire Galley 16 | (1104, 527), // Demolition Raft, Demolition Ship 17 | (527, 1104), // Demolition Ship, Demolition Raft 18 | ] 19 | .iter() 20 | .map(|(a, b)| (*a, UnitTypeID::from(*b))) 21 | .collect(); 22 | 23 | let terrain_ids_map = [ 24 | (11, 3), // Dirt 2, Dirt 3 25 | (16, 0), // Grass-ish, Grass 26 | (20, 19), // Oak Forest, Pine Forest 27 | ] 28 | .iter() 29 | .map(|(a, b)| (*a, *b)) 30 | .collect(); 31 | 32 | Self { 33 | object_ids_map, 34 | terrain_ids_map, 35 | } 36 | } 37 | } 38 | 39 | impl AoCToWK { 40 | /// Convert an object from AoC to WK. 41 | /// 42 | /// This updates the object type IDs. 43 | fn convert_object(&self, object: &mut ScenarioObject) { 44 | if let Some(new_type) = self.object_ids_map.get(&object.object_type.into()) { 45 | object.object_type = *new_type; 46 | } 47 | } 48 | 49 | /// Convert a trigger from AoC to WK. 50 | /// 51 | /// This updates the object type IDs in trigger conditions and effects. 52 | fn convert_trigger(&self, trigger: &mut Trigger) { 53 | trigger.conditions_unordered_mut().for_each(|cond| { 54 | if let Some(new_type) = self.object_ids_map.get(&cond.unit_type().into()) { 55 | cond.set_unit_type(*new_type); 56 | } 57 | if let Some(new_type) = self.object_ids_map.get(&cond.object_type().into()) { 58 | cond.set_object_type(*new_type); 59 | } 60 | }); 61 | trigger.effects_unordered_mut().for_each(|effect| { 62 | if let Some(new_type) = self.object_ids_map.get(&effect.unit_type().into()) { 63 | effect.set_unit_type(*new_type); 64 | } 65 | if let Some(new_type) = self.object_ids_map.get(&effect.object_type().into()) { 66 | effect.set_object_type(*new_type); 67 | } 68 | }); 69 | } 70 | 71 | /// Convert a terrain tile from AoC to WK. 72 | fn convert_terrain(&self, tile: &mut Tile) { 73 | if let Some(new_type) = self.terrain_ids_map.get(&tile.terrain) { 74 | tile.terrain = *new_type; 75 | } 76 | } 77 | 78 | /// Convert a scenario from AoC to WK in-place. 79 | pub fn convert(&self, scen: &mut Scenario) -> Result<(), ConvertError> { 80 | for object in scen.objects_mut() { 81 | self.convert_object(object); 82 | } 83 | 84 | for tile in scen.map_mut().tiles_mut() { 85 | self.convert_terrain(tile); 86 | } 87 | 88 | if let Some(trigger_system) = scen.triggers_mut() { 89 | for trigger in trigger_system.triggers_unordered_mut() { 90 | self.convert_trigger(trigger); 91 | } 92 | } 93 | 94 | Ok(()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/genie-scx/src/convert/hd_to_wk.rs: -------------------------------------------------------------------------------- 1 | use super::ConvertError; 2 | use crate::{Scenario, ScenarioObject, Tile, Trigger, UnitTypeID}; 3 | use nohash_hasher::IntMap; 4 | 5 | /// Convert an HD Edition scenario to a WololoKingdoms-compatible one. 6 | /// 7 | /// Maps HD unit IDs and terrain IDs to their WK equivalents. 8 | pub struct HDToWK { 9 | object_ids_map: IntMap, 10 | terrain_ids_map: IntMap, 11 | } 12 | 13 | impl Default for HDToWK { 14 | fn default() -> Self { 15 | let object_ids_map = [ 16 | // NOTE: These are special to make the tech tree work 17 | (1103, 529), // Fire Galley, Fire Ship 18 | (529, 1103), // Fire Ship, Fire Galley 19 | (1104, 527), // Demolition Raft, Demolition Ship 20 | (527, 1104), // Demolition Ship, Demolition Raft 21 | (1001, 106), // Organ Gun, INFIL_D 22 | (1003, 114), // Elite Organ Gun, LNGBT_D 23 | (1006, 183), // Elite Caravel, TMISB 24 | (1007, 203), // Camel Archer, VDML 25 | (1009, 208), // Elite Camel Archer, TWAL 26 | (1010, 223), // Genitour, VFREP_D 27 | (1012, 230), // Elite Genitour, VMREP_D 28 | (1013, 260), // Gbeto, OLD-FISH3 29 | (1015, 418), // Elite Gbeto, TROCK 30 | (1016, 453), // Shotel Warrior, DOLPH4 31 | (1018, 459), // Elite Shotel Warrior, FISH5 32 | (1103, 467), // Fire Ship, Nonexistent 33 | (1105, 494), // Siege Tower, CVLRY_D 34 | (1104, 653), // Demolition Ship, HFALS_D 35 | (947, 699), // Cutting Mangonel, HSUBO_D 36 | (948, 701), // Cutting Onager, HWOLF_D 37 | (1079, 732), // Genitour placeholder, HKHAN_D 38 | (1021, 734), // Feitoria, Nonexistent 39 | (1120, 760), // Ballista Elephant, BHUSK_D 40 | (1155, 762), // Imperial Skirmisher, BHUSKX_D 41 | (1134, 766), // Elite Battle Ele, UPLUM_D 42 | (1132, 774), // Battle Elephant, UCONQ_D 43 | (1131, 782), // Elite Rattan Archer, HPOPE_D 44 | (1129, 784), // Rattan Archer, HWITCH_D 45 | (1128, 811), // Elite Arambai, HEROBOAR_D 46 | (1126, 823), // Arambai, BOARJ_D 47 | (1125, 830), // Elite Karambit, UWAGO_D 48 | (1123, 836), // Karambit, HORSW_D 49 | (946, 848), // Noncut Ballista Elephant, TDONK_D 50 | (1004, 861), // Caravel, mkyby_D 51 | (1122, 891), // Elite Ballista Ele, SGTWR_D 52 | ] 53 | .iter() 54 | .map(|(a, b)| (*a, UnitTypeID::from(*b))) 55 | .collect(); 56 | 57 | let terrain_ids_map = [ 58 | (38, 33), // Snow Road, Snow Dirt 59 | (45, 38), // Cracked Earth, Snow Road 60 | (54, 11), // Mangrove Terrain 61 | (55, 20), // Mangrove Forest 62 | (50, 41), // Acacia Forest 63 | (49, 16), // Baobab Forest 64 | (11, 3), // Dirt 2, Dirt 3 65 | (16, 0), // Grass-ish, Grass 66 | (20, 19), // Oak Forest, Pine Forest 67 | ] 68 | .iter() 69 | .map(|(a, b)| (*a, *b)) 70 | .collect(); 71 | 72 | Self { 73 | object_ids_map, 74 | terrain_ids_map, 75 | } 76 | } 77 | } 78 | 79 | impl HDToWK { 80 | /// Convert an object from HD Edition to WK. 81 | /// 82 | /// This updates the object type IDs. 83 | fn convert_object(&self, object: &mut ScenarioObject) { 84 | if let Some(new_type) = self.object_ids_map.get(&object.object_type.into()) { 85 | object.object_type = *new_type; 86 | } 87 | } 88 | 89 | /// Convert a trigger from HD Edition to WK. 90 | /// 91 | /// This updates the object type IDs in trigger conditions and effects. 92 | fn convert_trigger(&self, trigger: &mut Trigger) { 93 | trigger.conditions_unordered_mut().for_each(|cond| { 94 | if let Some(new_type) = self.object_ids_map.get(&cond.unit_type().into()) { 95 | cond.set_unit_type(*new_type); 96 | } 97 | if let Some(new_type) = self.object_ids_map.get(&cond.object_type().into()) { 98 | cond.set_object_type(*new_type); 99 | } 100 | }); 101 | trigger.effects_unordered_mut().for_each(|effect| { 102 | if let Some(new_type) = self.object_ids_map.get(&effect.unit_type().into()) { 103 | effect.set_unit_type(*new_type); 104 | } 105 | if let Some(new_type) = self.object_ids_map.get(&effect.object_type().into()) { 106 | effect.set_object_type(*new_type); 107 | } 108 | }); 109 | } 110 | 111 | fn convert_terrain(&self, tile: &mut Tile) { 112 | if let Some(new_type) = self.terrain_ids_map.get(&tile.terrain) { 113 | tile.terrain = *new_type; 114 | } 115 | } 116 | 117 | /// Convert a scenario from HD to WK in-place. 118 | pub fn convert(&self, scen: &mut Scenario) -> Result<(), ConvertError> { 119 | for object in scen.objects_mut() { 120 | self.convert_object(object); 121 | } 122 | 123 | for tile in scen.map_mut().tiles_mut() { 124 | self.convert_terrain(tile); 125 | } 126 | 127 | if let Some(trigger_system) = scen.triggers_mut() { 128 | for trigger in trigger_system.triggers_unordered_mut() { 129 | self.convert_trigger(trigger); 130 | } 131 | } 132 | 133 | Ok(()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/genie-scx/src/convert/mod.rs: -------------------------------------------------------------------------------- 1 | //! Automated scenario conversions. 2 | //! 3 | //! This module implements conversions between different scenario formats and game versions. 4 | mod aoc_to_wk; 5 | mod hd_to_wk; 6 | 7 | use crate::{Scenario, ScenarioObject}; 8 | 9 | pub use aoc_to_wk::AoCToWK; 10 | pub use hd_to_wk::HDToWK; 11 | 12 | /// Error indicating scenario conversion failure. 13 | #[derive(Debug, thiserror::Error)] 14 | pub enum ConvertError { 15 | /// The input scenario version is not supported by the converter. 16 | #[error("invalid version")] 17 | InvalidVersion, 18 | } 19 | 20 | /// Convert an AoC or HD Edition scenario file to a WololoKingdoms one. 21 | /// 22 | /// It will auto-detect the version of the file, and output a WK compatible scenario. 23 | /// AoC scenarios will have their unit and terrain IDs switched around so they have the correct 24 | /// look in WK. 25 | /// HD Edition scenarios will have all the new unit and terrain IDs mapped to WK IDs. 26 | /// 27 | /// ## Usage 28 | /// 29 | /// ```rust,ignore 30 | /// use genie_scx::convert::AutoToWK; 31 | /// AutoToWK::default().convert(&mut scenario)? 32 | /// ``` 33 | 34 | #[derive(Debug, Default)] 35 | pub struct AutoToWK {} 36 | 37 | /// Check if a scenario object is likely a WololoKingdoms one. 38 | fn is_wk_object(object: &ScenarioObject) -> bool { 39 | // Stormy Dog is the highest ID in AoC 1.0c. 40 | const STORMY_DOG: i32 = 862; 41 | object.id > STORMY_DOG 42 | } 43 | 44 | impl AutoToWK { 45 | pub fn convert(&self, scen: &mut Scenario) -> Result<(), ConvertError> { 46 | if scen.version().is_hd_edition() { 47 | HDToWK::default().convert(scen) 48 | } else if scen.version().is_aok() || scen.version().is_aoc() { 49 | if scen.objects().any(is_wk_object) { 50 | // No conversion necessary. 51 | Ok(()) 52 | } else { 53 | AoCToWK::default().convert(scen) 54 | } 55 | } else { 56 | Err(ConvertError::InvalidVersion) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/genie-scx/src/header.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{DLCPackage, DataSet, SCXVersion}; 2 | use crate::Result; 3 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 4 | use genie_support::{write_opt_i32_str, ReadStringsExt}; 5 | use std::convert::TryFrom; 6 | use std::io::{Read, Write}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct DLCOptions { 10 | /// Version of the DLC options structure. 11 | pub version: i32, 12 | /// The base data set. 13 | pub game_data_set: DataSet, 14 | /// The DLCs that are required by this scenario. 15 | pub dependencies: Vec, 16 | } 17 | 18 | impl Default for DLCOptions { 19 | fn default() -> Self { 20 | Self { 21 | version: 1000, 22 | game_data_set: DataSet::BaseGame, 23 | dependencies: vec![DLCPackage::AgeOfKings, DLCPackage::AgeOfConquerors], 24 | } 25 | } 26 | } 27 | 28 | impl DLCOptions { 29 | pub fn read_from(mut input: impl Read) -> Result { 30 | // If version is 0 or 1, it's actually the dataset identifier from 31 | // before DLCOptions was versioned. 32 | let version_or_data_set = input.read_i32::()?; 33 | let game_data_set = 34 | DataSet::try_from(if version_or_data_set == 0 || version_or_data_set == 1 { 35 | version_or_data_set 36 | } else { 37 | input.read_i32::()? 38 | })?; 39 | 40 | // Set version to 0 for old DLCOptions. 41 | let version = if version_or_data_set == 1 { 42 | 0 43 | } else { 44 | version_or_data_set 45 | }; 46 | 47 | let num_dependencies = input.read_u32::()?; 48 | let mut dependencies = vec![DLCPackage::AgeOfKings; num_dependencies as usize]; 49 | for dependency in dependencies.iter_mut() { 50 | *dependency = DLCPackage::try_from(input.read_i32::()?)?; 51 | } 52 | 53 | Ok(DLCOptions { 54 | version, 55 | game_data_set, 56 | dependencies, 57 | }) 58 | } 59 | 60 | pub fn write_to(&self, mut output: impl Write) -> Result<()> { 61 | output.write_u32::(1000)?; 62 | output.write_i32::(self.game_data_set.into())?; 63 | output.write_u32::(self.dependencies.len() as u32)?; 64 | for dlc_id in &self.dependencies { 65 | output.write_i32::((*dlc_id).into())?; 66 | } 67 | Ok(()) 68 | } 69 | } 70 | 71 | #[derive(Debug, Clone)] 72 | pub struct SCXHeader { 73 | /// Version of the header. 74 | /// 75 | /// Versions 2 and up include a save timestamp. 76 | /// Versions 3 and up contain HD Edition DLC information. 77 | pub version: u32, 78 | /// Unix timestamp when this scenario was created, in seconds. 79 | pub timestamp: u32, 80 | /// Description text about the scenario. 81 | pub description: Option, 82 | /// Name of the person who created this scenario. Only available in DE2. 83 | pub author_name: Option, 84 | /// Whether the scenario has any victory conditions for singleplayer. 85 | pub any_sp_victory: bool, 86 | /// How many players are supported by this scenario. 87 | pub active_player_count: u32, 88 | /// HD Edition DLC information. 89 | pub dlc_options: Option, 90 | } 91 | 92 | impl SCXHeader { 93 | /// Parse an SCX header from a byte stream. 94 | pub fn read_from(mut input: impl Read, format_version: SCXVersion) -> Result { 95 | let _header_size = input.read_u32::()?; 96 | let version = input.read_u32::()?; 97 | log::debug!("Header version {}", version); 98 | let timestamp = if version >= 2 { 99 | input.read_u32::()? 100 | } else { 101 | 0 102 | }; 103 | let description = if format_version == *b"3.13" { 104 | input.read_hd_style_str()? 105 | } else { 106 | input.read_u32_length_prefixed_str()? 107 | }; 108 | 109 | let any_sp_victory = input.read_u32::()? != 0; 110 | let active_player_count = input.read_u32::()?; 111 | 112 | let dlc_options = if version > 2 && format_version != *b"3.13" { 113 | Some(DLCOptions::read_from(&mut input)?) 114 | } else { 115 | None 116 | }; 117 | 118 | let author_name; 119 | if version >= 5 { 120 | author_name = input.read_u32_length_prefixed_str()?; 121 | let _num_triggers = input.read_u32::()?; 122 | } else { 123 | author_name = None; 124 | } 125 | 126 | Ok(SCXHeader { 127 | version, 128 | timestamp, 129 | description, 130 | author_name, 131 | any_sp_victory, 132 | active_player_count, 133 | dlc_options, 134 | }) 135 | } 136 | 137 | /// Serialize an SCX header to a byte stream. 138 | pub fn write_to( 139 | &self, 140 | output: impl Write, 141 | format_version: SCXVersion, 142 | version: u32, 143 | ) -> Result<()> { 144 | let mut intermediate = vec![]; 145 | 146 | intermediate.write_u32::(version)?; 147 | 148 | if version >= 2 { 149 | intermediate.write_u32::(self.timestamp)?; 150 | } 151 | 152 | let mut description_bytes = vec![]; 153 | if let Some(ref description) = self.description { 154 | description_bytes.write_all(description.as_bytes())?; 155 | } 156 | description_bytes.push(0); 157 | if format_version == *b"3.13" { 158 | assert!( 159 | description_bytes.len() <= std::u16::MAX as usize, 160 | "description length must fit in u16" 161 | ); 162 | intermediate.write_u16::(description_bytes.len() as u16)?; 163 | } else { 164 | assert!( 165 | description_bytes.len() <= std::u32::MAX as usize, 166 | "description length must fit in u32" 167 | ); 168 | intermediate.write_u32::(description_bytes.len() as u32)?; 169 | } 170 | intermediate.write_all(&description_bytes)?; 171 | 172 | intermediate.write_u32::(u32::from(self.any_sp_victory))?; 173 | intermediate.write_u32::(self.active_player_count)?; 174 | 175 | if version > 2 && format_version != *b"3.13" { 176 | let def = DLCOptions::default(); 177 | let dlc_options = match self.dlc_options { 178 | Some(ref options) => options, 179 | None => &def, 180 | }; 181 | dlc_options.write_to(&mut intermediate)?; 182 | } 183 | 184 | if version >= 5 { 185 | write_opt_i32_str(&mut intermediate, &self.author_name)?; 186 | // TODO should be number of triggers 187 | intermediate.write_u32::(0)?; 188 | } 189 | 190 | // Make `output` mutable here so we don't accidentally use it above. 191 | let mut output = output; 192 | output.write_u32::(intermediate.len() as u32)?; 193 | output.write_all(&intermediate)?; 194 | 195 | Ok(()) 196 | } 197 | 198 | /// Update the timestamp. 199 | pub fn touch(&mut self) -> std::result::Result<(), std::time::SystemTimeError> { 200 | let system_time = std::time::SystemTime::now(); 201 | let duration = system_time.duration_since(std::time::UNIX_EPOCH)?; 202 | self.timestamp = duration.as_secs() as u32; 203 | Ok(()) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /crates/genie-scx/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A reader, writer, and converter for all versions of Age of Empires scenarios. 2 | //! 3 | //! This crate aims to support every single scenario that exists. If a scenario file from any Age 4 | //! of Empires 1 or Age of Empires 2 version does not work, please upload it and file an issue! 5 | 6 | #![deny(future_incompatible)] 7 | #![deny(nonstandard_style)] 8 | #![deny(rust_2018_idioms)] 9 | #![deny(unsafe_code)] 10 | #![warn(unused)] 11 | #![allow(missing_docs)] 12 | 13 | mod ai; 14 | mod bitmap; 15 | pub mod convert; 16 | mod format; 17 | mod header; 18 | mod map; 19 | mod player; 20 | mod triggers; 21 | mod types; 22 | mod victory; 23 | 24 | use format::SCXFormat; 25 | use genie_support::{ReadStringError, WriteStringError}; 26 | use std::io::{self, Read, Write}; 27 | 28 | pub use format::{ScenarioObject, TribeScen}; 29 | pub use genie_support::{DecodeStringError, EncodeStringError}; 30 | pub use genie_support::{StringKey, UnitTypeID}; 31 | pub use header::{DLCOptions, SCXHeader}; 32 | pub use map::{Map, Tile}; 33 | pub use player::{ScenarioPlayerData, WorldPlayerData}; 34 | pub use triggers::{Trigger, TriggerCondition, TriggerEffect, TriggerSystem}; 35 | pub use types::*; 36 | pub use victory::{VictoryConditions, VictoryEntry, VictoryPointEntry, VictoryState}; 37 | 38 | /// Error type for SCX methods, containing all types of errors that may occur while reading or 39 | /// writing scenario files. 40 | #[derive(Debug, thiserror::Error)] 41 | pub enum Error { 42 | /// The scenario that's attempted to be read does not contain a file name. 43 | #[error("must have a file name")] 44 | MissingFileNameError, 45 | /// Attempted to read a scenario with an unsupported format version identifier. 46 | #[error("unsupported format version {:?}", .0)] 47 | UnsupportedFormatVersionError(SCXVersion), 48 | /// Attempted to write a scenario with disabled technologies, to a version that doesn't support 49 | /// this many disabled technologies. 50 | #[error("too many disabled techs: got {}, but requested version supports up to 20", .0)] 51 | TooManyDisabledTechsError(i32), 52 | /// Attempted to write a scenario with disabled technologies, to a version that doesn't support 53 | /// disabling technologies. 54 | #[error("requested version does not support disabling techs")] 55 | CannotDisableTechsError, 56 | /// Attempted to write a scenario with disabled units, to a version that doesn't support 57 | /// disabling units. 58 | #[error("requested version does not support disabling units")] 59 | CannotDisableUnitsError, 60 | /// Attempted to write a scenario with disabled buildings, to a version that doesn't support 61 | /// this many disabled buildings. 62 | #[error("too many disabled buildings: got {}, but requested version supports up to {}", .0, .1)] 63 | TooManyDisabledBuildingsError(i32, i32), 64 | /// Attempted to write a scenario with disabled buildings, to a version that doesn't support 65 | /// disabling buildings. 66 | #[error("requested version does not support disabling buildings")] 67 | CannotDisableBuildingsError, 68 | /// Failed to decode a string from the scenario file, probably because of a wrong encoding. 69 | #[error(transparent)] 70 | DecodeStringError(#[from] DecodeStringError), 71 | /// Failed to encode a string into the scenario file, probably because of a wrong encoding. 72 | #[error(transparent)] 73 | EncodeStringError(#[from] EncodeStringError), 74 | /// The given ID is not a known diplomatic stance. 75 | #[error(transparent)] 76 | ParseDiplomaticStanceError(#[from] ParseDiplomaticStanceError), 77 | /// The given ID is not a known data set. 78 | #[error(transparent)] 79 | ParseDataSetError(#[from] ParseDataSetError), 80 | /// The given ID is not a known HD Edition DLC. 81 | #[error(transparent)] 82 | ParseDLCPackageError(#[from] ParseDLCPackageError), 83 | /// The given ID is not a known starting age in AoE1 or AoE2. 84 | #[error(transparent)] 85 | ParseStartingAgeError(#[from] ParseStartingAgeError), 86 | /// The given ID is not a known error code. 87 | #[error(transparent)] 88 | ParseAIErrorCodeError(#[from] num_enum::TryFromPrimitiveError), 89 | /// The given ID is not a known victory condition state. 90 | #[error(transparent)] 91 | ParseVictoryConditionStateError(#[from] num_enum::TryFromPrimitiveError), 92 | /// An error occurred while reading or writing. 93 | #[error(transparent)] 94 | IoError(#[from] io::Error), 95 | } 96 | 97 | impl From for Error { 98 | fn from(err: ReadStringError) -> Error { 99 | match err { 100 | ReadStringError::IoError(err) => Error::IoError(err), 101 | ReadStringError::DecodeStringError(err) => Error::DecodeStringError(err), 102 | } 103 | } 104 | } 105 | 106 | impl From for Error { 107 | fn from(err: WriteStringError) -> Error { 108 | match err { 109 | WriteStringError::IoError(err) => Error::IoError(err), 110 | WriteStringError::EncodeStringError(err) => Error::EncodeStringError(err), 111 | } 112 | } 113 | } 114 | 115 | /// Result type for SCX methods. 116 | pub type Result = std::result::Result; 117 | 118 | /// A Scenario file. 119 | #[derive(Debug, Clone)] 120 | pub struct Scenario { 121 | format: SCXFormat, 122 | version: VersionBundle, 123 | } 124 | 125 | impl Scenario { 126 | /// Read a scenario file. 127 | pub fn read_from(input: impl Read) -> Result { 128 | let format = SCXFormat::load_scenario(input)?; 129 | let version = format.version(); 130 | 131 | Ok(Self { format, version }) 132 | } 133 | 134 | /// Read a scenario file. 135 | #[deprecated = "Use Scenario::read_from instead."] 136 | pub fn from(input: &mut R) -> Result { 137 | Self::read_from(input) 138 | } 139 | 140 | /// Write the scenario file to an output stream. 141 | /// 142 | /// Equivalent to `scen.write_to_version(scen.version())`. 143 | pub fn write_to(&self, output: impl Write) -> Result<()> { 144 | self.format.write_to(output, self.version()) 145 | } 146 | 147 | /// Write the scenario file to an output stream, targeting specific game versions. 148 | pub fn write_to_version(&self, output: impl Write, version: &VersionBundle) -> Result<()> { 149 | self.format.write_to(output, version) 150 | } 151 | 152 | /// Get the format version of this SCX file. 153 | #[inline] 154 | pub fn format_version(&self) -> SCXVersion { 155 | self.version().format 156 | } 157 | 158 | /// Get the header version for this SCX file. 159 | #[inline] 160 | pub fn header_version(&self) -> u32 { 161 | self.version().header 162 | } 163 | 164 | /// Get the data version for this SCX file. 165 | #[inline] 166 | pub fn data_version(&self) -> f32 { 167 | self.version().data 168 | } 169 | 170 | /// Get the header. 171 | #[inline] 172 | pub fn header(&self) -> &SCXHeader { 173 | &self.format.header 174 | } 175 | 176 | /// Get the scenario description. 177 | #[inline] 178 | pub fn description(&self) -> Option<&str> { 179 | self.format.tribe_scen.description() 180 | } 181 | 182 | /// Get the scenario filename. 183 | #[inline] 184 | pub fn filename(&self) -> &str { 185 | &self.format.tribe_scen.base.name 186 | } 187 | 188 | /// Get data about the game versions this scenario file was made for. 189 | #[inline] 190 | pub fn version(&self) -> &VersionBundle { 191 | &self.version 192 | } 193 | 194 | /// Check if this scenario requires the given DLC (for HD Edition scenarios only). 195 | #[inline] 196 | pub fn requires_dlc(&self, dlc: DLCPackage) -> bool { 197 | match &self.header().dlc_options { 198 | Some(options) => options.dependencies.iter().any(|dep| *dep == dlc), 199 | None => false, 200 | } 201 | } 202 | 203 | /// Get the UserPatch mod name of the mod that was used to create this scenario. 204 | /// 205 | /// This returns the short name, like "WK" for WololoKingdoms or "aoc" for Age of Chivalry. 206 | #[inline] 207 | pub fn mod_name(&self) -> Option<&str> { 208 | self.format.mod_name() 209 | } 210 | 211 | /// Iterate over all the objects placed in the scenario. 212 | #[inline] 213 | pub fn objects(&self) -> impl Iterator { 214 | self.format 215 | .player_objects 216 | .iter() 217 | .flat_map(|list| list.iter()) 218 | } 219 | 220 | /// Iterate mutably over all the objects placed in the scenario. 221 | #[inline] 222 | pub fn objects_mut(&mut self) -> impl Iterator { 223 | self.format 224 | .player_objects 225 | .iter_mut() 226 | .flat_map(|list| list.iter_mut()) 227 | } 228 | 229 | pub fn world_players(&self) -> &[WorldPlayerData] { 230 | &self.format.world_players 231 | } 232 | 233 | pub fn scenario_players(&self) -> &[ScenarioPlayerData] { 234 | &self.format.scenario_players 235 | } 236 | 237 | /// Get the map/terrain data for this scenario. 238 | #[inline] 239 | pub fn map(&self) -> &Map { 240 | &self.format.map 241 | } 242 | 243 | /// Get the (mutable) map/terrain data for this scenario. 244 | #[inline] 245 | pub fn map_mut(&mut self) -> &mut Map { 246 | &mut self.format.map 247 | } 248 | 249 | /// Get trigger data for this scenario if it exists. 250 | #[inline] 251 | pub fn triggers(&self) -> Option<&TriggerSystem> { 252 | self.format.triggers.as_ref() 253 | } 254 | 255 | /// Get (mutable) trigger data for this scenario if it exists. 256 | #[inline] 257 | pub fn triggers_mut(&mut self) -> Option<&mut TriggerSystem> { 258 | self.format.triggers.as_mut() 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/genie-scx/src/map.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 3 | use genie_support::read_opt_u16; 4 | use std::io::{self, Read, Write}; 5 | 6 | /// A map tile. 7 | #[derive(Debug, Default, Clone, Copy)] 8 | pub struct Tile { 9 | /// The terrain. 10 | pub terrain: u8, 11 | /// Terrain type layered on top of this tile, if any. 12 | pub layered_terrain: Option, 13 | /// The elevation level. 14 | pub elevation: i8, 15 | /// Unused? 16 | pub zone: i8, 17 | /// Definitive Edition 2 value, not sure what it does, only seen `-1` in the wild so far 18 | mask_type: Option, 19 | } 20 | 21 | impl Tile { 22 | /// Read a tile from an input stream. 23 | pub fn read_from(mut input: impl Read, version: u32) -> Result { 24 | let mut tile = Self { 25 | terrain: input.read_u8()?, 26 | layered_terrain: None, 27 | elevation: input.read_i8()?, 28 | zone: input.read_i8()?, 29 | mask_type: None, 30 | }; 31 | if version >= 1 { 32 | tile.mask_type = read_opt_u16(&mut input)?; 33 | tile.layered_terrain = read_opt_u16(&mut input)?; 34 | } 35 | Ok(tile) 36 | } 37 | 38 | /// Write a tile to an output stream. 39 | pub fn write_to(&self, mut output: impl Write, version: u32) -> Result<()> { 40 | output.write_u8(self.terrain)?; 41 | output.write_i8(self.elevation)?; 42 | output.write_i8(self.zone)?; 43 | 44 | if version >= 1 { 45 | output.write_u16::(self.mask_type.unwrap_or(0xFFFF))?; 46 | output.write_u16::(self.layered_terrain.unwrap_or(0xFFFF))?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | /// Describes the terrain in a map. 54 | #[derive(Debug, Clone)] 55 | pub struct Map { 56 | /// Version of the map data format. 57 | version: u32, 58 | /// Width of this map in tiles. 59 | width: u32, 60 | /// Height of this map in tiles. 61 | height: u32, 62 | /// Should waves be rendered? 63 | render_waves: bool, 64 | /// Matrix of tiles on this map. 65 | tiles: Vec, 66 | } 67 | 68 | impl Map { 69 | /// Create a new, empty map. 70 | pub fn new(width: u32, height: u32) -> Self { 71 | Self { 72 | version: 0, 73 | width, 74 | height, 75 | render_waves: true, 76 | tiles: vec![Default::default(); (width * height) as usize], 77 | } 78 | } 79 | 80 | /// Fill the map with the given terrain type. 81 | pub fn fill(&mut self, terrain_type: u8) { 82 | for tile in self.tiles.iter_mut() { 83 | tile.terrain = terrain_type; 84 | } 85 | } 86 | 87 | /// Read map/terrain data from an input stream. 88 | pub fn read_from(mut input: impl Read) -> Result { 89 | let mut map = match input.read_u32::()? { 90 | 0xDEADF00D => { 91 | let version = input.read_u32::()?; 92 | let render_waves = if version < 2 { 93 | true 94 | } else { 95 | // Stored as a boolean `true` indicating "do not render waves", so flip it to 96 | // make it indicate "render waves? true/false". 97 | input.read_u8()? == 0 98 | }; 99 | 100 | let width = input.read_u32::()?; 101 | let height = input.read_u32::()?; 102 | 103 | Map { 104 | version, 105 | width, 106 | height, 107 | render_waves, 108 | tiles: vec![], 109 | } 110 | } 111 | width => { 112 | let height = input.read_u32::()?; 113 | 114 | Map { 115 | version: 0, 116 | width, 117 | height, 118 | render_waves: true, 119 | tiles: vec![], 120 | } 121 | } 122 | }; 123 | 124 | log::debug!("Map size: {}×{}", map.width, map.height); 125 | 126 | if map.width > 500 || map.height > 500 { 127 | return Err(io::Error::new( 128 | io::ErrorKind::Other, 129 | format!( 130 | "Unexpected map size {}×{}, this is likely a genie-scx bug.", 131 | map.width, map.height 132 | ), 133 | ) 134 | .into()); 135 | } 136 | 137 | map.tiles.reserve((map.height * map.height) as usize); 138 | for _ in 0..map.height { 139 | for _ in 0..map.width { 140 | map.tiles.push(Tile::read_from(&mut input, map.version)?); 141 | } 142 | } 143 | 144 | Ok(map) 145 | } 146 | 147 | /// Write map/terrain data to an output stream. 148 | pub fn write_to(&self, mut output: impl Write, version: u32) -> Result<()> { 149 | if version != 0 { 150 | output.write_u32::(0xDEADF00D)?; 151 | output.write_u32::(version)?; 152 | } 153 | if version >= 2 { 154 | output.write_u8(u8::from(!self.render_waves))?; 155 | } 156 | 157 | output.write_u32::(self.width)?; 158 | output.write_u32::(self.height)?; 159 | 160 | assert_eq!(self.tiles.len(), (self.height * self.width) as usize); 161 | 162 | for tile in &self.tiles { 163 | tile.write_to(&mut output, version)?; 164 | } 165 | 166 | Ok(()) 167 | } 168 | 169 | /// Get the version of the map data. 170 | pub fn version(&self) -> u32 { 171 | self.version 172 | } 173 | 174 | /// Get the width of the map. 175 | pub fn width(&self) -> u32 { 176 | self.width 177 | } 178 | 179 | /// Get the height of the map. 180 | pub fn height(&self) -> u32 { 181 | self.height 182 | } 183 | 184 | /// Get a tile at the given coordinates. 185 | /// 186 | /// If the coordinates are out of bounds, returns None. 187 | pub fn tile(&self, x: u32, y: u32) -> Option<&Tile> { 188 | self.tiles.get((y * self.width + x) as usize) 189 | } 190 | 191 | /// Get a mutable reference to the tile at the given coordinates. 192 | /// 193 | /// If the coordinates are out of bounds, returns None. 194 | pub fn tile_mut(&mut self, x: u32, y: u32) -> Option<&mut Tile> { 195 | self.tiles.get_mut((y * self.width + x) as usize) 196 | } 197 | 198 | /// Iterate over all the tiles. 199 | pub fn tiles(&self) -> impl Iterator { 200 | self.tiles.iter() 201 | } 202 | 203 | /// Iterate over all the tiles, with mutable references. 204 | /// 205 | /// This is handy if you want to replace terrains throughout the entire map, for example. 206 | pub fn tiles_mut(&mut self) -> impl Iterator { 207 | self.tiles.iter_mut() 208 | } 209 | 210 | /// Iterate over all the tiles by row. 211 | /// 212 | /// This is handy if you want to iterate over tiles while keeping track of coordinates. 213 | /// 214 | /// ## Example 215 | /// ```rust 216 | /// # use genie_scx::{Map, Tile}; 217 | /// # let map = Map::new(120, 120); 218 | /// let mut ys = vec![]; 219 | /// for (y, row) in map.rows().enumerate() { 220 | /// let mut xs = vec![]; 221 | /// for (x, tile) in row.iter().enumerate() { 222 | /// xs.push(x); 223 | /// } 224 | /// assert_eq!(xs, (0..120).collect::>()); 225 | /// ys.push(y); 226 | /// } 227 | /// assert_eq!(ys, (0..120).collect::>()); 228 | /// ``` 229 | pub fn rows(&self) -> impl Iterator { 230 | self.tiles.chunks_exact(self.width as usize) 231 | } 232 | 233 | /// Iterate over all the tiles by row, with mutable references. 234 | pub fn rows_mut(&mut self) -> impl Iterator { 235 | self.tiles.chunks_exact_mut(self.width as usize) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /crates/genie-scx/src/player.rs: -------------------------------------------------------------------------------- 1 | use crate::victory::VictoryConditions; 2 | use crate::Result; 3 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 4 | use genie_support::{write_opt_str, ReadStringsExt}; 5 | use std::io::{Read, Write}; 6 | 7 | #[derive(Debug, Default, Clone)] 8 | pub struct PlayerBaseProperties { 9 | pub(crate) posture: i32, 10 | pub(crate) player_type: i32, 11 | pub(crate) civilization: i32, 12 | pub(crate) active: i32, 13 | } 14 | 15 | #[derive(Debug, Default, Clone)] 16 | pub struct PlayerFiles { 17 | /// Obsolete. 18 | pub(crate) build_list: Option, 19 | /// Obsolete. 20 | pub(crate) city_plan: Option, 21 | /// String content of the AI of this player. 22 | pub(crate) ai_rules: Option, 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct PlayerStartResources { 27 | pub(crate) gold: i32, 28 | pub(crate) wood: i32, 29 | pub(crate) food: i32, 30 | pub(crate) stone: i32, 31 | pub(crate) ore: i32, 32 | pub(crate) goods: i32, 33 | pub(crate) player_color: Option, 34 | } 35 | 36 | impl Default for PlayerStartResources { 37 | fn default() -> Self { 38 | Self { 39 | gold: 100, 40 | wood: 200, 41 | food: 200, 42 | stone: 200, 43 | ore: 100, 44 | goods: 0, 45 | player_color: None, 46 | } 47 | } 48 | } 49 | 50 | impl PlayerStartResources { 51 | pub fn read_from(mut input: impl Read, version: f32) -> Result { 52 | Ok(Self { 53 | gold: input.read_i32::()?, 54 | wood: input.read_i32::()?, 55 | food: input.read_i32::()?, 56 | stone: input.read_i32::()?, 57 | ore: if version >= 1.17 { 58 | input.read_i32::()? 59 | } else { 60 | 100 61 | }, 62 | goods: if version >= 1.17 { 63 | input.read_i32::()? 64 | } else { 65 | 0 66 | }, 67 | player_color: if version >= 1.24 { 68 | Some(input.read_i32::()?) 69 | } else { 70 | None 71 | }, 72 | }) 73 | } 74 | 75 | pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> { 76 | output.write_i32::(self.gold)?; 77 | output.write_i32::(self.wood)?; 78 | output.write_i32::(self.food)?; 79 | output.write_i32::(self.stone)?; 80 | if version >= 1.17 { 81 | output.write_i32::(self.ore)?; 82 | output.write_i32::(self.goods)?; 83 | } 84 | if version >= 1.24 { 85 | output.write_i32::(self.player_color.unwrap_or(0))?; 86 | } 87 | Ok(()) 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone)] 92 | pub struct ScenarioPlayerData { 93 | pub name: Option, 94 | pub view: (f32, f32), 95 | pub location: (i16, i16), 96 | pub allied_victory: bool, 97 | pub relations: Vec, 98 | pub unit_diplomacy: Vec, 99 | pub color: Option, 100 | pub victory: VictoryConditions, 101 | } 102 | 103 | impl ScenarioPlayerData { 104 | /// Get the default player name. 105 | pub fn name(&self) -> Option<&str> { 106 | self.name.as_ref().map(|string| string.as_ref()) 107 | } 108 | 109 | /// Set the default player name. 110 | pub fn set_name(&mut self, name: impl Into) { 111 | self.name = Some(name.into()); 112 | } 113 | 114 | /// Read player data from an input stream. 115 | pub fn read_from(mut input: impl Read, version: f32) -> Result { 116 | let name = input.read_u16_length_prefixed_str()?; 117 | 118 | let view = (input.read_f32::()?, input.read_f32::()?); 119 | 120 | let location = (input.read_i16::()?, input.read_i16::()?); 121 | 122 | let allied_victory = if version > 1.0 { 123 | input.read_u8()? != 0 124 | } else { 125 | false 126 | }; 127 | 128 | let diplo_count = input.read_i16::()?; 129 | let mut relations = Vec::with_capacity(diplo_count as usize); 130 | for _ in 0..diplo_count { 131 | relations.push(input.read_i8()?); 132 | } 133 | 134 | let unit_diplomacy = if version >= 1.08 { 135 | vec![ 136 | input.read_i32::()?, 137 | input.read_i32::()?, 138 | input.read_i32::()?, 139 | input.read_i32::()?, 140 | input.read_i32::()?, 141 | input.read_i32::()?, 142 | input.read_i32::()?, 143 | input.read_i32::()?, 144 | input.read_i32::()?, 145 | ] 146 | } else { 147 | vec![0, 0, 0, 0, 0, 0, 0, 0, 0] 148 | }; 149 | 150 | let color = if version >= 1.13 { 151 | Some(input.read_i32::()?) 152 | } else { 153 | None 154 | }; 155 | 156 | let victory = VictoryConditions::read_from(&mut input, version >= 1.09)?; 157 | 158 | Ok(ScenarioPlayerData { 159 | name, 160 | view, 161 | location, 162 | allied_victory, 163 | relations, 164 | unit_diplomacy, 165 | color, 166 | victory, 167 | }) 168 | } 169 | 170 | /// Write player data to an output stream. 171 | pub fn write_to( 172 | &self, 173 | mut output: impl Write, 174 | version: f32, 175 | victory_version: f32, 176 | ) -> Result<()> { 177 | write_opt_str(&mut output, &self.name)?; 178 | 179 | output.write_f32::(self.view.0)?; 180 | output.write_f32::(self.view.1)?; 181 | 182 | output.write_i16::(self.location.0)?; 183 | output.write_i16::(self.location.1)?; 184 | 185 | if version > 1.0 { 186 | output.write_u8(u8::from(self.allied_victory))?; 187 | }; 188 | 189 | output.write_i16::(self.relations.len() as i16)?; 190 | for rel in &self.relations { 191 | output.write_i8(*rel)?; 192 | } 193 | 194 | if version >= 1.08 { 195 | output.write_i32::(self.unit_diplomacy[0])?; 196 | output.write_i32::(self.unit_diplomacy[1])?; 197 | output.write_i32::(self.unit_diplomacy[2])?; 198 | output.write_i32::(self.unit_diplomacy[3])?; 199 | output.write_i32::(self.unit_diplomacy[4])?; 200 | output.write_i32::(self.unit_diplomacy[5])?; 201 | output.write_i32::(self.unit_diplomacy[6])?; 202 | output.write_i32::(self.unit_diplomacy[7])?; 203 | output.write_i32::(self.unit_diplomacy[8])?; 204 | } 205 | 206 | if version >= 1.13 { 207 | output.write_i32::(self.color.unwrap_or(-1))?; 208 | } 209 | 210 | self.victory.write_to( 211 | &mut output, 212 | if version >= 1.09 { 213 | Some(victory_version) 214 | } else { 215 | None 216 | }, 217 | )?; 218 | 219 | Ok(()) 220 | } 221 | } 222 | 223 | /// Initial player attributes. 224 | #[derive(Debug, Clone)] 225 | pub struct WorldPlayerData { 226 | /// Initial food count. 227 | pub(crate) food: f32, 228 | /// Initial wood count. 229 | pub(crate) wood: f32, 230 | /// Initial gold count. 231 | pub(crate) gold: f32, 232 | /// Initial stone count. 233 | pub(crate) stone: f32, 234 | /// Initial ore count. (unused, except in some mods) 235 | pub(crate) ore: f32, 236 | /// Initial trade goods count. (unused) 237 | pub(crate) goods: f32, 238 | /// Max population. 239 | pub(crate) population: f32, 240 | } 241 | 242 | impl Default for WorldPlayerData { 243 | fn default() -> Self { 244 | Self { 245 | food: 200.0, 246 | wood: 200.0, 247 | gold: 100.0, 248 | stone: 200.0, 249 | ore: 100.0, 250 | goods: 0.0, 251 | population: 75.0, 252 | } 253 | } 254 | } 255 | 256 | impl WorldPlayerData { 257 | pub fn read_from(mut input: impl Read, version: f32) -> Result { 258 | Ok(Self { 259 | food: if version > 1.06 { 260 | input.read_f32::()? 261 | } else { 262 | 200.0 263 | }, 264 | wood: if version > 1.06 { 265 | input.read_f32::()? 266 | } else { 267 | 200.0 268 | }, 269 | gold: if version > 1.06 { 270 | input.read_f32::()? 271 | } else { 272 | 50.0 273 | }, 274 | stone: if version > 1.06 { 275 | input.read_f32::()? 276 | } else { 277 | 100.0 278 | }, 279 | ore: if version > 1.12 { 280 | input.read_f32::()? 281 | } else { 282 | 100.0 283 | }, 284 | goods: if version > 1.12 { 285 | input.read_f32::()? 286 | } else { 287 | 0.0 288 | }, 289 | population: if version >= 1.14 { 290 | input.read_f32::()? 291 | } else { 292 | 75.0 293 | }, 294 | }) 295 | } 296 | 297 | pub fn write_to(&self, mut output: impl Write, version: f32) -> Result<()> { 298 | if version > 1.06 { 299 | output.write_f32::(self.food)?; 300 | output.write_f32::(self.wood)?; 301 | output.write_f32::(self.gold)?; 302 | output.write_f32::(self.stone)?; 303 | } 304 | if version > 1.12 { 305 | output.write_f32::(self.ore)?; 306 | } 307 | if version > 1.12 { 308 | output.write_f32::(self.goods)?; 309 | } 310 | if version >= 1.14 { 311 | output.write_f32::(self.population)?; 312 | } 313 | Ok(()) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/ The Destruction of Rome.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/ The Destruction of Rome.scn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/A New Emporer.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/A New Emporer.scn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Age of Heroes b1-3-5.scx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Age of Heroes b1-3-5.scx -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Bronze Age Art of War.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Bronze Age Art of War.scn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/CAMELOT.SCN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/CAMELOT.SCN -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/CEASAR.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/CEASAR.scn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Corlis.aoescn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Corlis.aoescn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Dawn of a New Age.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Dawn of a New Age.scn -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/El advenimiento de los hunos_.scx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/El advenimiento de los hunos_.scx -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Hotkey Trainer Buildings.aoe2scenario: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Hotkey Trainer Buildings.aoe2scenario -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Jeremiah Johnson (Update).scx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Jeremiah Johnson (Update).scx -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/Year_of_the_Pig.aoe2scenario: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/Year_of_the_Pig.aoe2scenario -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/layertest.aoe2scenario: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/layertest.aoe2scenario -------------------------------------------------------------------------------- /crates/genie-scx/test/scenarios/real_world_amazon.scx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiegeEngineers/genie-rs/a77200aef567b40b7db51cf47c1fda8db75e8e67/crates/genie-scx/test/scenarios/real_world_amazon.scx -------------------------------------------------------------------------------- /crates/genie-support/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genie-support" 3 | version = "1.0.0" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Support library for genie-* crates" 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/genie-support" 10 | documentation = "https://docs.rs/genie-support" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | 14 | [dependencies] 15 | byteorder.workspace = true 16 | encoding_rs = { version = "0.8.31", optional = true } 17 | thiserror.workspace = true 18 | 19 | [features] 20 | strings = ["encoding_rs"] 21 | 22 | [dev-dependencies] 23 | anyhow.workspace = true 24 | -------------------------------------------------------------------------------- /crates/genie-support/README.md: -------------------------------------------------------------------------------- 1 | # genie-support 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-genie--support-blue?style=flat-square&color=blue)](https://docs.rs/genie-support) 4 | [![crates.io](https://img.shields.io/crates/v/genie-support.svg?style=flat-square&color=orange)](https://crates.io/crates/genie-support) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Support library for `genie-*` crates. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /crates/genie-support/src/ids.rs: -------------------------------------------------------------------------------- 1 | use crate::{fallible_try_from, fallible_try_into, infallible_try_into}; 2 | use std::convert::{TryFrom, TryInto}; 3 | use std::fmt; 4 | use std::num::TryFromIntError; 5 | 6 | /// An ID identifying a unit type. 7 | #[derive(Debug, Hash, Default, Clone, Copy, PartialEq, Eq)] 8 | pub struct UnitTypeID(u16); 9 | 10 | impl From for UnitTypeID { 11 | #[inline] 12 | fn from(n: u16) -> Self { 13 | UnitTypeID(n) 14 | } 15 | } 16 | 17 | impl From for u16 { 18 | #[inline] 19 | fn from(n: UnitTypeID) -> Self { 20 | n.0 21 | } 22 | } 23 | 24 | impl From for i32 { 25 | #[inline] 26 | fn from(n: UnitTypeID) -> Self { 27 | n.0.into() 28 | } 29 | } 30 | 31 | impl From for u32 { 32 | #[inline] 33 | fn from(n: UnitTypeID) -> Self { 34 | n.0.into() 35 | } 36 | } 37 | 38 | impl From for usize { 39 | #[inline] 40 | fn from(n: UnitTypeID) -> Self { 41 | n.0.into() 42 | } 43 | } 44 | 45 | fallible_try_into!(UnitTypeID, i16); 46 | fallible_try_from!(UnitTypeID, i16); 47 | fallible_try_from!(UnitTypeID, i32); 48 | fallible_try_from!(UnitTypeID, u32); 49 | 50 | /// An ID identifying a tech. 51 | #[derive(Debug, Hash, Default, Clone, Copy, PartialEq, Eq)] 52 | pub struct TechID(u16); 53 | 54 | impl From for TechID { 55 | fn from(n: u16) -> Self { 56 | TechID(n) 57 | } 58 | } 59 | 60 | impl From for u16 { 61 | fn from(n: TechID) -> Self { 62 | n.0 63 | } 64 | } 65 | 66 | impl From for usize { 67 | fn from(n: TechID) -> Self { 68 | n.0.into() 69 | } 70 | } 71 | 72 | fallible_try_into!(TechID, i16); 73 | infallible_try_into!(TechID, u32); 74 | fallible_try_from!(TechID, i32); 75 | fallible_try_from!(TechID, u32); 76 | 77 | /// An ID identifying a sprite. 78 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 79 | pub struct SpriteID(u16); 80 | impl From for SpriteID { 81 | fn from(n: u16) -> Self { 82 | SpriteID(n) 83 | } 84 | } 85 | 86 | impl From for u16 { 87 | fn from(n: SpriteID) -> Self { 88 | n.0 89 | } 90 | } 91 | 92 | impl From for i32 { 93 | fn from(n: SpriteID) -> Self { 94 | n.0.into() 95 | } 96 | } 97 | 98 | impl From for u32 { 99 | fn from(n: SpriteID) -> Self { 100 | n.0.into() 101 | } 102 | } 103 | 104 | impl From for usize { 105 | fn from(n: SpriteID) -> Self { 106 | n.0.into() 107 | } 108 | } 109 | 110 | fallible_try_into!(SpriteID, i16); 111 | fallible_try_from!(SpriteID, i16); 112 | fallible_try_from!(SpriteID, i32); 113 | fallible_try_from!(SpriteID, u32); 114 | 115 | /// A key in a language file. 116 | /// 117 | /// A key may be either a nonnegative integer or an arbitrary string. 118 | /// 119 | /// The original game supports only nonnegative integers. 120 | /// The HD Edition allows for integers as well as Strings to serve as keys in a 121 | /// key value file. 122 | #[derive(Debug, Hash, Clone, PartialEq, Eq)] 123 | pub enum StringKey { 124 | /// An integer string key. 125 | Num(u32), 126 | 127 | /// A named string key. 128 | /// The string must not represent a `u32` value (such keys must be `Num`). 129 | Name(String), 130 | } 131 | 132 | impl Default for StringKey { 133 | #[inline] 134 | fn default() -> Self { 135 | Self::Num(0) 136 | } 137 | } 138 | 139 | impl StringKey { 140 | /// Returns `true` if and only if this `StringKey` is a number. 141 | /// 142 | /// # Examples 143 | /// 144 | /// ``` 145 | /// use genie_support::StringKey; 146 | /// use std::convert::TryFrom; 147 | /// assert!(StringKey::try_from(0).unwrap().is_numeric()); 148 | /// assert!(!StringKey::from("").is_numeric()); 149 | /// ``` 150 | #[inline] 151 | pub fn is_numeric(&self) -> bool { 152 | match self { 153 | Self::Num(_) => true, 154 | Self::Name(_) => false, 155 | } 156 | } 157 | 158 | /// Returns `true` if and only if this `StringKey` is a string name. 159 | /// 160 | /// # Examples 161 | /// 162 | /// ``` 163 | /// use genie_support::StringKey; 164 | /// use std::convert::TryFrom; 165 | /// assert!(!StringKey::try_from(0).unwrap().is_named()); 166 | /// assert!(StringKey::from("").is_named()); 167 | /// ``` 168 | #[inline] 169 | pub fn is_named(&self) -> bool { 170 | match self { 171 | Self::Num(_) => false, 172 | Self::Name(_) => true, 173 | } 174 | } 175 | } 176 | 177 | impl fmt::Display for StringKey { 178 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 | match self { 180 | Self::Num(n) => write!(f, "{}", n), 181 | Self::Name(s) => write!(f, "{}", s), 182 | } 183 | } 184 | } 185 | 186 | impl From for StringKey { 187 | #[inline] 188 | fn from(n: u32) -> Self { 189 | Self::Num(n) 190 | } 191 | } 192 | 193 | impl From for StringKey { 194 | #[inline] 195 | fn from(n: u16) -> Self { 196 | Self::Num(n.into()) 197 | } 198 | } 199 | 200 | impl TryFrom for StringKey { 201 | type Error = TryFromIntError; 202 | #[inline] 203 | fn try_from(n: i32) -> Result { 204 | u32::try_from(n).map(Self::Num) 205 | } 206 | } 207 | 208 | impl TryFrom for StringKey { 209 | type Error = TryFromIntError; 210 | #[inline] 211 | fn try_from(n: i16) -> Result { 212 | u32::try_from(n).map(Self::Num) 213 | } 214 | } 215 | 216 | impl From<&str> for StringKey { 217 | #[inline] 218 | fn from(s: &str) -> Self { 219 | if let Ok(n) = s.parse() { 220 | Self::Num(n) 221 | } else { 222 | Self::Name(String::from(s)) 223 | } 224 | } 225 | } 226 | 227 | impl From for StringKey { 228 | #[inline] 229 | fn from(s: String) -> Self { 230 | Self::from(s.as_ref()) 231 | } 232 | } 233 | 234 | /// Error that may occur when converting a StringKey to some other Rust value, such as an integer 235 | /// or a string. 236 | /// 237 | /// When converting to an integer, this means that the StringKey is a named key, or it has a 238 | /// numeric value that is out of range for the target type. 239 | /// 240 | /// When converting to a string, this does not happen, as numeric keys will be converted to 241 | /// strings. 242 | #[derive(Debug, Clone, thiserror::Error)] 243 | #[error("could not convert StringKey to the wanted integer size")] 244 | pub struct TryFromStringKeyError; 245 | 246 | // Implement TryFrom<&StringKey> conversions for a bunch of stuff 247 | macro_rules! try_from_string_key { 248 | ($type:ty) => { 249 | impl TryFrom<&StringKey> for $type { 250 | type Error = TryFromStringKeyError; 251 | #[inline] 252 | fn try_from(key: &StringKey) -> Result { 253 | match key { 254 | StringKey::Num(n) => (*n).try_into().map_err(|_| TryFromStringKeyError), 255 | _ => Err(TryFromStringKeyError), 256 | } 257 | } 258 | } 259 | }; 260 | } 261 | 262 | try_from_string_key!(u32); 263 | try_from_string_key!(i32); 264 | try_from_string_key!(u16); 265 | try_from_string_key!(i16); 266 | 267 | #[cfg(test)] 268 | mod tests { 269 | use super::*; 270 | 271 | /// Tests converting from an int to a string key. 272 | #[test] 273 | fn string_key_from_int() { 274 | if let StringKey::Num(n) = StringKey::try_from(0).unwrap() { 275 | assert_eq!(0, n); 276 | } else { 277 | panic!(); 278 | } 279 | } 280 | 281 | /// Tests converting from a string representing an int to a string key. 282 | #[test] 283 | fn string_key_from_str_to_int() { 284 | let s = "57329"; 285 | if let StringKey::Num(n) = StringKey::from(s) { 286 | assert_eq!(57329, n); 287 | } else { 288 | panic!(); 289 | } 290 | } 291 | 292 | /// Tests converting from a string not representing an int to a string key. 293 | #[test] 294 | fn string_key_from_str_to_str() { 295 | let s = "grassDaut"; 296 | if let StringKey::Name(n) = StringKey::from(s) { 297 | assert_eq!(s, n); 298 | } else { 299 | panic!(); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /crates/genie-support/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Internal utilities for genie-rs modules. 2 | 3 | #![deny(future_incompatible)] 4 | #![deny(nonstandard_style)] 5 | #![deny(rust_2018_idioms)] 6 | #![deny(unsafe_code)] 7 | #![warn(unused)] 8 | #![allow(missing_docs)] 9 | 10 | mod ids; 11 | mod macros; 12 | mod map_into; 13 | mod read; 14 | #[cfg(feature = "strings")] 15 | mod strings; 16 | 17 | pub use ids::*; 18 | pub use macros::*; 19 | pub use map_into::*; 20 | pub use read::*; 21 | #[cfg(feature = "strings")] 22 | pub use strings::*; 23 | -------------------------------------------------------------------------------- /crates/genie-support/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | /// Create an infallible TryInto implementation for an ID container type that returns its contained number as the given target type. 3 | /// 4 | /// 5 | /// ## Example 6 | /// 7 | /// ```rust 8 | /// use genie_support::infallible_try_into; 9 | /// use std::convert::TryInto; 10 | /// struct Container(u16); 11 | /// infallible_try_into!(Container, u32); 12 | /// let num: u32 = Container(12).try_into().unwrap(); 13 | /// assert_eq!(num, 12u32); 14 | /// ``` 15 | macro_rules! infallible_try_into { 16 | ($from:ident, $to:ty) => { 17 | impl ::std::convert::TryFrom<$from> for $to { 18 | type Error = ::std::convert::Infallible; 19 | fn try_from(n: $from) -> ::std::result::Result { 20 | use ::std::convert::TryInto; 21 | n.0.try_into() 22 | } 23 | } 24 | }; 25 | } 26 | 27 | #[macro_export] 28 | /// Create a TryInto implementation for an ID container type that tries to returns its contained number as the given target type. 29 | /// 30 | /// ## Example 31 | /// 32 | /// ```rust 33 | /// use genie_support::fallible_try_into; 34 | /// use std::convert::{TryFrom, TryInto}; 35 | /// struct Container(u16); 36 | /// fallible_try_into!(Container, i16); 37 | /// let num: i16 = Container(12).try_into().unwrap(); 38 | /// assert_eq!(num, 12i16); 39 | /// assert!(i16::try_from(Container(50000u16)).is_err()); 40 | /// ``` 41 | macro_rules! fallible_try_into { 42 | ($from:ident, $to:ty) => { 43 | impl ::std::convert::TryFrom<$from> for $to { 44 | type Error = ::std::num::TryFromIntError; 45 | #[inline] 46 | fn try_from(n: $from) -> ::std::result::Result { 47 | use ::std::convert::TryInto; 48 | n.0.try_into() 49 | } 50 | } 51 | }; 52 | } 53 | 54 | #[macro_export] 55 | /// Create a TryFrom implementation for an ID container type that tries to wrap the given number 56 | /// type into the container. 57 | /// 58 | /// ## Example 59 | /// 60 | /// ```rust 61 | /// use genie_support::fallible_try_from; 62 | /// use std::convert::{TryFrom, TryInto}; 63 | /// #[derive(Debug, PartialEq, Eq)] 64 | /// struct Container(u16); 65 | /// fallible_try_from!(Container, i16); 66 | /// assert_eq!(Container::try_from(1i16).unwrap(), Container(1)); 67 | /// assert!(Container::try_from(-1i16).is_err()); 68 | /// ``` 69 | macro_rules! fallible_try_from { 70 | ($to:ty, $from:ident) => { 71 | impl ::std::convert::TryFrom<$from> for $to { 72 | type Error = ::std::num::TryFromIntError; 73 | #[inline] 74 | fn try_from(n: $from) -> ::std::result::Result { 75 | use ::std::convert::TryInto; 76 | n.try_into().map(Self) 77 | } 78 | } 79 | }; 80 | } 81 | 82 | #[macro_export] 83 | /// Check if two 32 bit floating point numbers are equal, with some error. 84 | /// 85 | /// ```rust 86 | /// use genie_support::f32_eq; 87 | /// let zero = 0.0; 88 | /// assert!(f32_eq!(zero, 0.0)); 89 | /// assert!(!f32_eq!(zero, 1.0)); 90 | /// ``` 91 | macro_rules! f32_eq { 92 | ($left:expr, $right:expr) => { 93 | f32::abs($left - $right) < std::f32::EPSILON 94 | }; 95 | } 96 | 97 | #[macro_export] 98 | /// Check if two 32 bit floating point numbers are not equal, with some error. 99 | /// 100 | /// ```rust 101 | /// use genie_support::f32_neq; 102 | /// let zero = 0.0; 103 | /// assert!(f32_neq!(zero, 1.0)); 104 | /// assert!(!f32_neq!(zero, 0.0)); 105 | /// ``` 106 | macro_rules! f32_neq { 107 | ($left:expr, $right:expr) => { 108 | f32::abs($left - $right) > std::f32::EPSILON 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /crates/genie-support/src/map_into.rs: -------------------------------------------------------------------------------- 1 | /// Helper trait to map a container of T to a container of some type F that implements From. 2 | /// 3 | /// For example, Result to Result>, or Option to Option>. 4 | pub trait MapInto { 5 | /// Map the contained `T` into a different type. 6 | /// 7 | /// Essentially, `.map_into()` is the same as doing `.map(|val| val.into())`. 8 | /// 9 | /// **Example** 10 | /// 11 | /// ```rust 12 | /// use genie_support::MapInto; 13 | /// 14 | /// let a: Option = Some(10); 15 | /// let b: Option = a.map_into(); 16 | /// assert_eq!(b, Some(10u16)); 17 | /// ``` 18 | fn map_into(self) -> T; 19 | } 20 | 21 | impl MapInto> for Result 22 | where 23 | Target: From, 24 | { 25 | #[inline] 26 | fn map_into(self) -> Result { 27 | self.map(|v| v.into()) 28 | } 29 | } 30 | 31 | impl MapInto> for Option 32 | where 33 | Target: From, 34 | { 35 | #[inline] 36 | fn map_into(self) -> Option { 37 | self.map(|v| v.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/genie-support/src/read.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{ReadBytesExt, LE}; 2 | use std::convert::{TryFrom, TryInto}; 3 | use std::io::{self, Error, ErrorKind, Read, Result}; 4 | 5 | /// Read a 2-byte integer that uses -1 as an "absent" value. 6 | /// 7 | /// ## Example 8 | /// 9 | /// ```rust 10 | /// use genie_support::read_opt_u16; 11 | /// 12 | /// let mut minus_one = std::io::Cursor::new(vec![0xFF, 0xFF]); 13 | /// let mut zero = std::io::Cursor::new(vec![0x00, 0x00]); 14 | /// 15 | /// assert_eq!(read_opt_u16::(&mut minus_one).unwrap(), None); 16 | /// assert_eq!(read_opt_u16(&mut zero).unwrap(), Some(0)); 17 | /// ``` 18 | #[inline] 19 | pub fn read_opt_u16(input: &mut R) -> Result> 20 | where 21 | T: TryFrom, 22 | T::Error: std::error::Error + Send + Sync + 'static, 23 | R: Read, 24 | { 25 | let opt = match input.read_u16::()? { 26 | 0xFFFF => None, 27 | v => Some( 28 | v.try_into() 29 | .map_err(|e| Error::new(ErrorKind::InvalidData, e))?, 30 | ), 31 | }; 32 | Ok(opt) 33 | } 34 | 35 | /// Read a 4-byte integer that uses -1 as an "absent" value. 36 | /// 37 | /// ## Example 38 | /// 39 | /// ```rust 40 | /// use genie_support::read_opt_u32; 41 | /// 42 | /// let mut minus_one = std::io::Cursor::new(vec![0xFF, 0xFF, 0xFF, 0xFF]); 43 | /// let mut one = std::io::Cursor::new(vec![0x01, 0x00, 0x00, 0x00]); 44 | /// 45 | /// assert_eq!(read_opt_u32::(&mut minus_one).unwrap(), None); 46 | /// assert_eq!(read_opt_u32(&mut one).unwrap(), Some(1)); 47 | /// ``` 48 | #[inline] 49 | pub fn read_opt_u32(input: &mut R) -> Result> 50 | where 51 | T: TryFrom, 52 | T::Error: std::error::Error + Send + Sync + 'static, 53 | R: Read, 54 | { 55 | let opt = match input.read_u32::()? { 56 | 0xFFFF_FFFF => None, 57 | // HD Edition uses -2 in some places. 58 | 0xFFFF_FFFE => None, 59 | v => Some( 60 | v.try_into() 61 | .map_err(|e| Error::new(ErrorKind::InvalidData, e))?, 62 | ), 63 | }; 64 | Ok(opt) 65 | } 66 | 67 | /// Extension trait that adds a `skip()` method to `Read` instances. 68 | pub trait ReadSkipExt { 69 | /// Read and discard a number of bytes. 70 | fn skip(&mut self, dist: u64) -> Result<()>; 71 | } 72 | 73 | impl ReadSkipExt for T { 74 | fn skip(&mut self, dist: u64) -> Result<()> { 75 | io::copy(&mut self.by_ref().take(dist), &mut io::sink())?; 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/genie-support/src/strings.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{ReadBytesExt, WriteBytesExt, LE}; 2 | use encoding_rs::WINDOWS_1252; 3 | use std::io::{self, Read, Write}; 4 | 5 | /// Failed to decode a string as WINDOWS-1252. 6 | /// 7 | /// This means that the scenario file contained a string that could not be decoded using the 8 | /// WINDOWS-1252 code page. In the future, genie-scx will support other encodings. 9 | #[derive(Debug, Clone, Copy, thiserror::Error)] 10 | #[error("could not decode string as WINDOWS-1252")] 11 | pub struct DecodeStringError; 12 | 13 | /// Failed to encode a string as WINDOWS-1252. 14 | /// 15 | /// This means that a string could not be encoded using the WINDOWS-1252 code page. In the future, genie-scx will support other encodings. 16 | #[derive(Debug, Clone, Copy, thiserror::Error)] 17 | #[error("could not encode string as WINDOWS-1252")] 18 | pub struct EncodeStringError; 19 | 20 | /// Failed to read a string. 21 | #[derive(Debug, thiserror::Error)] 22 | pub enum ReadStringError { 23 | /// Failed to read a string because the bytes could not be decoded. 24 | #[error(transparent)] 25 | DecodeStringError(#[from] DecodeStringError), 26 | /// Failed to read a string because the underlying I/O failed. 27 | #[error(transparent)] 28 | IoError(#[from] io::Error), 29 | } 30 | 31 | /// Failed to write a string. 32 | #[derive(Debug, thiserror::Error)] 33 | pub enum WriteStringError { 34 | /// Failed to read a string because it could not be encoded. 35 | #[error(transparent)] 36 | EncodeStringError(#[from] EncodeStringError), 37 | /// Failed to write a string because the underlying I/O failed. 38 | #[error(transparent)] 39 | IoError(#[from] std::io::Error), 40 | } 41 | 42 | /// Write a string to an output stream, using code page 1252, using a `u16` for the length prefix. 43 | /// 44 | /// This writes the length of the string (including NULL terminator) as a little-endian u16, 45 | /// followed by the encoded bytes, followed by a NULL terminator. 46 | pub fn write_str(output: &mut W, string: &str) -> Result<(), WriteStringError> { 47 | let (bytes, _enc, failed) = WINDOWS_1252.encode(string); 48 | if failed { 49 | return Err(WriteStringError::EncodeStringError(EncodeStringError)); 50 | } 51 | assert!(bytes.len() < std::i16::MAX as usize); 52 | output.write_i16::(bytes.len() as i16 + 1)?; 53 | output.write_all(&bytes)?; 54 | output.write_u8(0)?; 55 | Ok(()) 56 | } 57 | 58 | /// Write a string to an output stream, using code page 1252, using a `u32` for the length prefix. 59 | /// 60 | /// This writes the length of the string (including NULL terminator) as a little-endian u177, 61 | /// followed by the encoded bytes, followed by a NULL terminator. 62 | pub fn write_i32_str(output: &mut W, string: &str) -> Result<(), WriteStringError> { 63 | let (bytes, _enc, failed) = WINDOWS_1252.encode(string); 64 | if failed { 65 | return Err(WriteStringError::EncodeStringError(EncodeStringError)); 66 | } 67 | assert!(bytes.len() < std::i32::MAX as usize); 68 | output.write_i32::(bytes.len() as i32 + 1)?; 69 | output.write_all(&bytes)?; 70 | output.write_u8(0)?; 71 | Ok(()) 72 | } 73 | 74 | /// Write a string to an output stream, using code page 1252, using a `u16` for the length prefix. 75 | /// 76 | /// When given a `None`, it outputs a 0 for the length. Otherwise, see `write_str`. 77 | pub fn write_opt_str( 78 | output: &mut W, 79 | option: &Option, 80 | ) -> Result<(), WriteStringError> { 81 | if let Some(ref string) = option { 82 | write_str(output, string) 83 | } else { 84 | output.write_i16::(0)?; 85 | Ok(()) 86 | } 87 | } 88 | 89 | /// Write a string to an output stream, using code page 1252, using a `u32` for the length prefix. 90 | /// 91 | /// When given a `None`, it outputs a 0 for the length. Otherwise, see `write_str`. 92 | pub fn write_opt_i32_str( 93 | output: &mut W, 94 | option: &Option, 95 | ) -> Result<(), WriteStringError> { 96 | if let Some(ref string) = option { 97 | write_i32_str(output, string) 98 | } else { 99 | output.write_i32::(0)?; 100 | Ok(()) 101 | } 102 | } 103 | 104 | /// Decode a string using the WINDOWS-1252 code page. 105 | fn decode_str(bytes: &[u8]) -> Result { 106 | if bytes.is_empty() { 107 | return Ok("".to_string()); 108 | } 109 | 110 | let (decoded, _enc, failed) = WINDOWS_1252.decode(bytes); 111 | if failed { 112 | Err(DecodeStringError) 113 | } else { 114 | Ok(decoded.to_string()) 115 | } 116 | } 117 | 118 | /// Functions to read various kinds of strings from input streams. 119 | /// Extension trait for reading strings in several common formats used by AoE2. 120 | pub trait ReadStringsExt: Read { 121 | /// Read an optionally null-terminated WINDOWS-1252-encoded string with the given `length` in bytes. 122 | fn read_str(&mut self, length: usize) -> Result, ReadStringError> { 123 | if length > 0 { 124 | let mut bytes = vec![0; length as usize]; 125 | self.read_exact(&mut bytes)?; 126 | if let Some(end) = bytes.iter().position(|&byte| byte == 0) { 127 | bytes.truncate(end); 128 | } 129 | if bytes.is_empty() { 130 | Ok(None) 131 | } else { 132 | Ok(Some(decode_str(&bytes)?)) 133 | } 134 | } else { 135 | Ok(None) 136 | } 137 | } 138 | 139 | /// Read an u16 value, then read an optionally null-terminated WINDOWS-1252-encoded string of 140 | /// that length in bytes. 141 | fn read_u16_length_prefixed_str(&mut self) -> Result, ReadStringError> { 142 | match self.read_u16::()? { 143 | 0xFFFF => Ok(None), 144 | len => self.read_str(len as usize), 145 | } 146 | } 147 | 148 | /// Read an u32 value, then read an optionally null-terminated WINDOWS-1252-encoded string of 149 | /// that length in bytes. 150 | fn read_u32_length_prefixed_str(&mut self) -> Result, ReadStringError> { 151 | match self.read_u32::()? { 152 | 0xFFFF_FFFF => Ok(None), 153 | len => self.read_str(len as usize), 154 | } 155 | } 156 | 157 | /// Read an HD Edition style string. 158 | /// 159 | /// Reads a 'signature' value, then the `length` as an u16 value, then reads an optionally 160 | /// null-terminated WINDOWS-1252-encoded string of that length in bytes. 161 | fn read_hd_style_str(&mut self) -> Result, ReadStringError> { 162 | let open = self.read_u16::()?; 163 | // Check that this actually is the start of a string 164 | if open != 0x0A60 { 165 | return Err(DecodeStringError.into()); 166 | } 167 | let len = self.read_u16::()? as usize; 168 | let mut bytes = vec![0; len]; 169 | self.read_exact(&mut bytes[0..len])?; 170 | Ok(Some(decode_str(&bytes)?)) 171 | } 172 | } 173 | 174 | impl ReadStringsExt for T where T: Read {} 175 | -------------------------------------------------------------------------------- /crates/jascpal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jascpal" 3 | version = "0.1.1" 4 | rust-version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | description = "Read and write JASC palette files." 9 | homepage = "https://github.com/SiegeEngineers/genie-rs/tree/default/crates/jascpal" 10 | documentation = "https://docs.rs/jascpal" 11 | repository.workspace = true 12 | readme = "./README.md" 13 | 14 | 15 | [dependencies] 16 | nom = { version = "7.1.1", default-features = false, features = ["std"] } 17 | rgb.workspace = true 18 | thiserror.workspace = true 19 | 20 | [dev-dependencies] 21 | anyhow.workspace = true 22 | -------------------------------------------------------------------------------- /crates/jascpal/README.md: -------------------------------------------------------------------------------- 1 | # jascpal 2 | 3 | [![docs.rs](https://img.shields.io/badge/docs.rs-jascpal-blue?style=flat-square&color=blue)](https://docs.rs/jascpal) 4 | [![crates.io](https://img.shields.io/crates/v/jascpal.svg?style=flat-square&color=orange)](https://crates.io/crates/jascpal) 5 | [![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) 6 | ![MSRV](https://img.shields.io/badge/MSRV-1.64.0%2B-blue?style=flat-square) 7 | 8 | Read and write JASC palette files. 9 | 10 | ## License 11 | 12 | [GPL-3.0](../../LICENSE.md) 13 | -------------------------------------------------------------------------------- /examples/convertscx.rs: -------------------------------------------------------------------------------- 1 | extern crate genie; 2 | extern crate structopt; 3 | 4 | use genie::scx::{convert::AutoToWK, VersionBundle}; 5 | use genie::Scenario; 6 | use std::{fs::File, path::PathBuf}; 7 | use structopt::StructOpt; 8 | 9 | /// Convert Age of Empires scenario files between versions. 10 | #[derive(Debug, StructOpt)] 11 | struct Cli { 12 | /// Input scenario file. 13 | #[structopt(parse(from_os_str))] 14 | input: PathBuf, 15 | /// Output scenario file. 16 | #[structopt(parse(from_os_str))] 17 | output: PathBuf, 18 | /// Scenario version to output: 'aoe', 'ror', 'aoc', 'hd', 'wk' 19 | /// 20 | /// When setting the version to 'wk', HD edition and AoC scenarios will automatically be 21 | /// converted (swapping out unit types and terrains). 22 | version: Option, 23 | } 24 | 25 | fn main() -> anyhow::Result<()> { 26 | let Cli { 27 | input, 28 | output, 29 | version, 30 | } = Cli::from_args(); 31 | let version_arg = version; 32 | let version = match version_arg.as_deref() { 33 | Some("aoe") => VersionBundle::aoe(), 34 | Some("ror") => VersionBundle::ror(), 35 | Some("aoc") => VersionBundle::aoc(), 36 | Some("hd") => VersionBundle::hd_edition(), 37 | Some("wk") => VersionBundle::userpatch_15(), 38 | Some(name) => panic!("unknown version {}", name), 39 | _ => VersionBundle::aoc(), 40 | }; 41 | 42 | let instream = File::open(input)?; 43 | let mut scen = Scenario::read_from(instream)?; 44 | 45 | if version_arg == Some("wk".to_string()) { 46 | println!("Applying WololoKingdoms conversion..."); 47 | let converter = AutoToWK::default(); 48 | converter.convert(&mut scen)?; 49 | } 50 | 51 | let outstream = File::create(output)?; 52 | scen.write_to_version(outstream, &version)?; 53 | 54 | println!("Conversion complete!"); 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /examples/displayaochotkeys.rs: -------------------------------------------------------------------------------- 1 | //! Prints out all AoC hotkey groups and hotkeys, using a language file. 2 | 3 | // Example hotkey file path: 4 | // D:\SteamLibrary\steamapps\common\Age2HD\profiles 5 | 6 | // Example language file path: 7 | // D:\SteamLibrary\steamapps\common\Age2HD\resources\en\strings\key-value\key-value-strings-utf8.txt 8 | 9 | extern crate genie; 10 | extern crate structopt; 11 | 12 | use genie::hki::{self, HotkeyInfo}; 13 | use genie::lang::LangFileType; 14 | use std::fs::File; 15 | use std::path::PathBuf; 16 | use structopt::StructOpt; 17 | 18 | /// Displays hotkeys using a language file. 19 | #[derive(Debug, StructOpt)] 20 | #[structopt(name = "Display Language File")] 21 | struct DisplayLang { 22 | /// The name of the language file. 23 | #[structopt(name = "lang-file-name")] 24 | lang_file_name: PathBuf, 25 | 26 | /// The type of the language file. 27 | /// 28 | /// One of "dll", "ini", or "key-value". 29 | #[structopt(name = "file-type")] 30 | file_type: LangFileType, 31 | 32 | /// The name of the hotkey file. 33 | #[structopt(name = "hki-file-name")] 34 | hki_file_name: PathBuf, 35 | } 36 | 37 | /// Displays the hotkeys from a hotkey file and language file given to `stdout`. 38 | fn main() -> anyhow::Result<()> { 39 | let cli_input = DisplayLang::from_args(); 40 | 41 | let mut f_lang = File::open(&cli_input.lang_file_name)?; 42 | let lang_file = cli_input.file_type.read_from(&mut f_lang)?; 43 | 44 | let mut f_hki = File::open(&cli_input.hki_file_name)?; 45 | let info = HotkeyInfo::from(&mut f_hki)?; 46 | let aoc_him = hki::default_him(); 47 | 48 | println!("{}", info.get_string_from_lang(&lang_file, &aoc_him)); 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/displaycivs.rs: -------------------------------------------------------------------------------- 1 | //! Displays civilizations from a specified dat file. 2 | 3 | extern crate genie; 4 | extern crate structopt; 5 | 6 | use genie::DatFile; 7 | use std::fs::File; 8 | use std::path::PathBuf; 9 | use structopt::StructOpt; 10 | 11 | /// Display civilizations from a specified dat file. 12 | #[derive(Debug, StructOpt)] 13 | #[structopt(name = "displaycivs")] 14 | struct DisplayCivs { 15 | /// The name of the dat file. 16 | file_name: PathBuf, 17 | } 18 | 19 | /// Executes the CLI. 20 | fn main() -> anyhow::Result<()> { 21 | let cli_input = DisplayCivs::from_args(); 22 | let mut f = File::open(&cli_input.file_name)?; 23 | let dat = DatFile::read_from(&mut f)?; 24 | for civ in &dat.civilizations { 25 | println!("{}", civ.name()); 26 | } 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /examples/displayhotkey.rs: -------------------------------------------------------------------------------- 1 | //! Displays a hotkey to `stdout`. 2 | 3 | extern crate genie; 4 | extern crate structopt; 5 | 6 | use genie::hki::HotkeyInfo; 7 | use std::fs::File; 8 | use std::path::PathBuf; 9 | use structopt::StructOpt; 10 | 11 | // Example hotkey file path: 12 | // D:\SteamLibrary\steamapps\common\Age2HD\profiles 13 | 14 | // Example language file path: 15 | // D:\SteamLibrary\steamapps\common\Age2HD\resources\en\strings\key-value\key-value-strings-utf8.txt 16 | 17 | /// Displays an individual hotkey from a hotkey file. 18 | #[derive(Debug, StructOpt)] 19 | #[structopt(name = "Set Hotkey")] 20 | struct DisplayHotkey { 21 | /// The name of the hotkey file. 22 | #[structopt(name = "file-name")] 23 | file_name: PathBuf, 24 | 25 | /// The group index of the hotkey to display. 26 | #[structopt(name = "group-index")] 27 | group_index: usize, 28 | 29 | /// The index of the hotkey within the group. 30 | #[structopt(name = "hotkey-index")] 31 | hotkey_index: usize, 32 | } 33 | 34 | /// Executes the CLI. 35 | fn main() -> anyhow::Result<()> { 36 | let cli_input = DisplayHotkey::from_args(); 37 | let mut f = File::open(&cli_input.file_name)?; 38 | let info = HotkeyInfo::from(&mut f)?; 39 | let group = info.group(cli_input.group_index).unwrap(); 40 | let hotkey = group.hotkey(cli_input.hotkey_index).unwrap(); 41 | println!("{}", hotkey); 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/displaylang.rs: -------------------------------------------------------------------------------- 1 | //! Displays the key value pairs in a language file. 2 | 3 | extern crate genie; 4 | extern crate structopt; 5 | 6 | use genie::lang::LangFileType; 7 | use std::fs::File; 8 | use std::path::PathBuf; 9 | use structopt::StructOpt; 10 | 11 | /// Displays the strings from a language file. 12 | #[derive(Debug, StructOpt)] 13 | #[structopt(name = "Display Language File")] 14 | struct DisplayLang { 15 | /// The name of the language file. 16 | #[structopt(name = "file-name")] 17 | file_name: PathBuf, 18 | 19 | /// The type of the language file. 20 | /// 21 | /// One of "dll", "ini", or "key-value". 22 | #[structopt(name = "file-type")] 23 | file_type: LangFileType, 24 | } 25 | 26 | /// Prints the key value pairs of an input language file to `stdout`. 27 | fn main() -> anyhow::Result<()> { 28 | let cli_input = DisplayLang::from_args(); 29 | let mut f = File::open(&cli_input.file_name)?; 30 | let lang_file = cli_input.file_type.read_from(&mut f)?; 31 | println!("{}", lang_file); 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /examples/extractcpx.rs: -------------------------------------------------------------------------------- 1 | extern crate genie; 2 | extern crate structopt; 3 | 4 | use genie::Campaign; 5 | use std::{cmp, fs::File, path::PathBuf}; 6 | use structopt::StructOpt; 7 | 8 | #[derive(Debug, StructOpt)] 9 | struct List { 10 | /// Campaign file. 11 | #[structopt(parse(from_os_str))] 12 | input: PathBuf, 13 | } 14 | 15 | #[derive(Debug, StructOpt)] 16 | struct Extract { 17 | /// Campaign file. 18 | #[structopt(parse(from_os_str))] 19 | input: PathBuf, 20 | /// Output folder, defaults to cwd. 21 | #[structopt(parse(from_os_str))] 22 | output: Option, 23 | } 24 | 25 | #[derive(Debug, StructOpt)] 26 | #[structopt(name = "extractcpx", about = "Campaign file manager")] 27 | enum Cli { 28 | /// List the scenario files in the campaign file. 29 | #[structopt(name = "list")] 30 | List(List), 31 | /// Extract scenario files from the campaign file. 32 | #[structopt(name = "extract")] 33 | Extract(Extract), 34 | } 35 | 36 | fn list(args: List) { 37 | let f = File::open(args.input).expect("could not open file"); 38 | let mut campaign = Campaign::from(f).expect("not a campaign file"); 39 | 40 | println!("Name: {}", campaign.name()); 41 | println!("Version: {}", String::from_utf8_lossy(&campaign.version())); 42 | println!("Scenarios: ({})", campaign.len()); 43 | 44 | let names = campaign 45 | .entries() 46 | .map(|entry| entry.filename.to_string()) 47 | .collect::>(); 48 | 49 | (0..campaign.len()).for_each(|i| { 50 | let bytes = campaign.by_index_raw(i).expect("missing scenario data"); 51 | println!("- {} ({})", names[i], format_bytes(bytes.len() as u32)); 52 | }); 53 | } 54 | 55 | fn extract(args: Extract) { 56 | let dir = args 57 | .output 58 | .unwrap_or_else(|| std::env::current_dir().expect("invalid cwd")); 59 | 60 | let f = File::open(args.input).expect("could not open file"); 61 | let mut campaign = Campaign::from(f).expect("not a campaign file"); 62 | 63 | let names = campaign 64 | .entries() 65 | .map(|entry| entry.filename.to_string()) 66 | .collect::>(); 67 | 68 | (0..campaign.len()).for_each(|i| { 69 | let bytes = campaign.by_index_raw(i).expect("missing scenario data"); 70 | println!("{}", names[i]); 71 | std::fs::write(dir.join(&names[i]), bytes).expect("failed to write"); 72 | }); 73 | } 74 | 75 | /// Derived from https://github.com/banyan/rust-pretty-bytes/blob/master/src/converter.rs 76 | fn format_bytes(num: u32) -> String { 77 | let units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 78 | if num < 1 { 79 | return format!("{} {}", num, "B"); 80 | } 81 | let delimiter = 1000u32; 82 | let exponent = cmp::min( 83 | (f64::from(num).ln() / f64::from(delimiter).ln()).floor() as u32, 84 | (units.len() - 1) as u32, 85 | ); 86 | let pretty_bytes = num / delimiter.pow(exponent); 87 | let unit = units[exponent as usize]; 88 | format!("{:.2} {}", pretty_bytes, unit) 89 | } 90 | 91 | fn main() { 92 | match Cli::from_args() { 93 | Cli::List(args) => list(args), 94 | Cli::Extract(args) => extract(args), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/extractdrs.rs: -------------------------------------------------------------------------------- 1 | extern crate genie; 2 | extern crate genie_drs; 3 | extern crate structopt; 4 | 5 | use genie_drs::{DRSReader, DRSWriter, ReserveDirectoryStrategy}; 6 | use std::collections::HashSet; 7 | use std::fs::{create_dir_all, File}; 8 | use std::io::{self, stdout, Write}; 9 | use std::path::PathBuf; 10 | use structopt::StructOpt; 11 | 12 | #[derive(StructOpt)] 13 | struct Cli { 14 | #[structopt(subcommand)] 15 | command: Command, 16 | } 17 | 18 | #[derive(StructOpt)] 19 | enum Command { 20 | #[structopt(name = "list")] 21 | /// List the resources in 22 | List(List), 23 | #[structopt(name = "get")] 24 | /// Get a single resource by ID. 25 | Get(Get), 26 | #[structopt(name = "extract")] 27 | /// Extract the entire archive to a directory. 28 | Extract(Extract), 29 | #[structopt(name = "add")] 30 | /// Add a resource to an existing archive. 31 | Add(Add), 32 | } 33 | 34 | #[derive(StructOpt)] 35 | struct List { 36 | #[structopt(parse(from_os_str))] 37 | /// Path to the .drs archive. 38 | archive: PathBuf, 39 | } 40 | 41 | #[derive(StructOpt)] 42 | struct Get { 43 | /// Path to the .drs archive. 44 | #[structopt(parse(from_os_str))] 45 | archive: PathBuf, 46 | /// The ID of the resource. 47 | #[structopt(name = "resource")] 48 | resource_id: u32, 49 | } 50 | 51 | #[derive(StructOpt)] 52 | struct Extract { 53 | /// Path to the .drs archive. 54 | #[structopt(parse(from_os_str))] 55 | archive: PathBuf, 56 | /// Only extract resources from this table. 57 | #[structopt(long, short = "t")] 58 | table: Option, 59 | /// Output directory to place the resources in. 60 | #[structopt(long, short = "o", parse(from_os_str))] 61 | out: PathBuf, 62 | } 63 | 64 | #[derive(Debug, StructOpt)] 65 | struct Add { 66 | /// Path to the .drs archive. 67 | #[structopt(parse(from_os_str))] 68 | archive: PathBuf, 69 | /// Path to place the edited .drs archive. If not given, updates the archive in place. 70 | #[structopt(long, short = "o", parse(from_os_str))] 71 | output: Option, 72 | /// Table to add the file to. 73 | #[structopt(long, short = "t", number_of_values = 1)] 74 | table: Vec, 75 | /// ID of the file. 76 | #[structopt(long, short = "i", number_of_values = 1)] 77 | id: Vec, 78 | /// Path to the file to add. `-` for standard input. 79 | #[structopt(parse(from_os_str), default_value = "-")] 80 | file: Vec, 81 | } 82 | 83 | fn list(args: List) -> anyhow::Result<()> { 84 | let mut file = File::open(args.archive)?; 85 | let drs = DRSReader::new(&mut file)?; 86 | 87 | for table in drs.tables() { 88 | for resource in table.resources() { 89 | println!("{}.{}", resource.id, table.resource_ext()); 90 | } 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | fn get(args: Get) -> anyhow::Result<()> { 97 | let mut file = File::open(args.archive)?; 98 | let drs = DRSReader::new(&mut file)?; 99 | 100 | for table in drs.tables() { 101 | if let Some(resource) = table.get_resource(args.resource_id) { 102 | let buf = drs.read_resource(&mut file, table.resource_type, resource.id)?; 103 | stdout().write_all(&buf)?; 104 | return Ok(()); 105 | } 106 | } 107 | 108 | Err(io::Error::new( 109 | io::ErrorKind::NotFound, 110 | "Archive does not contain that resource", 111 | ) 112 | .into()) 113 | } 114 | 115 | fn extract(args: Extract) -> anyhow::Result<()> { 116 | let mut file = File::open(args.archive)?; 117 | let drs = DRSReader::new(&mut file)?; 118 | 119 | create_dir_all(&args.out)?; 120 | 121 | for table in drs.tables() { 122 | let table_ext = table.resource_ext(); 123 | if let Some(ref filter_ext) = args.table { 124 | if &table_ext != filter_ext { 125 | continue; 126 | } 127 | } 128 | 129 | for resource in table.resources() { 130 | let buf = drs.read_resource(&mut file, table.resource_type, resource.id)?; 131 | let mut outfile = 132 | File::create(args.out.join(format!("{}.{}", resource.id, table_ext)))?; 133 | outfile.write_all(&buf)?; 134 | } 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | fn add(args: Add) -> anyhow::Result<()> { 141 | assert_eq!( 142 | args.file.len(), 143 | args.table.len(), 144 | "Must set a --table for every file" 145 | ); 146 | assert_eq!( 147 | args.file.len(), 148 | args.id.len(), 149 | "Must set an --id for every file" 150 | ); 151 | 152 | let mut input = File::open(&args.archive)?; 153 | let drs_read = DRSReader::new(&mut input)?; 154 | 155 | let (tables, files) = drs_read.tables().fold((0, 0), |(tables, files), table| { 156 | (tables + 1, files + table.len() as u32) 157 | }); 158 | let new_tables = args 159 | .table 160 | .iter() 161 | .fold(HashSet::new(), |mut uniq, table| { 162 | uniq.insert(table); 163 | uniq 164 | }) 165 | .len() as u32; 166 | let new_files = args.id.len() as u32; 167 | 168 | use std::time::SystemTime; 169 | let suffix = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 170 | Ok(d) => format!("{}", d.as_secs()), 171 | _ => "temp".to_string(), 172 | }; 173 | let mut temp_out = args.output.as_ref().unwrap_or(&args.archive).clone(); 174 | temp_out.set_file_name(format!( 175 | "{}.{}", 176 | temp_out.file_name().unwrap().to_str().unwrap(), 177 | suffix 178 | )); 179 | 180 | let output = File::create(&temp_out)?; 181 | let mut drs_write = DRSWriter::new( 182 | output, 183 | ReserveDirectoryStrategy::new(tables + new_tables, files + new_files), 184 | )?; 185 | 186 | for t in drs_read.tables() { 187 | for r in t.resources() { 188 | let b = drs_read.get_resource_reader(&mut input, t.resource_type, r.id)?; 189 | drs_write.add(t.resource_type, r.id, b)?; 190 | } 191 | } 192 | 193 | for (i, path) in args.file.iter().enumerate() { 194 | let mut res_type = [0x20; 4]; 195 | let slice = args.table[i].as_bytes(); 196 | res_type[0..slice.len()].copy_from_slice(slice); 197 | res_type.reverse(); 198 | drs_write.add(res_type, args.id[i], File::open(path)?)?; 199 | } 200 | 201 | drs_write.flush()?; 202 | 203 | std::fs::rename( 204 | temp_out, 205 | if let Some(outfile) = args.output { 206 | outfile 207 | } else { 208 | args.archive 209 | }, 210 | )?; 211 | 212 | Ok(()) 213 | } 214 | 215 | fn main() -> anyhow::Result<()> { 216 | let args = Cli::from_args(); 217 | 218 | match args.command { 219 | Command::List(args) => list(args), 220 | Command::Get(args) => get(args), 221 | Command::Extract(args) => extract(args), 222 | Command::Add(args) => add(args), 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /examples/inspectscx.rs: -------------------------------------------------------------------------------- 1 | extern crate genie; 2 | extern crate simplelog; 3 | 4 | use genie::Scenario; 5 | use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode}; 6 | use std::fs::File; 7 | 8 | fn main() { 9 | let log_level = std::env::var("LOG") 10 | .ok() 11 | .and_then(|value| match value.as_str() { 12 | "info" => Some(LevelFilter::Info), 13 | "debug" => Some(LevelFilter::Debug), 14 | "trace" => Some(LevelFilter::Trace), 15 | _ => None, 16 | }) 17 | .unwrap_or(LevelFilter::Warn); 18 | let infile = std::env::args().nth(1).expect("usage: inspectscx "); 19 | 20 | TermLogger::init( 21 | log_level, 22 | Default::default(), 23 | TerminalMode::Mixed, 24 | ColorChoice::Auto, 25 | ) 26 | .unwrap(); 27 | 28 | let f = File::open(infile).expect("could not read file"); 29 | let scen = Scenario::read_from(f).expect("invalid scenario file"); 30 | 31 | println!("Scenario: {}", scen.filename()); 32 | println!("Version:"); 33 | println!(" Format: {}", scen.version().format); 34 | println!(" Header: {}", scen.version().header); 35 | match scen.version().dlc_options { 36 | None => println!(" DLC: absent"), 37 | Some(x) => println!(" DLC: {}", x), 38 | }; 39 | if let Some(mod_name) = scen.mod_name() { 40 | println!(" UP Mod: {}", mod_name); 41 | } 42 | println!(" Data: {}", scen.version().data); 43 | println!(" Victory: {}", scen.version().victory); 44 | println!(" Map: {}", scen.version().map); 45 | match scen.triggers() { 46 | Some(_) => println!(" Triggers: {}", scen.version().triggers.unwrap()), 47 | None => println!(" Triggers: absent"), 48 | }; 49 | println!(); 50 | 51 | println!("Map:"); 52 | println!(" Size: {}x{}", scen.map().width(), scen.map().height()); 53 | } 54 | -------------------------------------------------------------------------------- /examples/recactions.rs: -------------------------------------------------------------------------------- 1 | extern crate genie; 2 | extern crate structopt; 3 | 4 | use genie::RecordedGame; 5 | use std::fs::File; 6 | use std::path::PathBuf; 7 | use structopt::StructOpt; 8 | 9 | /// Print out all the actions stored in a recorded game file body. 10 | #[derive(StructOpt)] 11 | struct Cli { 12 | /// Path to the recorded game file. 13 | filename: PathBuf, 14 | } 15 | 16 | fn main() -> anyhow::Result<()> { 17 | let Cli { filename } = Cli::from_args(); 18 | 19 | let file = File::open(filename)?; 20 | let mut rec = RecordedGame::new(file)?; 21 | for action in rec.actions()? { 22 | println!("{:?}", action?); 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /examples/sethotkey.rs: -------------------------------------------------------------------------------- 1 | //! Sets a hotkey in a hotkey file. 2 | 3 | extern crate genie; 4 | extern crate genie_hki; 5 | extern crate structopt; 6 | 7 | use genie::hki::HotkeyInfo; 8 | use std::fs::File; 9 | use std::path::PathBuf; 10 | use structopt::StructOpt; 11 | 12 | // Example hotkey file path: 13 | // D:\SteamLibrary\steamapps\common\Age2HD\profiles 14 | 15 | // Example language file path: 16 | // D:\SteamLibrary\steamapps\common\Age2HD\resources\en\strings\key-value\key-value-strings-utf8.txt 17 | 18 | /// Sets an individual key binding in a hotkey file. 19 | #[derive(Debug, StructOpt)] 20 | #[structopt(name = "Set Hotkey")] 21 | struct SetHotkey { 22 | /// The name of the hotkey file. 23 | #[structopt(name = "file-name")] 24 | file_name: PathBuf, 25 | 26 | /// The group index of the hotkey to set. 27 | #[structopt(name = "group-index")] 28 | group_index: u32, 29 | 30 | /// The index of the hotkey within the group. 31 | #[structopt(name = "hotkey-index")] 32 | hotkey_index: u32, 33 | 34 | /// The new value of the key binding. 35 | #[structopt(name = "keycode")] 36 | keycode: i32, 37 | 38 | /// Whether control is held while pressing the hotkey. 39 | #[structopt(long = "ctrl", short = "c")] 40 | ctrl: bool, 41 | 42 | /// Whether alt is held while pressing the hotkey. 43 | #[structopt(long = "alt", short = "a")] 44 | alt: bool, 45 | 46 | /// Whether shift is held while pressing the hotkey. 47 | #[structopt(long = "shift", short = "s")] 48 | shift: bool, 49 | } 50 | 51 | /// Executes the CLI. 52 | fn main() -> anyhow::Result<()> { 53 | let cli_input = SetHotkey::from_args(); 54 | let mut f = File::open(&cli_input.file_name)?; 55 | let info = HotkeyInfo::from(&mut f)?; 56 | let info = info.bind_key( 57 | cli_input.group_index as usize, 58 | cli_input.hotkey_index as usize, 59 | cli_input.keycode, 60 | cli_input.ctrl, 61 | cli_input.alt, 62 | cli_input.shift, 63 | )?; 64 | let mut f = File::create(&cli_input.file_name)?; 65 | info.write_to(&mut f)?; 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /examples/wolololang.rs: -------------------------------------------------------------------------------- 1 | //! Converts a key-value language file to an ini language file, removing all 2 | //! strings with a string key name instead of a numeric key name. 3 | //! 4 | //! The order in which string keys are written to the output file is currently 5 | //! unspecified. 6 | 7 | extern crate genie; 8 | extern crate structopt; 9 | 10 | use genie::lang::LangFileType::KeyValue; 11 | use std::fs::File; 12 | use std::path::PathBuf; 13 | use structopt::StructOpt; 14 | 15 | /// Struct to collect input and output file paths from the command line. 16 | #[derive(Debug, StructOpt)] 17 | #[structopt(name = "Wololo Language File")] 18 | struct WololoLang { 19 | /// The path of the input language key-value file. 20 | #[structopt(name = "path-in")] 21 | path_in: PathBuf, 22 | 23 | /// The path of the output language ini file. 24 | /// 25 | /// Overwrites this file if it already exists. 26 | #[structopt(name = "path-out")] 27 | path_out: PathBuf, 28 | } 29 | 30 | /// Collects command line input and converts the specified key-value language 31 | /// file into an ini language file. 32 | fn main() -> anyhow::Result<()> { 33 | let cli_input = WololoLang::from_args(); 34 | let mut f_in = File::open(&cli_input.path_in)?; 35 | let lang_file = KeyValue.read_from(&mut f_in)?; 36 | let mut f_out = File::create(&cli_input.path_out)?; 37 | lang_file.write_to_ini(&mut f_out)?; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Libraries for reading/writing Age of Empires 2 data files. 2 | //! 3 | //! ## Data Files 4 | //! 5 | //! > Supported version range: Age of Empires 2: Age of Kings, Age of Conquerors, HD Edition 6 | //! 7 | //! genie-dat can read data files (empires.dat) for Age of Empires 2. When reading a file, the 8 | //! version is detected automatically, based on the amount of terrains included in the file (since 9 | //! that is hardcoded in each game executable). 10 | //! 11 | //! Writing data files is not yet supported, and many of the things that the library reads are not 12 | //! yet exposed in the public API. 13 | //! 14 | //! ```rust 15 | //! # fn main() -> anyhow::Result<()> { 16 | //! use genie::DatFile; 17 | //! let mut input = std::fs::File::open("./crates/genie-dat/fixtures/aok.dat")?; 18 | //! 19 | //! let dat = DatFile::read_from(&mut input)?; 20 | //! assert_eq!(dat.civilizations.len(), 14); 21 | //! assert_eq!(dat.civilizations[1].name(), "British"); 22 | //! # Ok(()) } 23 | //! ``` 24 | //! 25 | //! ## Scenario Files 26 | //! 27 | //! > Supported version range: AoE1 betas through to Age of Empires 2: HD Edition 28 | //! 29 | //! genie-scx can read and write scenario files for almost all Age of Empires versions. When 30 | //! reading a file, the version is detected automatically. When writing a file, you can choose the 31 | //! version to save it as. For example, you can read an HD Edition scenario file, but save it for 32 | //! AoC 1.0c. Note that scenarios that are converted like this may crash the game, because they may 33 | //! refer to terrains or units that do not exist in the different version. 34 | //! 35 | //! ```rust 36 | //! # fn main() -> anyhow::Result<()> { 37 | //! use genie::Scenario; 38 | //! use genie::scx::VersionBundle; 39 | //! 40 | //! /// Read an AoE1 scenario file 41 | //! let infile = "./crates/genie-scx/test/scenarios/Dawn of a New Age.scn"; 42 | //! let input = std::fs::File::open(infile)?; 43 | //! let output = std::fs::File::create("converted.scx")?; 44 | //! 45 | //! let scen = Scenario::read_from(input)?; 46 | //! scen.write_to_version(output, &VersionBundle::aoc())?; 47 | //! 48 | //! # std::fs::remove_file("converted.scx")?; 49 | //! # Ok(()) } 50 | //! ``` 51 | //! 52 | //! ### Implementation Status 53 | //! 54 | //! There aren't many ways to edit a scenario file yet. Initially, we'll work towards the necessary 55 | //! features for proper conversion between AoE versions, especially HD → WololoKingdoms. When that 56 | //! is fairly robust, we'll work on adding methods to edit scenarios and create them from scratch. 57 | //! 58 | //! ## Campaign Files 59 | //! 60 | //! > Supported version range: all versions 61 | //! 62 | //! Campaign files are archives that contain a bunch of scenario files. genie-cpx can extract 63 | //! scenarios from campaign archives and create new campaign archives. 64 | //! 65 | //! ## Hotkey Files 66 | //! 67 | //! > Supported version range: all versions 68 | //! 69 | //! Hotkey files contain groups of key mappings for different game areas. 70 | //! 71 | //! ## Palette Files 72 | //! 73 | //! > Supported version range: all versions 74 | //! 75 | //! Palette files contain the 256-bit colour palettes used in different areas of the game. Each 76 | //! palette contains up to 256 r, g, b colour values. Both reading and writing is supported. 77 | 78 | #![deny(future_incompatible)] 79 | #![deny(nonstandard_style)] 80 | #![deny(rust_2018_idioms)] 81 | #![deny(unsafe_code)] 82 | #![warn(unused)] 83 | #![allow(missing_docs)] 84 | 85 | pub extern crate genie_cpx; 86 | pub extern crate genie_dat; 87 | pub extern crate genie_drs; 88 | pub extern crate genie_hki; 89 | pub extern crate genie_lang; 90 | pub extern crate genie_rec; 91 | pub extern crate genie_scx; 92 | pub extern crate jascpal; 93 | 94 | pub use genie_cpx as cpx; 95 | pub use genie_dat as dat; 96 | pub use genie_drs as drs; 97 | pub use genie_hki as hki; 98 | pub use genie_lang as lang; 99 | pub use genie_rec as rec; 100 | pub use genie_scx as scx; 101 | pub use jascpal as pal; 102 | 103 | pub use genie_cpx::Campaign; 104 | pub use genie_dat::DatFile; 105 | pub use genie_drs::{DRSReader, DRSWriter}; 106 | pub use genie_hki::HotkeyInfo; 107 | pub use genie_lang::LangFile; 108 | pub use genie_rec::RecordedGame; 109 | pub use genie_scx::Scenario; 110 | pub use jascpal::Palette; 111 | --------------------------------------------------------------------------------