diff --git a/.gitignore b/.gitignore index ea8c4bf..23fb76a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +asar_symbols.txt +base_patch.json +enemizer_base_patch.json diff --git a/Cargo.lock b/Cargo.lock index 664456c..e82f6c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,39 @@ version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base_patch_generator" +version = "0.1.0" +dependencies = [ + "anyhow", + "enemize", + "md5", + "serde_json", +] + [[package]] name = "bin_comp" version = "0.1.0" dependencies = [ "anyhow", - "serde", + "enemize", "serde_json", "tempfile", ] @@ -31,10 +58,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "enemize-rs" +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "enemize" version = "0.1.0" dependencies = [ - "md5", + "anyhow", + "clap", + "lazy_static", + "rand", + "serde", + "serde_json", + "thiserror", ] [[package]] @@ -46,6 +118,48 @@ dependencies = [ "instant", ] +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -61,6 +175,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.126" @@ -73,6 +193,42 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.39" @@ -91,6 +247,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -146,6 +332,12 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.95" @@ -171,12 +363,59 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "winapi" version = "0.3.9" @@ -193,6 +432,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 16d8175..aee92b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "enemize", + "base_patch_generator", "bin_comp", + "enemize", ] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c18e449 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Lyle Mantooth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/base_patch_generator/Cargo.toml b/base_patch_generator/Cargo.toml new file mode 100644 index 0000000..746828c --- /dev/null +++ b/base_patch_generator/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "base_patch_generator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +enemize = { path = "../enemize" } +md5 = "0.7.0" +serde_json = "1.0" diff --git a/base_patch_generator/src/main.rs b/base_patch_generator/src/main.rs new file mode 100644 index 0000000..9d47946 --- /dev/null +++ b/base_patch_generator/src/main.rs @@ -0,0 +1,58 @@ +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use enemize::asar; +use enemize::rom::RomData; +use enemize::PatchSet; + +fn main() -> Result<(), anyhow::Error> { + let mut args = env::args(); + + let output_path = args.next_back().expect("No output file"); + let symbols_path = args.next_back().expect("No symbols file"); + let patch_path = args.next_back().expect("No patch file"); + let rom_path = args.next_back().expect("No ROM file"); + + let mut rom_bytes = vec![]; + let mut rom_file = File::open(&rom_path)?; + rom_file.read_to_end(&mut rom_bytes)?; + + rom_bytes = if rom_bytes.len() % 1024 == 512 { + rom_bytes.into_iter().skip(512).collect() + } else { + rom_bytes + }; + + let expected = [ + 0x03, 0xa6, 0x39, 0x45, 0x39, 0x81, 0x91, 0x33, 0x7e, 0x89, 0x6e, 0x57, 0x71, 0xf7, 0x71, + 0x73, + ]; + let actual: [u8; 16] = md5::compute(&rom_bytes).into(); + assert_eq!(expected, actual, "Invalid rom file"); + + println!("Applying Patch to rom"); + + rom_bytes.resize(4 * 1024 * 1024, 0); + + let mut symbols_file = File::open(&symbols_path)?; + let mut symbols_text = String::new(); + symbols_file.read_to_string(&mut symbols_text)?; + let symbols = asar::load_symbols(&symbols_text)?; + + let mut rom = RomData::new(symbols, rom_bytes); + + let mut patches = PatchSet::load(Path::new(&patch_path))?; + patches.patch_rom(&mut rom); + + rom.move_room_headers(); + + let rom_patches = rom.generate_patch(); + patches.add_patches(rom_patches); + + println!("Writing output file {}", output_path); + patches.save_to_file(Path::new(&output_path))?; + + Ok(()) +} diff --git a/enemize/Cargo.toml b/enemize/Cargo.toml index c59205e..6a55151 100644 --- a/enemize/Cargo.toml +++ b/enemize/Cargo.toml @@ -1,9 +1,15 @@ [package] -name = "enemize-rs" +name = "enemize" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { version = 1.0, features = ["derive"] } +anyhow = "1" +clap = { version = "3.1.18", features = ["derive"] } +lazy_static = "1.4" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1" diff --git a/enemize/src/asar.rs b/enemize/src/asar.rs new file mode 100644 index 0000000..033f07d --- /dev/null +++ b/enemize/src/asar.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +pub type Symbols = HashMap; + +pub fn load_symbols(contents: &str) -> anyhow::Result { + let symbols = contents + .lines() + .filter_map(|line| { + let mut words = line.split_ascii_whitespace(); + match (words.next(), words.next(), words.next()) { + // Get only two-word lines. + (Some(address), Some(symbol), None) => { + Some((symbol.to_owned(), address.to_owned())) + } + + _ => None, + } + }) + .filter_map(|(symbol, mut address)| { + if let Some(colon_at) = address.find(':') { + address.remove(colon_at); + } + + // Filter out the ones that don't have a hexadecimal number as the first word. + let snes_address = u32::from_str_radix(&address, 16).ok()?; + + let pc_address = snes_to_pc_address(snes_address); + + Some((symbol, pc_address)) + }) + .collect(); + + Ok(symbols) +} + +pub fn snes_to_pc_address(snes: u32) -> usize { + ((snes & 0x7FFF) + ((snes / 2) & 0xFF8000)) as usize +} + +pub fn pc_to_snes_address(pc: usize) -> u32 { + ((pc & 0x7FFF) + ((pc & 0xFF8000) * 2)) as u32 +} diff --git a/enemize/src/bosses/mod.rs b/enemize/src/bosses/mod.rs new file mode 100644 index 0000000..ea7692b --- /dev/null +++ b/enemize/src/bosses/mod.rs @@ -0,0 +1,73 @@ +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +use crate::InvalidEnumError; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[repr(u8)] +#[serde(into = "u8", try_from = "u8")] +pub enum BossType { + Kholdstare, + Moldorm, + Mothula, + Vitreous, + Helmasaur, + Armos, + Lanmola, + Blind, + Arrghus, + Trinexx, + // Don't use these. They are only for manual settings passed in by the randomizer web app. + Agahnim, + Agahnim2, + Ganon, + NoBoss = 255, +} + +impl From for u8 { + fn from(t: BossType) -> u8 { + use BossType::*; + + match t { + Kholdstare => 0, + Moldorm => 1, + Mothula => 2, + Vitreous => 3, + Helmasaur => 4, + Armos => 5, + Lanmola => 6, + Blind => 7, + Arrghus => 8, + Trinexx => 9, + Agahnim => 10, + Agahnim2 => 11, + Ganon => 12, + NoBoss => 255, + } + } +} + +impl TryFrom for BossType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Kholdstare), + 1 => Ok(Self::Moldorm), + 2 => Ok(Self::Mothula), + 3 => Ok(Self::Vitreous), + 4 => Ok(Self::Helmasaur), + 5 => Ok(Self::Armos), + 6 => Ok(Self::Lanmola), + 7 => Ok(Self::Blind), + 8 => Ok(Self::Arrghus), + 9 => Ok(Self::Trinexx), + 10 => Ok(Self::Agahnim), + 11 => Ok(Self::Agahnim2), + 12 => Ok(Self::Ganon), + 255 => Ok(Self::NoBoss), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} diff --git a/enemize/src/constants.rs b/enemize/src/constants.rs new file mode 100644 index 0000000..3becd7a --- /dev/null +++ b/enemize/src/constants.rs @@ -0,0 +1,114 @@ +pub const ROOM_HEADER_BANK_LOCATION: usize = 0x0B5E7; +pub const DUNGEON_HEADER_POINTER_TABLE: usize = 0x271E2; +pub const DUNGEON_SPRITE_POINTER_TABLE: usize = 0x4D62E; +pub const OBJECT_DATA_POINTER_TABLE: usize = 0xF8000; +pub const OVERWORLD_AREA_GRAPHICS_BLOCK: usize = 0x007A81; +pub const OVERWORLD_SPRITE_POINTER_TABLE: usize = 0x04C901; +pub const MOLDORM_EYE_COUNT_ADDRESS_VANILLA: usize = 0x0EDBB3; +pub const MOLDORM_EYE_COUNT_ADDRESS_ENEMIZER: usize = 0x1B0102; +pub const NEW_BOSS_GRAPHICS: usize = 0x1B0000; +pub const RANDOM_SPRITE_GRAPHICS: usize = 0x300000; +pub const ENEMIZER_FILE_LENGTH: usize = 0x200000; +pub const HIDDEN_ENEMY_CHANCE_POOL: usize = 0xD7BBB; + +pub(crate) const ORIGINAL_ROOM_POINTERS: [u8; 640] = [ + 0x62, 0xF4, 0x6C, 0xF4, 0x7A, 0xF4, 0xDD, 0xF5, 0x85, 0xF4, 0x90, 0xF4, 0x90, 0xF4, 0x97, 0xF4, + 0xA2, 0xF4, 0xA9, 0xF4, 0xB5, 0xF4, 0xC0, 0xF4, 0xCB, 0xF4, 0xD8, 0xF4, 0xDF, 0xF4, 0xEA, 0xF4, + 0xEA, 0xF4, 0xF1, 0xF4, 0xFC, 0xF4, 0x03, 0xF5, 0x11, 0xF5, 0x18, 0xF5, 0x23, 0xF5, 0x2E, 0xF5, + 0x73, 0xFC, 0x3A, 0xF5, 0x41, 0xF5, 0x4D, 0xF5, 0x58, 0xF5, 0x63, 0xF5, 0x6E, 0xF5, 0x79, 0xF5, + 0x84, 0xF5, 0x8B, 0xF5, 0x8B, 0xF5, 0x03, 0xF5, 0x92, 0xF5, 0x99, 0xF5, 0x99, 0xF5, 0xA6, 0xF5, + 0xB2, 0xF5, 0xBD, 0xF5, 0xC4, 0xF5, 0xCB, 0xF5, 0x73, 0xFC, 0xD6, 0xF5, 0xD6, 0xF5, 0xDD, 0xF5, + 0xE4, 0xF5, 0xEF, 0xF5, 0xFB, 0xF5, 0x06, 0xF6, 0x0D, 0xF6, 0x18, 0xF6, 0x1F, 0xF6, 0x18, 0xF6, + 0x26, 0xF6, 0x31, 0xF6, 0x3B, 0xF6, 0x46, 0xF6, 0x51, 0xF6, 0x58, 0xF6, 0x63, 0xF6, 0x6E, 0xF6, + 0x7A, 0xF6, 0x86, 0xF6, 0x91, 0xF6, 0x9D, 0xF6, 0xA4, 0xF6, 0xAB, 0xF6, 0xB6, 0xF6, 0xBD, 0xF6, + 0xBD, 0xF6, 0xBD, 0xF6, 0xC4, 0xF6, 0xD0, 0xF6, 0xDA, 0xF6, 0xE5, 0xF6, 0xF0, 0xF6, 0xFB, 0xF6, + 0x05, 0xF7, 0x13, 0xF7, 0x1E, 0xF7, 0x2C, 0xF7, 0x37, 0xF7, 0x42, 0xF7, 0x49, 0xF7, 0x50, 0xF7, + 0x57, 0xF7, 0x5E, 0xF7, 0x65, 0xF7, 0x6C, 0xF7, 0x73, 0xF7, 0x7E, 0xF7, 0x89, 0xF7, 0x94, 0xF7, + 0xA0, 0xF7, 0xA7, 0xF7, 0xA0, 0xF7, 0xB2, 0xF7, 0xBD, 0xF7, 0xC8, 0xF7, 0xD2, 0xF7, 0xDD, 0xF7, + 0xE4, 0xF7, 0xEB, 0xF7, 0xEB, 0xF7, 0xF7, 0xF7, 0x02, 0xF8, 0x0D, 0xF8, 0x14, 0xF8, 0x1F, 0xF8, + 0x1F, 0xF8, 0x2B, 0xF8, 0x36, 0xF8, 0x41, 0xF8, 0x48, 0xF8, 0x4F, 0xF8, 0x56, 0xF8, 0x63, 0xF8, + 0x70, 0xF8, 0x70, 0xF8, 0x70, 0xF8, 0x70, 0xF8, 0x7A, 0xF8, 0x81, 0xF8, 0x8B, 0xF8, 0x96, 0xF8, + 0xA1, 0xF8, 0xAC, 0xF8, 0xAC, 0xF8, 0xB3, 0xF8, 0xBA, 0xF8, 0xC1, 0xF8, 0xC8, 0xF8, 0xC8, 0xF8, + 0xD4, 0xF8, 0xD4, 0xF8, 0xDE, 0xF8, 0xDE, 0xF8, 0xE5, 0xF8, 0xF2, 0xF8, 0xF9, 0xF8, 0x04, 0xF9, + 0x04, 0xF9, 0x0B, 0xF9, 0x16, 0xF9, 0x1D, 0xF9, 0x28, 0xF9, 0x28, 0xF9, 0x2F, 0xF9, 0x3A, 0xF9, + 0x45, 0xF9, 0x50, 0xF9, 0x5B, 0xF9, 0x5B, 0xF9, 0x65, 0xF9, 0x6C, 0xF9, 0x76, 0xF9, 0x81, 0xF9, + 0x88, 0xF9, 0x93, 0xF9, 0x9A, 0xF9, 0x93, 0xF9, 0xA5, 0xF9, 0xAC, 0xF9, 0xB7, 0xF9, 0xC2, 0xF9, + 0xCC, 0xF9, 0xD3, 0xF9, 0xDD, 0xF9, 0xE4, 0xF9, 0xEF, 0xF9, 0xF6, 0xF9, 0xF6, 0xF9, 0x01, 0xFA, + 0x08, 0xFA, 0x14, 0xFA, 0x1E, 0xFA, 0x25, 0xFA, 0x2C, 0xFA, 0x37, 0xFA, 0x42, 0xFA, 0x0A, 0xF5, + 0x4D, 0xFA, 0x54, 0xFA, 0x5B, 0xFA, 0x62, 0xFA, 0x69, 0xFA, 0x74, 0xFA, 0x74, 0xFA, 0x7F, 0xFA, + 0x86, 0xFA, 0x92, 0xFA, 0x99, 0xFA, 0xA0, 0xFA, 0xA7, 0xFA, 0xB2, 0xFA, 0x0A, 0xF5, 0xB9, 0xFA, + 0xC0, 0xFA, 0xC7, 0xFA, 0xCE, 0xFA, 0xCE, 0xFA, 0xCE, 0xFA, 0xD5, 0xFA, 0xD5, 0xFA, 0xDF, 0xFA, + 0xDF, 0xFA, 0xEB, 0xFA, 0xF6, 0xFA, 0x01, 0xFB, 0x01, 0xFB, 0xB2, 0xFA, 0x0A, 0xF5, 0x01, 0xFB, + 0x01, 0xFB, 0x08, 0xFB, 0x0F, 0xFB, 0xCE, 0xFA, 0xCE, 0xFA, 0x1A, 0xFB, 0x1A, 0xFB, 0x21, 0xFB, + 0x2C, 0xFB, 0x37, 0xFB, 0x3E, 0xFB, 0x45, 0xFB, 0x4C, 0xFB, 0x4C, 0xFB, 0x53, 0xFB, 0x53, 0xFB, + 0x5A, 0xFB, 0x68, 0xFB, 0x68, 0xFB, 0x73, 0xFB, 0x7E, 0xFB, 0x7E, 0xFB, 0x8A, 0xFB, 0x94, 0xFB, + 0x53, 0xFB, 0x53, 0xFB, 0xA0, 0xFB, 0xA0, 0xFB, 0xA5, 0xFB, 0xA5, 0xFB, 0xAC, 0xFB, 0xAC, 0xFB, + 0xAC, 0xFB, 0xBA, 0xFB, 0xC1, 0xFB, 0xCC, 0xFB, 0xD7, 0xFB, 0xD7, 0xFB, 0xBA, 0xFB, 0xE3, 0xFB, + 0xEE, 0xFB, 0xFC, 0xFB, 0x03, 0xFC, 0x0A, 0xFC, 0x11, 0xFC, 0x18, 0xFC, 0x1F, 0xFC, 0x26, 0xFC, + 0x2D, 0xFC, 0x34, 0xFC, 0x3B, 0xFC, 0x42, 0xFC, 0x49, 0xFC, 0x50, 0xFC, 0x57, 0xFC, 0xF5, 0xFB, + 0xF5, 0xFB, 0x5E, 0xFC, 0x65, 0xFC, 0x6C, 0xFC, 0x73, 0xFC, 0x73, 0xFC, 0x7A, 0xFC, 0x81, 0xFC, + 0x0A, 0xFC, 0x88, 0xFC, 0x93, 0xFC, 0x9A, 0xFC, 0xF5, 0xFB, 0xA1, 0xFC, 0xAC, 0xFC, 0xB3, 0xFC, + 0xBA, 0xFC, 0x5E, 0xFC, 0x5E, 0xFC, 0xC1, 0xFC, 0xC8, 0xFC, 0xC8, 0xFC, 0xC8, 0xFC, 0xAC, 0xFC, + 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, + 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, + 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, 0xCF, 0xFC, +]; + +pub(crate) const ORIGINAL_ROOM_BLOCKS: [u8; 576] = [ + 0x00, 0x49, 0x00, 0x00, 0x46, 0x49, 0x0C, 0x1D, 0x48, 0x49, 0x13, 0x1D, 0x46, 0x49, 0x13, 0x0E, + 0x48, 0x49, 0x0C, 0x11, 0x48, 0x49, 0x0C, 0x10, 0x4F, 0x49, 0x4A, 0x50, 0x0E, 0x49, 0x4A, 0x11, + 0x46, 0x49, 0x12, 0x00, 0x00, 0x49, 0x00, 0x50, 0x00, 0x49, 0x00, 0x11, 0x48, 0x49, 0x0C, 0x00, + 0x00, 0x00, 0x37, 0x36, 0x48, 0x49, 0x4C, 0x11, 0x5D, 0x2C, 0x0C, 0x44, 0x00, 0x00, 0x4E, 0x00, + 0x0F, 0x00, 0x12, 0x10, 0x00, 0x00, 0x00, 0x4C, 0x00, 0x0D, 0x17, 0x00, 0x16, 0x0D, 0x17, 0x1B, + 0x16, 0x0D, 0x17, 0x14, 0x15, 0x0D, 0x17, 0x15, 0x16, 0x0D, 0x18, 0x19, 0x16, 0x0D, 0x17, 0x19, + 0x16, 0x0D, 0x00, 0x00, 0x16, 0x0D, 0x18, 0x1B, 0x0F, 0x49, 0x4A, 0x11, 0x4B, 0x2A, 0x5C, 0x15, + 0x16, 0x49, 0x17, 0x1D, 0x00, 0x00, 0x00, 0x15, 0x16, 0x0D, 0x17, 0x10, 0x16, 0x49, 0x12, 0x00, + 0x16, 0x49, 0x0C, 0x11, 0x00, 0x00, 0x12, 0x10, 0x16, 0x0D, 0x00, 0x11, 0x16, 0x49, 0x0C, 0x00, + 0x16, 0x0D, 0x4C, 0x11, 0x0E, 0x0D, 0x4A, 0x11, 0x16, 0x1A, 0x17, 0x1B, 0x4F, 0x34, 0x4A, 0x50, + 0x35, 0x4D, 0x65, 0x36, 0x4A, 0x34, 0x4E, 0x00, 0x0E, 0x34, 0x4A, 0x11, 0x51, 0x34, 0x5D, 0x59, + 0x4B, 0x49, 0x4C, 0x11, 0x2D, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x12, 0x59, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x49, 0x2B, 0x2D, 0x46, 0x49, 0x1C, 0x52, 0x00, 0x49, 0x1C, 0x52, 0x5D, 0x49, 0x00, 0x52, + 0x46, 0x49, 0x13, 0x52, 0x4B, 0x4D, 0x4A, 0x5A, 0x47, 0x49, 0x1C, 0x52, 0x4B, 0x4D, 0x39, 0x36, + 0x1F, 0x2C, 0x2E, 0x52, 0x1F, 0x2C, 0x2E, 0x1D, 0x2F, 0x2C, 0x2E, 0x52, 0x2F, 0x2C, 0x2E, 0x31, + 0x1F, 0x1E, 0x30, 0x52, 0x51, 0x49, 0x13, 0x00, 0x4F, 0x49, 0x13, 0x50, 0x4F, 0x4D, 0x4A, 0x50, + 0x4B, 0x49, 0x4C, 0x2B, 0x1F, 0x20, 0x22, 0x53, 0x55, 0x3D, 0x42, 0x43, 0x1F, 0x1E, 0x23, 0x52, + 0x1F, 0x1E, 0x39, 0x3A, 0x1F, 0x1E, 0x3A, 0x3E, 0x1F, 0x1E, 0x3C, 0x3D, 0x40, 0x1E, 0x27, 0x3F, + 0x55, 0x1A, 0x42, 0x43, 0x1F, 0x1E, 0x2A, 0x52, 0x1F, 0x1E, 0x38, 0x52, 0x1F, 0x20, 0x28, 0x52, + 0x1F, 0x20, 0x26, 0x52, 0x1F, 0x2C, 0x25, 0x52, 0x1F, 0x20, 0x27, 0x52, 0x1F, 0x1E, 0x29, 0x52, + 0x1F, 0x2C, 0x3B, 0x52, 0x46, 0x49, 0x24, 0x52, 0x21, 0x41, 0x45, 0x33, 0x1F, 0x2C, 0x28, 0x31, + 0x1F, 0x0D, 0x29, 0x52, 0x1F, 0x1E, 0x27, 0x52, 0x1F, 0x20, 0x27, 0x53, 0x48, 0x49, 0x13, 0x52, + 0x0E, 0x1E, 0x4A, 0x50, 0x1F, 0x20, 0x26, 0x53, 0x15, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x2A, 0x52, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x5D, 0x49, 0x00, 0x52, 0x55, 0x49, 0x42, 0x43, + 0x61, 0x62, 0x63, 0x50, 0x61, 0x62, 0x63, 0x50, 0x61, 0x62, 0x63, 0x50, 0x61, 0x62, 0x63, 0x50, + 0x61, 0x62, 0x63, 0x50, 0x61, 0x62, 0x63, 0x50, 0x61, 0x56, 0x57, 0x50, 0x61, 0x62, 0x63, 0x50, + 0x61, 0x62, 0x63, 0x50, 0x61, 0x56, 0x57, 0x50, 0x61, 0x56, 0x63, 0x50, 0x61, 0x56, 0x57, 0x50, + 0x61, 0x56, 0x33, 0x50, 0x61, 0x56, 0x57, 0x50, 0x61, 0x62, 0x63, 0x50, 0x61, 0x62, 0x63, 0x50, +]; + +pub(crate) const ORIGINAL_OVERWORLD_BLOCKS: [u8; 272] = [ + 0x07, 0x07, 0x07, 0x10, 0x10, 0x10, 0x10, 0x10, 0x07, 0x07, 0x07, 0x10, 0x10, 0x10, 0x10, 0x04, + 0x06, 0x06, 0x00, 0x03, 0x03, 0x00, 0x0D, 0x0A, 0x06, 0x06, 0x01, 0x01, 0x01, 0x04, 0x05, 0x05, + 0x06, 0x06, 0x06, 0x01, 0x01, 0x04, 0x05, 0x05, 0x06, 0x09, 0x0F, 0x00, 0x00, 0x0B, 0x0B, 0x05, + 0x08, 0x08, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x08, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x07, 0x07, 0x1A, 0x10, 0x10, 0x10, 0x10, 0x10, 0x07, 0x07, 0x1A, 0x10, 0x10, 0x10, 0x10, 0x04, + 0x06, 0x06, 0x00, 0x03, 0x03, 0x00, 0x0D, 0x0A, 0x06, 0x06, 0x1C, 0x1C, 0x1C, 0x02, 0x05, 0x05, + 0x06, 0x06, 0x06, 0x1C, 0x1C, 0x00, 0x05, 0x05, 0x06, 0x00, 0x0F, 0x00, 0x00, 0x23, 0x23, 0x05, + 0x1F, 0x1F, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x1F, 0x1F, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x13, 0x13, 0x17, 0x14, 0x14, 0x14, 0x14, 0x14, 0x13, 0x13, 0x17, 0x14, 0x14, 0x14, 0x14, 0x16, + 0x15, 0x15, 0x12, 0x13, 0x13, 0x18, 0x16, 0x16, 0x15, 0x15, 0x13, 0x26, 0x26, 0x13, 0x17, 0x17, + 0x15, 0x15, 0x15, 0x26, 0x26, 0x13, 0x17, 0x17, 0x1B, 0x1D, 0x11, 0x13, 0x13, 0x18, 0x18, 0x17, + 0x16, 0x16, 0x13, 0x13, 0x13, 0x19, 0x19, 0x19, 0x16, 0x16, 0x18, 0x13, 0x18, 0x19, 0x19, 0x19, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x05, 0x05, 0x06, 0x09, 0x09, 0x09, 0x09, 0x09, 0x05, 0x05, 0x06, 0x09, 0x09, 0x09, 0x09, 0x03, +]; diff --git a/enemize/src/graph/item.rs b/enemize/src/graph/item.rs new file mode 100644 index 0000000..13f3f16 --- /dev/null +++ b/enemize/src/graph/item.rs @@ -0,0 +1,554 @@ +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; + +use lazy_static::lazy_static; + +#[derive(Clone, Copy)] +pub enum ItemType { + Normal, + Bottle, + Special, + Progressive { level: u8 }, + Consumable { found: u8, used: u8 }, +} + +impl ItemType { + pub fn level(&self) -> u8 { + match self { + Self::Progressive { level } => *level, + _ => 0, + } + } + + pub fn increase_level(&mut self) { + match self { + Self::Progressive { ref mut level } => *level += 1, + _ => (), + } + } + + pub fn found(&self) -> u8 { + match self { + Self::Consumable { found, .. } => *found, + _ => 0, + } + } + + pub fn increase_count(&mut self) { + match self { + Self::Consumable { ref mut found, .. } => *found += 1, + _ => (), + } + } + + pub fn used(&self) -> u8 { + match self { + Self::Consumable { used, .. } => *used, + _ => 0, + } + } + + pub fn consume(&mut self) { + match self { + Self::Consumable { ref mut used, .. } => *used += 1, + _ => (), + } + } + + pub fn usable(&self) -> bool { + match self { + Self::Consumable { found, used } => found > used, + _ => false, + } + } +} + +#[derive(Clone, Copy)] +pub struct Item { + pub id: ItemId, + pub name: &'static str, + pub item_type: ItemType, +} + +impl Item { + pub fn normal(id: ItemId, name: &'static str) -> Self { + Self { + id, + name, + item_type: ItemType::Normal, + } + } + + pub fn bottle(id: ItemId, name: &'static str) -> Self { + Self { + id, + name, + item_type: ItemType::Bottle, + } + } + + pub fn special(id: ItemId, name: &'static str) -> Self { + Self { + id, + name, + item_type: ItemType::Special, + } + } + + pub fn progressive(id: ItemId, name: &'static str) -> Self { + Self { + id, + name, + item_type: ItemType::Progressive { level: 0 }, + } + } + + pub fn consumable(id: ItemId, name: &'static str) -> Self { + Self { + id, + name, + item_type: ItemType::Consumable { found: 0, used: 0 }, + } + } +} + +impl PartialEq for Item { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Item {} + +impl Hash for Item { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +#[repr(u32)] +pub enum ItemId { + FightersSwordAndShield = 0x00, + MasterSword1 = 0x01, + TemperedSword = 0x02, + GoldenSword = 0x03, + FightersShield = 0x04, + FireShield = 0x05, + MirrorShield = 0x06, + FireRod = 0x07, + IceRod = 0x08, + Hammer = 0x09, + Hookshot = 0x0a, + Bow = 0x0b, + Boomerang = 0x0c, + Powder = 0x0d, + Bee = 0x0e, + Bombos = 0x0f, + Ether = 0x10, + Quake = 0x11, + Lamp = 0x12, + Shovel = 0x13, + Flute = 0x14, + Somaria = 0x15, + Bottle = 0x16, + PieceOfHeart = 0x17, + Byrna = 0x18, + Cape = 0x19, + Mirror = 0x1a, + L1Gloves = 0x1b, + L2Gloves = 0x1c, + Book = 0x1d, + Flippers = 0x1e, + MoonPearl = 0x1f, + Crystal = 0x20, + BugCatchingNet = 0x21, + BlueMail = 0x22, + RedMail = 0x23, + Key = 0x24, + Compass = 0x25, + HeartContainerNoAnimation = 0x26, + Bomb = 0x27, + ThreeBombs = 0x28, + Mushroom = 0x29, + MagicalBoomerang = 0x2a, + BottleRedPotion = 0x2b, + BottleGreenPotion = 0x2c, + BottleBluePotion = 0x2d, + RedPotion = 0x2e, + GreenPotion = 0x2f, + BluePotion = 0x30, + TenBombs = 0x31, + BigKey = 0x32, + DungeonMap = 0x33, + GreenRupee = 0x34, + BlueRupee = 0x35, + RedRupee = 0x36, + PendantGreen = 0x37, + PendantRed = 0x38, + PendantBlue = 0x39, + BowAndArrows = 0x3a, + BowAndSilverArrows = 0x3b, + BottleBee = 0x3c, + BottleFairy = 0x3d, + HeartContainer = 0x3e, + HeartContainerRefill = 0x3f, + OneHundredRupees = 0x40, + FiftyRupees = 0x41, + Heart = 0x42, + SingleArrow = 0x43, + TenArrows = 0x44, + SmallMagic = 0x45, + ThreeHundredRupees = 0x46, + RedRupee2 = 0x47, + BottleGoldenBee = 0x48, + FightersSword = 0x49, + FluteActive = 0x4a, + Boots = 0x4b, + MaxBombs = 0x4c, + MaxArrows = 0x4d, + HalfMagic = 0x4e, + QuarterMagic = 0x4f, + MasterSword2 = 0x50, + BombUpgrade5 = 0x51, + BombUpgrade10 = 0x52, + ArrowUpgrade5 = 0x53, + ArrowUpgrade10 = 0x54, + Programmable1 = 0x55, + Programmable2 = 0x56, + Programmable3 = 0x57, + SilverArrows = 0x58, + Rupoor = 0x59, + Nothing = 0x5a, + RedClock = 0x5b, + BlueClock = 0x5c, + GreenClock = 0x5d, + ProgressiveSword = 0x5e, + ProgressiveShield = 0x5f, + ProgressiveArmor = 0x60, + ProgressiveGloves = 0x61, + UniqueRngItem = 0x62, + NonUniqueRngItem = 0x63, + ProgressiveBow = 0x64, + ProgressiveBow2 = 0x65, + Triforce = 0x6a, + PowerStar = 0x6b, + TriforcePiece = 0x6c, + LightWorldMap = 0x70, + DarkWorldMap = 0x71, + GTMap = 0x72, + TurtleMap = 0x73, + ThievesMap = 0x74, + HeraMap = 0x75, + IceMap = 0x76, + SkullMap = 0x77, + MireMap = 0x78, + PodMap = 0x79, + SwampMap = 0x7a, + AgaMap = 0x7b, + DesertMap = 0x7c, + EasternMap = 0x7d, + HyruleMap = 0x7e, + SewersMap = 0x7f, + GTCompass = 0x82, + TurtleCompass = 0x83, + ThievesCompass = 0x84, + HeraCompass = 0x85, + IceCompass = 0x86, + SkullCompass = 0x87, + MireCompass = 0x88, + PodCompass = 0x89, + SwampCompass = 0x8a, + AgaCompass = 0x8b, + DesertCompass = 0x8c, + EasternCompass = 0x8d, + HyruleCompass = 0x8e, + SewersCompass = 0x8f, + SkeletonKey = 0x90, + Reserved0x91 = 0x91, + GTBigKey = 0x92, + TurtleBigKey = 0x93, + ThievesBigKey = 0x94, + HeraBigKey = 0x95, + IceBigKey = 0x96, + SkullBigKey = 0x97, + MireBigKey = 0x98, + PodBigKey = 0x99, + SwampBigKey = 0x9a, + AgaBigKey = 0x9b, + DesertBigKey = 0x9c, + EasternBigKey = 0x9d, + HyruleBigKey = 0x9e, + SewersBigKey = 0x9f, + SewersKey = 0xa0, + HyruleKey = 0xa1, + EasternKey = 0xa2, + DesertKey = 0xa3, + AgaKey = 0xa4, + SwampKey = 0xa5, + PodKey = 0xa6, + MireKey = 0xa7, + SkullKey = 0xa8, + IceKey = 0xa9, + HeraKey = 0xaa, + ThievesKey = 0xab, + TurtleKey = 0xac, + GTKey = 0xad, + Reserved0xAE = 0xae, + Reserved0xAF = 0xaf, + Agahnim1 = 0x01ff, + Agahnim1Boss = 0x02ff, + Agahnim2 = 0x03ff, + Agahnim2Boss = 0x04ff, + BigBomb = 0x05ff, + Crystal1 = 0x06ff, + Crystal2 = 0x07ff, + Crystal3 = 0x08ff, + Crystal4 = 0x09ff, + Crystal5 = 0x0aff, + Crystal6 = 0x0bff, + Crystal7 = 0x0cff, + DesertBoss = 0x0dff, + EasternBoss = 0x0eff, + Smith = 0x0fff, + Ganon = 0x10ff, + GTArmosBoss = 0x11ff, + GTLanmolasBoss = 0x12ff, + GTMoldormBoss = 0x13ff, + HeraBoss = 0x14ff, + IceBoss = 0x15ff, + IceBlock = 0x16ff, + L1Shield = 0x17ff, + L1Sword = 0x18ff, + L2Shield = 0x19ff, + L2Sword = 0x1aff, + L3Shield = 0x1bff, + L3Sword = 0x1cff, + L4Sword = 0x1dff, + MireBoss = 0x1eff, + MiseryMireToken = 0x1fff, + MireSwitch = 0x20ff, + PodBoss = 0x21ff, + PurpleChest = 0x22ff, + SkullBoss = 0x23ff, + SwampBoss = 0x24ff, + ThievesBoss = 0x25ff, + TurtleBoss = 0x26ff, + TurtleRockToken = 0x27ff, + ProgressiveMagic = 0x28ff, +} + +lazy_static! { + pub static ref ITEMS: HashMap = { + use ItemId::*; + + let mut items: HashMap = HashMap::new(); + + items.insert(FightersSwordAndShield, Item::normal(FightersSwordAndShield, "Fighters Sword and Shield")); + items.insert(MasterSword1, Item::normal(MasterSword1, "Master Sword")); + items.insert(TemperedSword, Item::normal(TemperedSword, "Tempered Sword")); + items.insert(GoldenSword, Item::normal(GoldenSword, "Golden Sword")); + items.insert(FightersShield, Item::normal(FightersShield, "Fighters Shield")); + items.insert(FireShield, Item::normal(FireShield, "Fire Shield")); + items.insert(FireRod, Item::normal(FireRod, "Fire Rod")); + items.insert(IceRod, Item::normal(IceRod, "Ice Rod")); + items.insert(Hammer, Item::normal(Hammer, "Hammer")); + items.insert(Hookshot, Item::normal(Hookshot, "Hookshot")); + items.insert(Bow, Item::normal(Bow, "Bow")); + items.insert(Boomerang, Item::normal(Boomerang, "Boomerang")); + items.insert(Powder, Item::normal(Powder, "Magic Powder")); + items.insert(Bee, Item::normal(Bee, "Bee")); + items.insert(Bombos, Item::normal(Bombos, "Bombos")); + items.insert(Ether, Item::normal(Ether, "Ether")); + items.insert(Quake, Item::normal(Quake, "Quake")); + items.insert(Lamp, Item::normal(Lamp, "Lamp")); + items.insert(Shovel, Item::normal(Shovel, "Shovel")); + items.insert(Flute, Item::normal(Flute, "Flute")); + items.insert(Somaria, Item::normal(Somaria, "Cane of Somaria")); + items.insert(Bottle, Item::bottle(Bottle, "Bottle")); + items.insert(PieceOfHeart, Item::normal(PieceOfHeart, "Piece of Heart")); + items.insert(Byrna, Item::normal(Byrna, "Cane of Byrna")); + items.insert(Cape, Item::normal(Cape, "Magic Cape")); + items.insert(Mirror, Item::normal(Mirror, "Magic Mirror")); + items.insert(L1Gloves, Item::progressive(L1Gloves, "Power Gloves")); + items.insert(L2Gloves, Item::progressive(L2Gloves, "Titan Mitts")); + items.insert(Book, Item::normal(Book, "Book of Mudora")); + items.insert(Flippers, Item::normal(Flippers, "Zora's Flippers")); + items.insert(MoonPearl, Item::normal(MoonPearl, "Moon Pearl")); + items.insert(Crystal, Item::special(Crystal, "Crystal")); + items.insert(BugCatchingNet, Item::normal(BugCatchingNet, "Bug Catching Net")); + items.insert(BlueMail, Item::normal(BlueMail, "Blue Mail")); + items.insert(RedMail, Item::normal(RedMail, "Red Mail")); + items.insert(Key, Item::consumable(Key, "Key")); + items.insert(Compass, Item::special(Compass, "Compass")); + items.insert(HeartContainerNoAnimation, Item::normal(HeartContainerNoAnimation, "Heart Container (no animation)")); + items.insert(Bomb, Item::normal(Bomb, "Bomb")); + items.insert(ThreeBombs, Item::normal(ThreeBombs, "3 Bombs")); + items.insert(Mushroom, Item::consumable(Mushroom, "Mushroom")); + items.insert(MagicalBoomerang, Item::normal(MagicalBoomerang, "Magical Boomerang")); + items.insert(BottleRedPotion, Item::bottle(BottleRedPotion, "Bottle (Red Potion)")); + items.insert(BottleGreenPotion, Item::bottle(BottleGreenPotion, "Bottle (Green Potion)")); + items.insert(BottleBluePotion, Item::bottle(BottleBluePotion, "Bottle (Blue Potion)")); + items.insert(RedPotion, Item::special(RedPotion, "Red Potion")); + items.insert(GreenPotion, Item::special(GreenPotion, "Green Potion")); + items.insert(BluePotion, Item::special(BluePotion, "Blue Potion")); + items.insert(TenBombs, Item::normal(TenBombs, "10 Bombs")); + items.insert(BigKey, Item::special(BigKey, "Big Key")); + items.insert(DungeonMap, Item::special(DungeonMap, "Dungeon Map")); + items.insert(GreenRupee, Item::normal(GreenRupee, "Green Rupee")); + items.insert(BlueRupee, Item::normal(BlueRupee, "Blue Rupee")); + items.insert(RedRupee, Item::normal(RedRupee, "Red Rupee")); + items.insert(PendantGreen, Item::special(PendantGreen, "Pendant of Courage")); + items.insert(PendantRed, Item::special(PendantRed, "Pendant of Wisdom")); + items.insert(PendantBlue, Item::special(PendantBlue, "Pendant of Power")); + items.insert(BowAndArrows, Item::special(BowAndArrows, "Bow and Arrows")); + items.insert(BowAndSilverArrows, Item::special(BowAndSilverArrows, "Bow and Silver Arrows")); + items.insert(BottleBee, Item::bottle(BottleBee, "Bottle (Bee)")); + items.insert(BottleFairy, Item::bottle(BottleFairy, "Bottle, (Fairy)")); + items.insert(HeartContainer, Item::normal(HeartContainer, "Heart Container")); + items.insert(HeartContainerRefill, Item::normal(HeartContainerRefill, "Heart Container (refill)")); + items.insert(OneHundredRupees, Item::normal(OneHundredRupees, "100 Rupees")); + items.insert(FiftyRupees, Item::normal(FiftyRupees, "50 Rupees")); + items.insert(Heart, Item::normal(Heart, "Heart")); + items.insert(SingleArrow, Item::normal(SingleArrow, "Single Arrow")); + items.insert(TenArrows, Item::normal(TenArrows, "10 Arrows")); + items.insert(SmallMagic, Item::normal(SmallMagic, "Small Magic Refill")); + items.insert(ThreeHundredRupees, Item::normal(ThreeHundredRupees, "300 Rupees")); + items.insert(RedRupee2, Item::normal(RedRupee2, "Red Rupee")); + items.insert(BottleGoldenBee, Item::bottle(BottleGoldenBee, "Bottle (Golden Bee)")); + items.insert(FightersSword, Item::normal(FightersSword, "Fighter's Sword")); + items.insert(FluteActive, Item::normal(FluteActive, "Flute (activated)")); + items.insert(Boots, Item::normal(Boots, "Pegasus Boots")); + items.insert(MaxBombs, Item::normal(MaxBombs, "Max Bomb Upgrade (50)")); + items.insert(MaxArrows, Item::normal(MaxArrows, "Max Arrow Upgrade (70)")); + items.insert(HalfMagic, Item::normal(HalfMagic, "Half Magic")); + items.insert(QuarterMagic, Item::normal(QuarterMagic, "Quarter Magic")); + items.insert(MasterSword2, Item::normal(MasterSword2, "Master Sword")); + items.insert(BombUpgrade5, Item::normal(BombUpgrade5, "Bomb Upgrade (5)")); + items.insert(BombUpgrade10, Item::normal(BombUpgrade10, "Bomb Upgrade (10)")); + items.insert(ArrowUpgrade5, Item::normal(ArrowUpgrade5, "Arrow Upgrade (5)")); + items.insert(ArrowUpgrade10, Item::normal(ArrowUpgrade10, "Arrow Upgrade (10)")); + items.insert(Programmable1, Item::normal(Programmable1, "Programmable 1")); + items.insert(Programmable2, Item::normal(Programmable2, "Programmable 2")); + items.insert(Programmable3, Item::normal(Programmable3, "Programmable 3")); + items.insert(SilverArrows, Item::normal(SilverArrows, "Silver Arrows")); + items.insert(Rupoor, Item::normal(Rupoor, "Rupoor")); + items.insert(Nothing, Item::normal(Nothing, "Nothing (Null Item)")); + items.insert(RedClock, Item::consumable(RedClock, "Red Clock")); + items.insert(BlueClock, Item::consumable(BlueClock, "Blue Clock")); + items.insert(GreenClock, Item::consumable(GreenClock, "Green Clock")); + items.insert(ProgressiveSword, Item::progressive(ProgressiveSword, "Progressive Sword")); + items.insert(ProgressiveShield, Item::progressive(ProgressiveShield, "Progressive Shield")); + items.insert(ProgressiveArmor, Item::progressive(ProgressiveArmor, "Progressive Armor")); + items.insert(ProgressiveGloves, Item::progressive(ProgressiveGloves, "Progressive Gloves")); + items.insert(UniqueRngItem, Item::special(UniqueRngItem, "Unique RNG Item (RNG Pool Single)")); + items.insert(NonUniqueRngItem, Item::special(NonUniqueRngItem, "Non-unique RNG Item (RNG Pool Multi)")); + items.insert(ProgressiveBow, Item::normal(ProgressiveBow, "Progressive Bow")); // *NOT* ProgressiveItem??? + items.insert(ProgressiveBow2, Item::normal(ProgressiveBow2, "Progressive Bow 2")); + items.insert(Triforce, Item::special(Triforce, "Triforce (Single Goal Item)")); + items.insert(PowerStar, Item::special(PowerStar, "Power Star (Multi Goal Item)")); + items.insert(TriforcePiece, Item::special(TriforcePiece, "Triforce Piece")); + items.insert(LightWorldMap, Item::special(LightWorldMap, "Light World Map")); + items.insert(DarkWorldMap, Item::special(DarkWorldMap, "Dark World Map")); + items.insert(GTMap, Item::special(GTMap, "Ganon's Tower Map")); + items.insert(TurtleMap, Item::special(TurtleMap, "Turtle Rock Map")); + items.insert(ThievesMap, Item::special(ThievesMap, "Thieves' Town Map")); + items.insert(HeraMap, Item::special(HeraMap, "Tower of Hera Map")); + items.insert(IceMap, Item::special(IceMap, "Ice Palace Map")); + items.insert(SkullMap, Item::special(SkullMap, "Skull Woods Map")); + items.insert(MireMap, Item::special(MireMap, "Misery Mire Map")); + items.insert(PodMap, Item::special(PodMap, "Palace of Darkness Map")); + items.insert(SwampMap, Item::special(SwampMap, "Swamp Palace Map")); + items.insert(AgaMap, Item::special(AgaMap, "Agahnim's Tower Map")); + items.insert(DesertMap, Item::special(DesertMap, "Desert Palace Map")); + items.insert(EasternMap, Item::special(EasternMap, "Eastern Palace Map")); + items.insert(HyruleMap, Item::special(HyruleMap, "Hyrule Castle Map")); + items.insert(SewersMap, Item::special(SewersMap, "Sewers Map")); // not the same as HyruleMap? + items.insert(GTCompass, Item::special(GTCompass, "Ganon's Tower Compass")); + items.insert(TurtleCompass, Item::special(TurtleCompass, "Turtle Rock Compass")); + items.insert(ThievesCompass, Item::special(ThievesCompass, "Thieves' Town Compass")); + items.insert(HeraCompass, Item::special(HeraCompass, "Tower of Hera Compass")); + items.insert(IceCompass, Item::special(IceCompass, "Ice Palace Compass")); + items.insert(SkullCompass, Item::special(SkullCompass, "Skull Woods Compass")); + items.insert(MireCompass, Item::special(MireCompass, "Misery Mire Compass")); + items.insert(PodCompass, Item::special(PodCompass, "Palace of Darkness Compass")); + items.insert(SwampCompass, Item::special(SwampCompass, "Swamp Palace Compass")); + items.insert(AgaCompass, Item::special(AgaCompass, "Agahnim's Tower Compass")); + items.insert(DesertCompass, Item::special(DesertCompass, "Desert Palace Compass")); + items.insert(EasternCompass, Item::special(EasternCompass, "Eastern Palace Compass")); + items.insert(HyruleCompass, Item::special(HyruleCompass, "Hyrule Castle Compass")); + items.insert(SewersCompass, Item::special(SewersCompass, "Sewers Compass")); // not the same as HyruleCompass? + items.insert(SkeletonKey, Item::special(SkeletonKey, "Skeleton Key")); + items.insert(Reserved0x91, Item::special(Reserved0x91, "")); + items.insert(GTBigKey, Item::special(GTBigKey, "Ganon's Tower Big Key")); + items.insert(TurtleBigKey, Item::special(TurtleBigKey, "Turtle Rock Big Key")); + items.insert(ThievesBigKey, Item::special(ThievesBigKey, "Thieves' Town Big Key")); + items.insert(HeraBigKey, Item::special(HeraBigKey, "Tower of Hera Big Key")); + items.insert(IceBigKey, Item::special(IceBigKey, "Ice Palace Big Key")); + items.insert(SkullBigKey, Item::special(SkullBigKey, "Skull Woods Big Key")); + items.insert(MireBigKey, Item::special(MireBigKey, "Misery Mire Big Key")); + items.insert(PodBigKey, Item::special(PodBigKey, "Palace of Darkness Big Key")); + items.insert(SwampBigKey, Item::special(SwampBigKey, "Swamp Palace Big Key")); + items.insert(AgaBigKey, Item::special(AgaBigKey, "Agahnim's Tower Big Key")); + items.insert(DesertBigKey, Item::special(DesertBigKey, "Desert Palace Big Key")); + items.insert(EasternBigKey, Item::special(EasternBigKey, "Eastern Palace Big Key")); + items.insert(HyruleBigKey, Item::special(HyruleBigKey, "Hyrule Castle Big Key")); + items.insert(SewersBigKey, Item::special(SewersBigKey, "Sewers Big Key")); // not the same as HyruleBigKey? + items.insert(SewersKey, Item::consumable(SewersKey, "Sewers Key")); + items.insert(HyruleKey, Item::consumable(HyruleKey, "Hyrule Castle Key")); // not the same as SewersKey? + items.insert(EasternKey, Item::consumable(EasternKey, "Eastern Palace Key")); + items.insert(DesertKey, Item::consumable(DesertKey, "Desert Palace Key")); + items.insert(AgaKey, Item::consumable(AgaKey, "Agahnim's Tower Key")); + items.insert(SwampKey, Item::consumable(SwampKey, "Swamp Palace Key")); + items.insert(PodKey, Item::consumable(PodKey, "Palace of Darkness Key")); + items.insert(MireKey, Item::consumable(MireKey, "Misery Mire Key")); + items.insert(SkullKey, Item::consumable(SkullKey, "Skull Woods Key")); + items.insert(IceKey, Item::consumable(IceKey, "Ice Palace Key")); + items.insert(HeraKey, Item::consumable(HeraKey, "Tower of Hera Key")); + items.insert(ThievesKey, Item::consumable(ThievesKey, "Thieves' Town Key")); + items.insert(TurtleKey, Item::consumable(TurtleKey, "Turtle Rock Key")); + items.insert(GTKey, Item::consumable(GTKey, "Ganon's Tower Key")); + items.insert(Reserved0xAE, Item::special(Reserved0xAE, "")); + items.insert(Reserved0xAF, Item::special(Reserved0xAF, "")); + items.insert(Agahnim1, Item::special(Agahnim1, "Agahnim 1 Defeated")); + items.insert(Agahnim1Boss, Item::special(Agahnim1Boss, "Agahnim Boss")); + items.insert(Agahnim2, Item::special(Agahnim2, "Agahnim 2 Defeated")); + items.insert(Agahnim2Boss, Item::special(Agahnim2Boss, "Agahnim 2 Boss")); + items.insert(BigBomb, Item::special(BigBomb, "Big Bomb")); + items.insert(Crystal1, Item::special(Crystal1, "Crystal 1")); + items.insert(Crystal2, Item::special(Crystal2, "Crystal 2")); + items.insert(Crystal3, Item::special(Crystal3, "Crystal 3")); + items.insert(Crystal4, Item::special(Crystal4, "Crystal 4")); + items.insert(Crystal5, Item::special(Crystal5, "Crystal 5")); + items.insert(Crystal6, Item::special(Crystal6, "Crystal 6")); + items.insert(Crystal7, Item::special(Crystal7, "Crystal 7")); + items.insert(DesertBoss, Item::special(DesertBoss, "Desert Boss")); + items.insert(EasternBoss, Item::special(EasternBoss, "Eastern Boss")); + items.insert(Smith, Item::special(Smith, "Frog Smith")); + items.insert(Ganon, Item::special(Ganon, "Ganon")); + items.insert(GTArmosBoss, Item::special(GTArmosBoss, "GT Armos Boss")); + items.insert(GTLanmolasBoss, Item::special(GTLanmolasBoss, "GT Lanmolas Boss")); + items.insert(GTMoldormBoss, Item::special(GTMoldormBoss, "GT Moldorm Boss")); + items.insert(HeraBoss, Item::special(HeraBoss, "Hera Boss")); + items.insert(IceBoss, Item::special(IceBoss, "Ice Boss")); + items.insert(IceBlock, Item::special(IceBlock, "Ice Palace Block")); + items.insert(L1Shield, Item::progressive(L1Shield, "L1 Shield")); + items.insert(L1Sword, Item::progressive(L1Sword, "L1 Sword")); + items.insert(L2Shield, Item::progressive(L2Shield, "L2 Shield")); + items.insert(L2Sword, Item::progressive(L2Sword, "L2 Sword")); + items.insert(L3Shield, Item::progressive(L3Shield, "L3 Shield")); + items.insert(L3Sword, Item::progressive(L3Sword, "L3 Sword")); + items.insert(L4Sword, Item::progressive(L4Sword, "L4 Sword")); + items.insert(MireBoss, Item::special(MireBoss, "Mire Boss")); + items.insert(MiseryMireToken, Item::special(MiseryMireToken, "Misery Mire Entrance Token")); + items.insert(MireSwitch, Item::special(MireSwitch, "Misery Mire Switch")); + items.insert(PodBoss, Item::special(PodBoss, "PoD Boss")); + items.insert(PurpleChest, Item::special(PurpleChest, "Purple Chest")); + items.insert(SkullBoss, Item::special(SkullBoss, "Skull Boss")); + items.insert(ThievesBoss, Item::special(ThievesBoss, "Thieves Boss")); + items.insert(TurtleBoss, Item::special(TurtleBoss, "Turtle Boss")); + items.insert(ProgressiveMagic, Item::special(ProgressiveMagic, "Progressive Magic")); + + items + }; +} diff --git a/enemize/src/graph/locations.rs b/enemize/src/graph/locations.rs new file mode 100644 index 0000000..1192e1a --- /dev/null +++ b/enemize/src/graph/locations.rs @@ -0,0 +1,1146 @@ +use std::collections::HashMap; + +use lazy_static::lazy_static; + +use super::item::ItemId; + +#[derive(Debug, thiserror::Error)] +#[error("{0} is not a dungeon super tile")] +pub struct NotADungeonSuperTile(u8); + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub enum Dungeon { + HyruleCastle, + EasternPalace, + DesertPalace, + TowerOfHera, + AgahnimsTower, + PalaceOfDarkness, + SwampPalace, + SkullWoods, + ThievesTown, + IcePalace, + MiseryMire, + TurtleRock, + GanonsTower, +} + +pub fn get_dungeon_from_room(room_id: u8) -> Result { + for (dungeon, rooms) in DUNGEON_ROOMS.iter() { + if let Ok(_) = rooms.binary_search(&room_id) { + return Ok(*dungeon); + } + } + + Err(NotADungeonSuperTile(room_id)) +} + +const HYRULE_CASTLE_ROOMS: [u8; 21] = [ + 1, 2, 17, 18, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, +]; + +const EASTERN_PALACE_ROOMS: [u8; 13] = [ + 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, +]; + +const DESERT_PALACE_ROOMS: [u8; 10] = [51, 67, 83, 99, 115, 116, 117, 131, 132, 133]; + +const TOWER_OF_HERA_ROOMS: [u8; 7] = [7, 23, 39, 49, 119, 135, 167]; + +const AGAHNIMS_TOWER_ROOMS: [u8; 7] = [32, 48, 64, 176, 192, 208, 224]; + +const PALACE_OF_DARKNESS_ROOMS: [u8; 14] = [9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106]; + +const SWAMP_PALACE_ROOMS: [u8; 13] = [6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118]; + +const SKULL_WOODS_ROOMS: [u8; 9] = [41, 57, 73, 86, 87, 88, 89, 103, 104]; + +const THIEVES_TOWN_ROOMS: [u8; 12] = [68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220]; + +const ICE_PALACE_ROOMS: [u8; 22] = [ + 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, + 222, +]; + +const MISERY_MIRE_ROOMS: [u8; 18] = [ + 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, +]; + +const TURTLE_ROCK_ROOMS: [u8; 17] = [ + 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, +]; + +const GANONS_TOWER_ROOMS: [u8; 26] = [ + 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, + 155, 156, 157, 165, 166, +]; + +lazy_static! { + static ref DUNGEON_ROOMS: HashMap = { + use Dungeon::*; + + let mut rooms = HashMap::new(); + + rooms.insert(HyruleCastle, &HYRULE_CASTLE_ROOMS[..]); + rooms.insert(EasternPalace, &EASTERN_PALACE_ROOMS[..]); + rooms.insert(DesertPalace, &DESERT_PALACE_ROOMS[..]); + rooms.insert(TowerOfHera, &TOWER_OF_HERA_ROOMS[..]); + rooms.insert(AgahnimsTower, &AGAHNIMS_TOWER_ROOMS[..]); + rooms.insert(PalaceOfDarkness, &PALACE_OF_DARKNESS_ROOMS[..]); + rooms.insert(SwampPalace, &SWAMP_PALACE_ROOMS[..]); + rooms.insert(SkullWoods, &SKULL_WOODS_ROOMS[..]); + rooms.insert(ThievesTown, &THIEVES_TOWN_ROOMS[..]); + rooms.insert(IcePalace, &ICE_PALACE_ROOMS[..]); + rooms.insert(MiseryMire, &MISERY_MIRE_ROOMS[..]); + rooms.insert(TurtleRock, &TURTLE_ROCK_ROOMS[..]); + rooms.insert(GanonsTower, &GANONS_TOWER_ROOMS[..]); + + rooms + }; + pub static ref DUNGEON_KEYS: HashMap = { + use Dungeon::*; + use ItemId::*; + + let mut keys = HashMap::new(); + + keys.insert(HyruleCastle, HyruleKey); + keys.insert(EasternPalace, EasternKey); + keys.insert(DesertPalace, DesertKey); + keys.insert(TowerOfHera, HeraKey); + keys.insert(AgahnimsTower, AgaKey); + keys.insert(PalaceOfDarkness, PodKey); + keys.insert(SwampPalace, SwampKey); + keys.insert(SkullWoods, SkullKey); + keys.insert(ThievesTown, ThievesKey); + keys.insert(IcePalace, IceKey); + keys.insert(MiseryMire, MireKey); + keys.insert(TurtleRock, TurtleKey); + keys.insert(GanonsTower, GTKey); + + keys + }; + pub static ref DUNGEON_BIG_KEYS: HashMap = { + use Dungeon::*; + use ItemId::*; + + let mut keys = HashMap::new(); + + keys.insert(HyruleCastle, HyruleBigKey); + keys.insert(EasternPalace, EasternBigKey); + keys.insert(DesertPalace, DesertBigKey); + keys.insert(TowerOfHera, HeraBigKey); + keys.insert(AgahnimsTower, AgaBigKey); + keys.insert(PalaceOfDarkness, PodBigKey); + keys.insert(SwampPalace, SwampBigKey); + keys.insert(SkullWoods, SkullBigKey); + keys.insert(ThievesTown, ThievesBigKey); + keys.insert(IcePalace, IceBigKey); + keys.insert(MiseryMire, MireBigKey); + keys.insert(TurtleRock, TurtleBigKey); + keys.insert(GanonsTower, GTBigKey); + + keys + }; +} + +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +#[repr(u16)] +pub enum SuperTile { + Ganon = 0x0, + HyruleCastleNorthCorridor = 0x1, + HyruleCastleSwitchRoom = 0x2, + HoulihanRoom = 0x3, + TurtleRockCrystalRollerRoom = 0x4, + EmptyCloneRoom0x05 = 0x5, + SwampPalaceArrghus = 0x6, + TowerOfHeraMoldorm = 0x7, + CaveHealingFairy = 0x8, + PalaceOfDarknessMedusaChestRoom = 0x9, + PalaceOfDarknessStalfossTrapRoom = 0xa, + PalaceOfDarknessTurtleRoom = 0xb, + GanonsTowerEntranceRoom = 0xc, + GanonsTowerAgahnim2 = 0xd, + IcePalaceEntranceRoom = 0xe, + EmptyCloneRoom0x0F = 0xf, + GanonEvacuationRoute = 0x10, + HyruleCastleBombableStockRoom = 0x11, + InsideSanctuary = 0x12, + TurtleRockHokkuBokkuKeyRoom2 = 0x13, + TurtleRockBigKeyRoom = 0x14, + TurtleRockUselessTubes = 0x15, + SwampPalaceSwimmingTreadmill = 0x16, + TowerOfHeraMoldormFallRoom = 0x17, + Cave0x18BigFairyDropEntrance = 0x18, + PalaceOfDarknessDarkMaze = 0x19, + PalaceOfDarknessBigChestRoom = 0x1a, + PalaceOfDarknessMimicsMovingWallRoom = 0x1b, + GanonsTowerIceArmos = 0x1c, + GanonsTowerFinalHallway = 0x1d, + IcePalaceBombFloorBariRoom = 0x1e, + IcePalacePengatorBigKeyRoom = 0x1f, + AgahnimsTowerAgahnim = 0x20, + HyruleCastleKeyRatRoom = 0x21, + HyruleCastleSewerTextTriggerRoom = 0x22, + TurtleRockWestExitToBalcony = 0x23, + TurtleRockDoubleHokkuBokkuBigChestRoom = 0x24, + EmptyCloneRoom0x25 = 0x25, + SwampPalaceStatueRoom = 0x26, + TowerOfHeraBigChest = 0x27, + SwampPalaceEntranceRoom = 0x28, + SkullWoodsMothula = 0x29, + PalaceOfDarknessBigHubRoom = 0x2a, + PalaceOfDarknessMapChestFairyRoom = 0x2b, + CaveHookshotCaveBackdoor = 0x2c, + EmptyCloneRoom0x2D = 0x2d, + IcePalaceCompassRoom = 0x2e, + CaveKakarikoWellHP = 0x2f, + AgahnimsTowerMaidenSacrificeChamber = 0x30, + TowerOfHeraHardhatBeetleRoom = 0x31, + HyruleCastleSewerKeyChestRoom = 0x32, + DesertPalaceLanmolas = 0x33, + SwampPalacePushBlockPuzzlePreBigKeyRoom = 0x34, + SwampPalaceBigKeyBsRoom = 0x35, + SwampPalaceBigChestRoom = 0x36, + SwampPalaceMapChestWaterFillRoom = 0x37, + SwampPalaceKeyPotRoom = 0x38, + SkullWoodsGibdoKeyMothulaHoleRoom = 0x39, + PalaceOfDarknessBombableFloorRoom = 0x3a, + PalaceOfDarknessSpikeBlockConveyorRoom = 0x3b, + CaveHookshotCave = 0x3c, + GanonsTowerTorchRoom2 = 0x3d, + IcePalaceStalfosKnightConveyorHellway = 0x3e, + IcePalaceMapChestRoom = 0x3f, + AgahnimsTowerFinalBridgeRoom = 0x40, + HyruleCastleFirstDarkRoom = 0x41, + HyruleCastle6RopesRoom = 0x42, + DesertPalaceTorchPuzzleMovingWallRoom = 0x43, + ThievesTownBigChestRoom = 0x44, + ThievesTownJailCellsRoom = 0x45, + SwampPalaceCompassChestRoom = 0x46, + EmptyCloneRoom0x47 = 0x47, + EmptyCloneRoom0x48 = 0x48, + SkullWoodsGibdoTorchPuzzleRoom = 0x49, + PalaceOfDarknessEntranceRoom = 0x4a, + PalaceOfDarknessWarpsSouthMimicRoom = 0x4b, + GanonsTowerMiniHelmasaurConveyorRoom = 0x4c, + GanonsTowerMoldormRoom = 0x4d, + IcePalaceBombJumpRoom = 0x4e, + IcePalaceCloneRoomFairyRoom = 0x4f, + HyruleCastleWestCorridor = 0x50, + HyruleCastleThroneRoom = 0x51, + HyruleCastleEastCorridor = 0x52, + DesertPalacePopos2BeamosHellwayRoom = 0x53, + SwampPalaceUpstairsPitsRoom = 0x54, + CastleSecretEntranceUncleRoom = 0x55, + SkullWoodsKeyPotTrapRoom = 0x56, + SkullWoodsBigKeyRoom = 0x57, + SkullWoodsBigChestRoom = 0x58, + SkullWoodsFinalSectionEntranceRoom = 0x59, + PalaceOfDarknessHelmasaurKing = 0x5a, + GanonsTowerSpikePitRoom = 0x5b, + GanonsTowerGanonBallZ = 0x5c, + GanonsTowerGauntlet123 = 0x5d, + IcePalaceLonelyFirebar = 0x5e, + IcePalaceHiddenChestSpikeFloorRoom = 0x5f, + HyruleCastleWestEntranceRoom = 0x60, + HyruleCastleMainEntranceRoom = 0x61, + HyruleCastleEastEntranceRoom = 0x62, + DesertPalaceFinalSectionEntranceRoom = 0x63, + ThievesTownWestAtticRoom = 0x64, + ThievesTownEastAtticRoom = 0x65, + SwampPalaceHiddenChestHiddenDoorRoom = 0x66, + SkullWoodsCompassChestRoom = 0x67, + SkullWoodsKeyChestTrapRoom = 0x68, + EmptyCloneRoom0x69 = 0x69, + PalaceOfDarknessRupeeRoom = 0x6a, + GanonsTowerMimicsRoom = 0x6b, + GanonsTowerLanmolasRoom = 0x6c, + GanonsTowerGauntlet45 = 0x6d, + IcePalacePengatorsRoom = 0x6e, + EmptyCloneRoom0x6F = 0x6f, + HyruleCastleSmallCorridorToJailCells = 0x70, + HyruleCastleBoomerangChestRoom = 0x71, + HyruleCastleMapChestRoom = 0x72, + DesertPalaceBigChestRoom = 0x73, + DesertPalaceMapChestRoom = 0x74, + DesertPalaceBigKeyChestRoom = 0x75, + SwampPalaceWaterDrainRoom = 0x76, + TowerOfHeraEntranceRoom = 0x77, + EmptyCloneRoom0x78 = 0x78, + EmptyCloneRoom0x79 = 0x79, + EmptyCloneRoom0x7A = 0x7a, + GanonsTowerMisc = 0x7b, + GanonsTowerEastSideCollapsingBridgeExplodingWallRoom = 0x7c, + GanonsTowerWinderWarpMazeRoom = 0x7d, + IcePalaceHiddenChestBombableFloorRoom = 0x7e, + IcePalaceBigSpikeTrapsRoom = 0x7f, + HyruleCastleJailCellRoom = 0x80, + HyruleCastleNextToChasmRoom = 0x81, + HyruleCastleBasementChasmRoom = 0x82, + DesertPalaceWestEntranceRoom = 0x83, + DesertPalaceMainEntranceRoom = 0x84, + DesertPalaceEastEntranceRoom = 0x85, + EmptyCloneRoom0x86 = 0x86, + TowerOfHeraTileRoom = 0x87, + EmptyCloneRoom0x88 = 0x88, + EasternPalaceFairyRoom = 0x89, + EmptyCloneRoom0x8A = 0x8a, + GanonsTowerBlockPuzzleSpikeSkipMapChestRoom = 0x8b, + GanonsTowerEastAndWestDownstairsBigChestRoom = 0x8c, + GanonsTowerTileTorchPuzzleRoom = 0x8d, + IcePalaceBlobsWithTetrisBarrier = 0x8e, + EmptyCloneRoom0x8F = 0x8f, + MiseryMireVitreous = 0x90, + MiseryMireFinalSwitchRoom = 0x91, + MiseryMireDarkBombWallSwitchesRoom = 0x92, + MiseryMireDarkCaneFLoorSwitchPuzzleRoom = 0x93, + EmptyCloneRoom0x94 = 0x94, + GanonsTowerFinalCollapsingBridgeRoom = 0x95, + GanonsTowerTorches1Room = 0x96, + MiseryMireTorchPuzzleMovingWallRoom = 0x97, + MiseryMireEntranceRoom = 0x98, + EasternPalaceEyegoreKeyRoom = 0x99, + EmptyCloneRoom0x9A = 0x9a, + GanonsTowerManySpikesWarpMazeRoom = 0x9b, + GanonsTowerInvisibleFloorMazeRoom = 0x9c, + GanonsTowerCompassChestInvisibleFloorRoom = 0x9d, + IcePalaceBigChestRoom = 0x9e, + IcePalaceIceFloorPotsKeyAndSwitch = 0x9f, + MiseryMirePreVitreousRoom = 0xa0, + MiseryMireFishRoom = 0xa1, + MiseryMireBridgeKeyChestRoom = 0xa2, + MiseryMireEmptyLConnectingRoom = 0xa3, + TurtleRockTrinexx = 0xa4, + GanonsTowerWizzrobesRooms = 0xa5, + GanonsTowerMoldormFallRoom = 0xa6, + TowerOfHeraFairyRoom = 0xa7, + EasternPalaceStalfosSpawnRoom = 0xa8, + EasternPalaceBigChestRoom = 0xa9, + EasternPalaceMapChestRoom = 0xaa, + ThievesTownMovingSpikesKeyPotRoom = 0xab, + ThievesTownBlindTheThief = 0xac, + EmptyCloneRoom0xAD = 0xad, + IcePalaceIceTRoom = 0xae, + IcePalaceIceBridgeRoom = 0xaf, + AgahnimsTowerCircleOfPots = 0xb0, + MiseryMireHourglassRoom = 0xb1, + MiseryMireSlugRoom = 0xb2, + MiseryMireSpikeKeyChestRoom = 0xb3, + TurtleRockPreTrinexxRoom = 0xb4, + TurtleRockDarkMaze = 0xb5, + TurtleRockChainChompsRoom = 0xb6, + TurtleRockMapChestKeyChestRollerRoom = 0xb7, + EasternPalaceBigKeyRoom = 0xb8, + EasternPalaceLobbyCannonballsRoom = 0xb9, + EasternPalaceDarkAntifairyKeyPotRoom = 0xba, + ThievesTownHellway = 0xbb, + ThievesTownConveyorToilet = 0xbc, + EmptyCloneRoom0xBD = 0xbd, + IcePalaceBlockPuzzleRoom = 0xbe, + IcePalaceCloneRoomSwitchRoom = 0xbf, + AgahnimsTowerDarkBridgeRoom = 0xc0, + MiseryMireCompassChestTileRoom = 0xc1, + MiseryMireBigHubRoom = 0xc2, + MiseryMireBigChestRoom = 0xc3, + TurtleRockFinalCrystalSwitchPuzzleRoom = 0xc4, + TurtleRockLaserBridge = 0xc5, + TurtleRockSomariaHub = 0xc6, + TurtleRockTorchPuzzle = 0xc7, + EasternPalaceArmosKnights = 0xc8, + EasternPalaceEntranceRoom = 0xc9, + UnknownRoom = 0xca, + ThievesTownNorthWestEntranceRoom = 0xcb, + ThievesTownNorthEastEntranceRoom = 0xcc, + EmptyCloneRoom0xCD = 0xcd, + IcePalaceHoleToKholdstareRoom = 0xce, + EmptyCloneRoom0xCF = 0xcf, + AgahnimsTowerDarkMaze = 0xd0, + MiseryMireConveyorSlugBigKeyRoom = 0xd1, + MiseryMireWizzrobesRoom = 0xd2, + EmptyCloneRoom0xD3 = 0xd3, + EmptyCloneRoom0xD4 = 0xd4, + TurtleRockLaserKeyRoom = 0xd5, + TurtleRockEntranceRoom = 0xd6, + EmptyCloneRoom0xD7 = 0xd7, + EasternPalacePreArmosKnightsRoom = 0xd8, + EasternPalaceCannonballRoom = 0xd9, + EasternPalacePotSwitchRoom = 0xda, + ThievesTownSouthWestEntranceRoom = 0xdb, + ThievesTownSouthEastEntranceRoom = 0xdc, + EmptyCloneRoom0xDD = 0xdd, + IcePalaceKholdstare = 0xde, + CaveBackwardsDeathMountainTopFloor = 0xdf, + AgahnimsTowerEntranceRoom = 0xe0, + CaveLostWoodsHP = 0xe1, + CaveLumberjacksTreeHP = 0xe2, + CaveMagicBat = 0xe3, + CaveLostOldManHouse = 0xe4, + CaveLostOldManHouseBack = 0xe5, + Cave0xE6 = 0xe6, + Cave0xE7 = 0xe7, + CaveSuperBunnyTop = 0xe8, + EmptyCloneRoom0xE9 = 0xe9, + CaveSpectacleRockHP = 0xea, + CaveBumperCaveTop = 0xeb, + EmptyCloneRoom0xEC = 0xec, + Cave0xED = 0xed, + CaveSpiralCave = 0xee, + CaveCrystalSwitch5ChestsRoom = 0xef, + CaveLostOldManStartingCaveBottom = 0xf0, + CaveLostOldManStartingCaveTop = 0xf1, + HouseOldWomanNextDoor = 0xf2, + HouseOldWomanSahasrahlasWifeMaybe = 0xf3, + HouseAngryBrothersWest = 0xf4, + HouseAngryBrothersEast = 0xf5, + EmptyCloneRoom0xF6 = 0xf6, + EmptyCloneRoom0xF7 = 0xf7, + CaveSuperBunnyBottom = 0xf8, + CaveSpectacleRockExit = 0xf9, + CaveSpectacleRockPrizeEntrance = 0xfa, + CaveBumperCaveBottom = 0xfb, + EmptyCloneRoom0xFC = 0xfc, + Cave0xFD = 0xfd, + CaveSpiralCaveExit = 0xfe, + CaveParadoxCaveMiddleEntrance = 0xff, + ShopInLostWoods0x100 = 0x100, + ScaredLadyHouses = 0x101, + SickKid = 0x102, + InnBushHouse = 0x103, + InsideLinksHouse = 0x104, + ShabadooHouse = 0x105, + ChestGameBombHouse = 0x106, + LibraryBombFarmRoom = 0x107, + ChickenHouse = 0x108, + InsideWitchHut = 0x109, + Aginah = 0x10a, + Dam = 0x10b, + MimicCave = 0x10c, + CaveOutsideMiseryMire = 0x10d, + CaveDarkHyliaLedgeHint = 0x10e, + ShopDarkWorldShops = 0x10f, + ShopFireShieldShop = 0x110, + ArcherGame = 0x111, + CaveShop = 0x112, + KingsTomb = 0x113, + WaterfallCaveMireHint = 0x114, + BigFairy = 0x115, + FatFairy = 0x116, + SpikeCave = 0x117, + ChestGamblingGame = 0x118, + BlindsHouse = 0x119, + Mutant = 0x11a, + MirrorCaveGroveAndTomb = 0x11b, + InsideBombShop = 0x11c, + BlindsBasement = 0x11d, + HypeCave = 0x11e, + KakarikoShopLumberjackHouse = 0x11f, + IceRodCave = 0x120, + SmithHouse = 0x121, + FortuneTellers = 0x122, + MiniMoldormCave = 0x123, + FiftyRupeeCaveBonkCave = 0x124, + TwentyRupeeCaveDarkHyliaSpikeHintCave = 0x125, + CheckerBoardCave = 0x126, + HammerPegCave = 0x127, + LostWoods = 0x8000, + LumberjackHouse = 0x8002, + DeathMountainWest = 0x8003, + DeathMountainEast = 0x8005, + DeathMountainTurtleWarp = 0x8007, + EntranceDeathMountain = 0x800a, + EntranceZorasDomain = 0x800f, + PathFromLostWoodsToKakariko = 0x8010, + KakarikoFortuneTeller = 0x8011, + NorthWestWhirlpoolWarp = 0x8012, + Sanctuary = 0x8013, + Graveyard = 0x8014, + River = 0x8015, + WitchHut = 0x8016, + EastOfWitchHut = 0x8017, + Kakariko = 0x8018, + CastleWoods = 0x801a, + HyruleCastle = 0x801b, + WitchGraveyardBridge = 0x801d, + EasternPalace = 0x801e, + Smithy = 0x8022, + StonyFieldNorth = 0x8025, + KakarikoMazeRace = 0x8028, + KakarikoLibrary = 0x8029, + HauntedGrove = 0x802a, + BetweenGroveAndLinksHouse = 0x802b, + LinksHouse = 0x802c, + StonyFieldSouth = 0x802d, + BetweenLakeHyliaAndEasternPalace = 0x802e, + FluteSpot5 = 0x802f, + Desert = 0x8030, + SouthOfHauntedGrove = 0x8032, + NorthwestSwamp = 0x8033, + NortheastSwamp = 0x8034, + LakeHylia = 0x8035, + IceCave = 0x8037, + DesertPass = 0x803a, + SouthwestSwamp = 0x803b, + SoutheastSwamp = 0x803c, + SouthShore = 0x803f, + ShullWoods = 0x8040, + DarkLumberjackShop = 0x8042, + DarkDeathMountainWest = 0x8043, + DarkDeathMountainEast = 0x8045, + TurtleRock = 0x8047, + BumperCave = 0x804a, + Catfish = 0x804f, + PathFromSkullWoodsToVillage = 0x8050, + OutcastFortuneTeller = 0x8051, + NorthWestPond = 0x8052, + DarkChapel = 0x8053, + DarkGraveyard = 0x8054, + DarkRiver = 0x8055, + DarkWitchHut = 0x8056, + EastOfDarkWitchHut = 0x8057, + VillageOfOutcasts = 0x8058, + FireShieldShopForest = 0x805a, + Pyramid = 0x805b, + BrokenBridge = 0x805d, + PalaceOfDarkness = 0x805e, + PurpleChestHouse = 0x8062, + DarkStonyFieldNorth = 0x8065, + DiggingGame = 0x8068, + TrappedFrog = 0x8069, + StumpysGrove = 0x806a, + BetweenGroveAndBombShop = 0x806b, + BombShop = 0x806c, + HammerPegBridge = 0x806d, + BetweenPODAndIceLake = 0x806e, + DarkFluteSpot5 = 0x806f, + MiseryMire = 0x8070, + SouthOfStumpysGrove = 0x8072, + DarkNorthwestSwamp = 0x8073, + DarkNortheastSwamp = 0x8074, + IceLake = 0x8075, + DarkIceCave = 0x8077, + BoxCanyonBetweenMireAndSwamp = 0x807a, + DarkSouthwestSwamp = 0x807b, + DarkSoutheastSwamp = 0x807c, + DarkSouthShore = 0x807f, + MasterSwordGladeAndUnderBridge = 0x8080, + ZorasDomain = 0x8081, +} + +use SuperTile::*; + +lazy_static! { + pub static ref ROOM_NAMES: HashMap = { + + let names = [ + (Ganon, "Ganon"), + (HyruleCastleNorthCorridor, "Hyrule Castle (North Corridor)"), + (HyruleCastleSwitchRoom, "Hyrule Castle (Switch Room)"), + (HoulihanRoom, "Houlihan Room"), + (TurtleRockCrystalRollerRoom, "Turtle Rock (Crystal Roller Room)"), + (EmptyCloneRoom0x05, "Empty Clone Room"), + (SwampPalaceArrghus, "Swamp Palace (Arrghus[Boss])"), + (TowerOfHeraMoldorm, "Tower of Hera (Moldorm[Boss])"), + (CaveHealingFairy, "Cave (Healing Fairy)"), + (PalaceOfDarknessMedusaChestRoom, "Palace of Darkness (Medusa Chest Room)"), + (PalaceOfDarknessStalfossTrapRoom, "Palace of Darkness (Stalfoss Trap Room)"), + (PalaceOfDarknessTurtleRoom, "Palace of Darkness (Turtle Room)"), + (GanonsTowerEntranceRoom, "Ganon's Tower (Entrance Room)"), + (GanonsTowerAgahnim2, "Ganaon's Tower (Agahnim2[Boss])"), + (IcePalaceEntranceRoom, "Ice Palace (Entrance Room)"), + (EmptyCloneRoom0x0F, "Empty Clone Room"), + (GanonEvacuationRoute, "Ganon Evacuation Route"), + (HyruleCastleBombableStockRoom, "Hyrule Castle (Bombable Stock Room)"), + (Sanctuary, "Sanctuary"), + (TurtleRockHokkuBokkuKeyRoom2, "Turtle Rock (Hokku-Bokku Key Room 2)"), + (TurtleRockBigKeyRoom, "Turtle Rock (Big Key Room)"), + (TurtleRockUselessTubes, "Turtle Rock (Useless Tubes)"), + (SwampPalaceSwimmingTreadmill, "Swamp Palace (Swimming Treadmill)"), + (TowerOfHeraMoldormFallRoom, "Tower of Hera (Moldorm Fall Room)"), + (Cave0x18BigFairyDropEntrance, "Big Fairy Drop Entrance Cave"), + (PalaceOfDarknessDarkMaze, "Palace of Darkness (Dark Maze)"), + (PalaceOfDarknessBigChestRoom, "Palace of Darkness (Big Chest Room)"), + (PalaceOfDarknessMimicsMovingWallRoom, "Palace of Darkness (Mimics / Moving Wall Room"), + (GanonsTowerIceArmos, "Ganon's Tower (Ice Armos)"), + (GanonsTowerFinalHallway, "Ganon's Tower (Final Hallway)"), + (IcePalaceBombFloorBariRoom, "Ice Palace (Bomb Floor / Bari Room)"), + (IcePalacePengatorBigKeyRoom, "Ice Palace (Pengator / Big Key Room)"), + (AgahnimsTowerAgahnim, "Agahnim's Tower (Agahnim[Boss])"), + (HyruleCastleKeyRatRoom, "Hyrule Castle (Key Rat Room)"), + (HyruleCastleSewerTextTriggerRoom, "Hyrule Castle (Sewer Text Trigger Room)"), + (TurtleRockWestExitToBalcony, "Turtle Rock (West Exit to Balcony)"), + (TurtleRockDoubleHokkuBokkuBigChestRoom, "Turtle Rock (Double Hokku-Bokku / Big Chest Room)"), + (EmptyCloneRoom0x25, "Empty Clone Room"), + (SwampPalaceStatueRoom, "Swamp Palace (Statue Room)"), + (TowerOfHeraBigChest, "Tower of Hera (Big Chest)"), + (SwampPalaceEntranceRoom, "Swamp Palace (Entrance Room)"), + (SkullWoodsMothula, "Skull Woods (Mothula[Boss])"), + (PalaceOfDarknessBigHubRoom, "Palace of Darkness (Big Hub Room)"), + (PalaceOfDarknessMapChestFairyRoom, "Palace of Darkness (Map Chest / Fairy Room)"), + (CaveHookshotCaveBackdoor, "Hookshot Cave Backdoor (Big Fairy)"), + (EmptyCloneRoom0x2D, "Empty Clone Room"), + (IcePalaceCompassRoom, "Ice Palace (Compass Room)"), + (CaveKakarikoWellHP, "Kakariko Well HP"), + (AgahnimsTowerMaidenSacrificeChamber, "Agahnim's Tower (Maiden Sacrifice Chamber)"), + (TowerOfHeraHardhatBeetleRoom, "Tower of Hera (Hardhat Beetles Room)"), + (HyruleCastleSewerKeyChestRoom, "Hyrule Castle (Sewer Key Chest Room)"), + (DesertPalaceLanmolas, "Desert Palace (Lanmolas[Boss])"), + (SwampPalacePushBlockPuzzlePreBigKeyRoom, "Swamp Palace (Push Block Puzzle / Pre-Big-Key Room"), + (SwampPalaceBigKeyBsRoom, "Swamp Palace (Big Key / BS Room)"), + (SwampPalaceBigChestRoom, "Swamp Palace (Big Chest Room)"), + (SwampPalaceMapChestWaterFillRoom, "Swamp Palace (Map Chest / Water Fill Room)"), + (SwampPalaceKeyPotRoom, "Swamp Palace (Key Pot Room)"), + (SkullWoodsGibdoKeyMothulaHoleRoom, "Skull Woods (Gibdo Key / Mothula Hole Room)"), + (PalaceOfDarknessBombableFloorRoom, "Palace of Darkness (Bombable Floor Room)"), + (PalaceOfDarknessSpikeBlockConveyorRoom, "Palace of Darkness (Spike Block / Conveyor Room)"), + (CaveHookshotCave, "Hookshot Cave"), + (GanonsTowerTorchRoom2, "Ganon's Tower (Torch Room 2)"), + (IcePalaceStalfosKnightConveyorHellway, "Ice Palace (Stalfos Knight / Conveyor Hellway)"), + (IcePalaceMapChestRoom, "Ice Palace (Map Chest Room)"), + (AgahnimsTowerFinalBridgeRoom, "Agahnim's Tower (Final Bridge Room)"), + (HyruleCastleFirstDarkRoom, "Hyrule Castle (First Dark Room)"), + (HyruleCastle6RopesRoom, "Hyrule Castle (6 Ropes Room)"), + (DesertPalaceTorchPuzzleMovingWallRoom, "Desert Palace (Torch Puzzle / Moving Wall Room)"), + (ThievesTownBigChestRoom, "Thieves' Town (Big Chest Room)"), + (ThievesTownJailCellsRoom, "Thieves' Town (Jail Cells Room)"), + (SwampPalaceCompassChestRoom, "Swamp Palace (Compass Chest Room)"), + (EmptyCloneRoom0x47, "Empty Clone Room"), + (EmptyCloneRoom0x48, "Empty Clone Room"), + (SkullWoodsGibdoTorchPuzzleRoom, "Skull Woods (Gibdo Torch Puzzle Room)"), + (PalaceOfDarknessEntranceRoom, "Palace of Darkness (Entrance Room)"), + (PalaceOfDarknessWarpsSouthMimicRoom, "Palace of Darkness (Warps / South Mimic Room)"), + (GanonsTowerMiniHelmasaurConveyorRoom, "Ganon's Tower (Mini-Helmasaur Conveyor Room)"), + (GanonsTowerMoldormRoom, "Ganon's Tower (Moldorm Room)"), + (IcePalaceBombJumpRoom, "Ice Palace (Bomb Jump Room)"), + (IcePalaceCloneRoomFairyRoom, "Ice Palace Clone Room (Fairy Room)"), + (HyruleCastleWestCorridor, "Hyrule Castle (West Corridor)"), + (HyruleCastleThroneRoom, "Hyrule Castle (Throne Room)"), + (HyruleCastleEastCorridor, "Hyrule Castle (East Corridor)"), + (DesertPalacePopos2BeamosHellwayRoom, "Desert Palace (Popos 2 / Beamos Hellway Room)"), + (SwampPalaceUpstairsPitsRoom, "Swamp Palace (Upstairs Pits Room)"), + (CastleSecretEntranceUncleRoom, "Castle Secret Entrance / Uncle Room"), + (SkullWoodsKeyPotTrapRoom, "Skull Woods (Key Pot / Trap Room)"), + (SkullWoodsBigKeyRoom, "Skull Woods (Big Key Room)"), + (SkullWoodsBigChestRoom, "Skull Woods (Big Chest Room)"), + (SkullWoodsFinalSectionEntranceRoom, "Skull Woods (Final Section Entrance Room)"), + (PalaceOfDarknessHelmasaurKing, "Palace of Darkness (Helmasaur King[Boss])"), + (GanonsTowerSpikePitRoom, "Ganon's Tower (Spike Pit Room)"), + (GanonsTowerGanonBallZ, "Ganon's Tower (Ganon-Ball Z)"), + (GanonsTowerGauntlet123, "Ganon's Tower (Gauntlet 1/2/3)"), + (IcePalaceLonelyFirebar, "Ice Palace (Lonely Firebar)"), + (IcePalaceHiddenChestSpikeFloorRoom, "Ice Palace (Hidden Chest / Spike Floor Room)"), + (HyruleCastleWestEntranceRoom, "Hyrule Castle (West Entrance Room)"), + (HyruleCastleMainEntranceRoom, "Hyrule Castle (Main Entrance Room)"), + (HyruleCastleEastEntranceRoom, "Hyrule Castle (East Entrance Room)"), + (DesertPalaceFinalSectionEntranceRoom, "Desert Palace (Final Section Entrance Room)"), + (ThievesTownWestAtticRoom, "Thieves' Town (West Attic Room)"), + (ThievesTownEastAtticRoom, "Thieves' Town (East Attic Room)"), + (SwampPalaceHiddenChestHiddenDoorRoom, "Swamp Palace (Hidden Chest / Hidden Door Room)"), + (SkullWoodsCompassChestRoom, "Skull Woods (Compass Chest Room)"), + (SkullWoodsKeyChestTrapRoom, "Skull Woods (Key Chest / Trap Room)"), + (EmptyCloneRoom0x69, "Empty Clone Room"), + (PalaceOfDarknessRupeeRoom, "Palace of Darkness (Rupee Room)"), + (GanonsTowerMimicsRoom, "Ganon's Tower (Mimics Room)"), + (GanonsTowerLanmolasRoom, "Ganon's Tower (Lanmolas Room"), + (GanonsTowerGauntlet45, "Ganon's Tower (Gauntlet 4/5)"), + (IcePalacePengatorsRoom, "Ice Palace (Pengators Room)"), + (EmptyCloneRoom0x6F, "Empty Clone Room"), + (HyruleCastleSmallCorridorToJailCells, "Hyrule Castle (Small Corridor to Jail Cells"), + (HyruleCastleBoomerangChestRoom, "Hyrule Castle (Boomerang Chest Room)"), + (HyruleCastleMapChestRoom, "Hyrule Castle (Map Chest Room)"), + (DesertPalaceBigChestRoom, "Desert Palace (Big Chest Room)"), + (DesertPalaceMapChestRoom, "Desert Palace (Map Chest Room)"), + (DesertPalaceBigKeyChestRoom, "Desert Palace (Big Key Chest Room)"), + (SwampPalaceWaterDrainRoom, "Swamp Palace (Water Drain Room)"), + (TowerOfHeraEntranceRoom, "Tower of Hera (Entrance Room)"), + (EmptyCloneRoom0x78, "Empty Clone Room"), + (EmptyCloneRoom0x79, "Empty Clone Room"), + (EmptyCloneRoom0x7A, "Empty Clone Room"), + (GanonsTowerMisc, "Ganon's Tower (Sideways Conveyors / 4 Chest 4 Shooter / Square Pit"), + (GanonsTowerEastSideCollapsingBridgeExplodingWallRoom, "Ganon's Tower (East Side Collapsing Bridge / Exploding Wall Room)"), + (GanonsTowerWinderWarpMazeRoom, "Ganon's Tower (Winder / Warp Maze Room)"), + (IcePalaceHiddenChestBombableFloorRoom, "Ice Palace (Hidden Chest / Bombable Floor Room)"), + (IcePalaceBigSpikeTrapsRoom, "Ice Palace (Big Spike Traps Room)"), + (HyruleCastleJailCellRoom, "Hyrule Castle (Jail Cell Room)"), + (HyruleCastleNextToChasmRoom, "Hyrule Castle (Next to Chasm Room)"), + (HyruleCastleBasementChasmRoom, "Hyrule Castle (Basement Chasm Room)"), + (DesertPalaceWestEntranceRoom, "Desert Palace (West Entrance Room)"), + (DesertPalaceMainEntranceRoom, "Desert Palace (Main Entrance Room)"), + (DesertPalaceEastEntranceRoom, "Desert Palace (East Entrance Room)"), + (EmptyCloneRoom0x86, "Empty Clone Room"), + (TowerOfHeraTileRoom, "Tower of Hera (Tile Room)"), + (EmptyCloneRoom0x88, "Empty Clone Room"), + (EasternPalaceFairyRoom, "Eastern Palace (Fairy Room)"), + (EmptyCloneRoom0x8A, "Empty Clone Room"), + (GanonsTowerBlockPuzzleSpikeSkipMapChestRoom, "Ganon's Tower (Block Puzzle / Spike Skip / Map Chest Room)"), + (GanonsTowerEastAndWestDownstairsBigChestRoom, "Ganon's Tower (East and West Downstairs / Big Chest Room)"), + (GanonsTowerTileTorchPuzzleRoom, "Ganon's Tower (Tile / Torch Puzzle Room)"), + (IcePalaceBlobsWithTetrisBarrier, "Ice Palace (Blobs with Tetris Barrier)"), + (EmptyCloneRoom0x8F, "Empty Clone Room"), + (MiseryMireVitreous, "Misery Mire (Vitreous[Boss])"), + (MiseryMireFinalSwitchRoom, "Misery Mire (Final Switch Room)"), + (MiseryMireDarkBombWallSwitchesRoom, "Misery Mire (Dark Bomb Wall / Switches Room)"), + (MiseryMireDarkCaneFLoorSwitchPuzzleRoom, "Misery Mire (Dark Cane FLoor Switch Puzzle Room)"), + (EmptyCloneRoom0x94, "Empty Clone Room"), + (GanonsTowerFinalCollapsingBridgeRoom, "Ganon's Tower (Final Collapsing Bridge Room)"), + (GanonsTowerTorches1Room, "Ganon's Tower (Torches 1 Room)"), + (MiseryMireTorchPuzzleMovingWallRoom, "Misery Mire (Torch Puzzle / Moving Wall Room)"), + (MiseryMireEntranceRoom, "Misery Mire (Entrance Room)"), + (EasternPalaceEyegoreKeyRoom, "Eastern Palace (Eyegore Key Room)"), + (EmptyCloneRoom0x9A, "Empty Clone Room"), + (GanonsTowerManySpikesWarpMazeRoom, "Ganon's Tower (Many Spikes / Warp Maze Room)"), + (GanonsTowerInvisibleFloorMazeRoom, "Ganon's Tower (Invisible Floor Maze Room)"), + (GanonsTowerCompassChestInvisibleFloorRoom, "Ganon's Tower (Compass Chest / Invisible Floor Room)"), + (IcePalaceBigChestRoom, "Ice Palace (Big Chest Room)"), + (IcePalaceIceFloorPotsKeyAndSwitch, "Ice Palace (Room with ice floor, key, and 4 wall rats)"), + (MiseryMirePreVitreousRoom, "Misery Mire (Pre-Vitreous Room)"), + (MiseryMireFishRoom, "Misery Mire (Fish Room)"), + (MiseryMireBridgeKeyChestRoom, "Misery Mire (Bridge Key Chest Room)"), + (MiseryMireEmptyLConnectingRoom, "Misery Mire (Empty 'L' Connecting Room)"), + (TurtleRockTrinexx, "Turtle Rock (Trinexx[Boss])"), + (GanonsTowerWizzrobesRooms, "Ganon's Tower (Wizzrobes Rooms)"), + (GanonsTowerMoldormFallRoom, "Ganon's Tower (Moldorm Fall Room)"), + (TowerOfHeraFairyRoom, "Tower of Hera (Fairy Room)"), + (EasternPalaceStalfosSpawnRoom, "Eastern Palace (Staflos Spawn Room)"), + (EasternPalaceBigChestRoom, "Eastern Palace (Big Chest Room)"), + (EasternPalaceMapChestRoom, "Eastern Palace (Map Chest Room)"), + (ThievesTownMovingSpikesKeyPotRoom, "Thieves' Town (Moving Spikes / Key Pot Room)"), + (ThievesTownBlindTheThief, "Thieves' Town (Blind the Thief[Boss])"), + (EmptyCloneRoom0xAD, "Empty Clone Room"), + (IcePalaceIceTRoom, "Ice Palace (Ice T Room)"), + (IcePalaceIceBridgeRoom, "Ice Palace (Ice Bridge Room)"), + (AgahnimsTowerCircleOfPots, "Agahnim's Tower (Circle of Pots)"), + (MiseryMireHourglassRoom, "Misery Mire (Hourglass Room)"), + (MiseryMireSlugRoom, "Misery Mire (Slug Room)"), + (MiseryMireSpikeKeyChestRoom, "Misery Mire (Spike Key Chest Room)"), + (TurtleRockPreTrinexxRoom, "Turtle Rock (Pre-Trinexx Room)"), + (TurtleRockDarkMaze, "Turtle Rock (Dark Maze)"), + (TurtleRockChainChompsRoom, "Turtle Rock (Chain Chomps Room)"), + (TurtleRockMapChestKeyChestRollerRoom, "Turtle Rock (Map Chest / Key Chest / Roller Room)"), + (EasternPalaceBigKeyRoom, "Eastern Palace (Big Key Room)"), + (EasternPalaceLobbyCannonballsRoom, "Eastern Palace (Lobby Cannonballs Room)"), + (EasternPalaceDarkAntifairyKeyPotRoom, "Eastern Palace (Dark Antifairy / Key Pot Room)"), + (ThievesTownHellway, "Thieves' Town (Hellway)"), + (ThievesTownConveyorToilet, "Thieves' Town (Conveyor Toilet)"), + (EmptyCloneRoom0xBD, "Empty Clone Room"), + (IcePalaceBlockPuzzleRoom, "Ice Palace (Block Puzzle Room)"), + (IcePalaceCloneRoomSwitchRoom, "Ice Palace Clone Room (Switch Room)"), + (AgahnimsTowerDarkBridgeRoom, "Agahnim's Tower (Dark Bridge Room)"), + (MiseryMireCompassChestTileRoom, "Misery Mire (Compass Chest / Tile Room)"), + (MiseryMireBigHubRoom, "Misery Mire (Big Hub Room)"), + (MiseryMireBigChestRoom, "Misery Mire (Big Chest Room)"), + (TurtleRockFinalCrystalSwitchPuzzleRoom, "Turtle Rock (Final Crystal Switch Puzzle Room)"), + (TurtleRockLaserBridge, "Turtle Rock (Laser Bridge)"), + (TurtleRockSomariaHub, "Turtle Rock (Somaria Hub Room)"), + (TurtleRockTorchPuzzle, "Turtle Rock (Torch Puzzle)"), + (EasternPalaceArmosKnights, "Eastern Palace (Armos Knights[Boss])"), + (EasternPalaceEntranceRoom, "Eastern Palace (Entrance Room)"), + (UnknownRoom, "Unused Room??"), + (ThievesTownNorthWestEntranceRoom, "Thieves' Town (North West Entrance Room)"), + (ThievesTownNorthEastEntranceRoom, "Thieves' Town (North East Entrance Room)"), + (EmptyCloneRoom0xCD, "Empty Clone Room"), + (IcePalaceHoleToKholdstareRoom, "Ice Palace (Hole to Kholdstare Room)"), + (EmptyCloneRoom0xCF, "Empty Clone Room"), + (AgahnimsTowerDarkMaze, "Agahnim's Tower (Dark Maze)"), + (MiseryMireConveyorSlugBigKeyRoom, "Misery Mire (Conveyor Slug / Big Key Room)"), + (MiseryMireWizzrobesRoom, "Misery Mire (Wizzrobes Room)"), + (EmptyCloneRoom0xD3, "Empty Clone Room"), + (EmptyCloneRoom0xD4, "Empty Clone Room"), + (TurtleRockLaserKeyRoom, "Turtle Rock (Laser Key Room)"), + (TurtleRockEntranceRoom, "Turtle Rock (Entrance Room)"), + (EmptyCloneRoom0xD7, "Empty Clone Room"), + (EasternPalacePreArmosKnightsRoom, "Eastern Palace (Pre-Armos-Knights Room)"), + (EasternPalaceCannonballRoom, "Eastern Palace (Cannonball Room)"), + (EasternPalacePotSwitchRoom, "Eastern Palace (Pot Switch Room)"), + (ThievesTownSouthWestEntranceRoom, "Thieves' Town (South West Entrance Room)"), + (ThievesTownSouthEastEntranceRoom, "Thieves Town (South East Entrance Room)"), + (EmptyCloneRoom0xDD, "Empty Clone Room"), + (IcePalaceKholdstare, "Ice Palace (Kholdstare[Boss])"), + (CaveBackwardsDeathMountainTopFloor, "Death Mountain Exit (Top)"), + (AgahnimsTowerEntranceRoom, "Agahnim's Tower (Entrance Room)"), + (CaveLostWoodsHP, "Cave (Lost Woods HP)"), + (CaveLumberjacksTreeHP, "Cave (Lumberjacks' Tree HP)"), + (CaveMagicBat, "Cave (Magic Bat)"), + (CaveLostOldManHouse, "Cave (Lost Old Man's House)"), + (CaveLostOldManHouseBack, "Cave (Lost Old Man's House Back)"), + (Cave0xE6, "Cave (Bunch of Keese)"), + (Cave0xE7, "Cave (Bunch of Keese 2)"), + (CaveSuperBunnyTop, "Cave (Super Bunny Top)"), + (EmptyCloneRoom0xE9, "Empty Clone Room"), + (CaveSpectacleRockHP, "Cave (Inside Spectacle Rock HP)"), + (CaveBumperCaveTop, "Cave (Bumper Cave Top)"), + (EmptyCloneRoom0xEC, "Empty Clone Room"), + (Cave0xED, "Cave (??)"), + (CaveSpiralCave, "Cave (Spiral Cave)"), + (CaveCrystalSwitch5ChestsRoom, "Cave (Crystal Switch / 5 Chests Room)"), + (CaveLostOldManStartingCaveBottom, "Cave (Lost Old Man Starting Cave Bottom)"), + (CaveLostOldManStartingCaveTop, "Cave (Lost Old Man Starting Cave Top)"), + (HouseOldWomanNextDoor, "House (Old Woman Next Door)"), + (HouseOldWomanSahasrahlasWifeMaybe, "House (Old Woman / Sahasrahla's Wife?)"), + (HouseAngryBrothersWest, "House (Angry Brothers West)"), + (HouseAngryBrothersEast, "House (Angry Brothers East)"), + (EmptyCloneRoom0xF6, "Empty Clone Room"), + (EmptyCloneRoom0xF7, "Empty Clone Room"), + (CaveSuperBunnyBottom, "Cave (Super Bunny Bottom)"), + (CaveSpectacleRockExit, "Cave (Spectacle Rock Exit)"), + (CaveSpectacleRockPrizeEntrance, "Cave (Spectacle Rock Prize Entrance)"), + (CaveBumperCaveBottom, "Cave (Bumper Cave Bottom)"), + (EmptyCloneRoom0xFC, "Empty Clone Room"), + (Cave0xFD, "Cave (Death Mountain)"), + (CaveSpiralCaveExit, "Cave (Spiral Cave Exit)"), + (CaveParadoxCaveMiddleEntrance, "Cave (Paradox Cave 'Middle' Entrance)"), + (ShopInLostWoods0x100, "Shop in Lost Woods"), + (ScaredLadyHouses, "Scared Ladies' Houses"), + (SickKid, "Sick Kid"), + (InnBushHouse, "Inn / BushHouse"), + (LinksHouse, "Link's House"), + (ShabadooHouse, "Sahasrahla's House"), + (ChestGameBombHouse, "Chest Game / Outcast Village Bomb House"), + (LibraryBombFarmRoom, "Library / Bomb Farm Room"), + (ChickenHouse, "Chicken House"), + (WitchHut, "Witch Hut"), + (Aginah, "Aginah's Cave"), + (Dam, "Dam"), + (MimicCave, "Mimic Cave"), + (CaveOutsideMiseryMire, "Cave Outside Misery Mire"), + (CaveDarkHyliaLedgeHint, "Cave (Dark Hylia Ledge Hint)"), + (ShopDarkWorldShops, "Shop (Dark World)"), + (ShopFireShieldShop, "Shop (Fire Shield Shop)"), + (ArcherGame, "Archery Game"), + (CaveShop, "Cave (Shop)"), + (KingsTomb, "King's Tomb"), + (WaterfallCaveMireHint, "Waterfall Cave / Cave (Mire Hint)"), + (BigFairy, "Big Fairy"), + (FatFairy, "Fat Fairy"), + (SpikeCave, "Spike Cave"), + (ChestGamblingGame, "Chest Gambling Game"), + (BlindsHouse, "Blind's House"), + (Mutant, "Mutant Hut"), + (MirrorCaveGroveAndTomb, "Mirror Caves (South of Grove / Graveyard Ledge)"), + (BombShop, "Bomb Shop"), + (BlindsBasement, "Blind's Basement"), + (HypeCave, "Hype Cave"), + (KakarikoShopLumberjackHouse, "Kakariko Shop / Lumberjacks' House"), + (IceRodCave, "Ice Rod Cave"), + (SmithHouse, "Smiths' House"), + (FortuneTellers, "Fortune Tellers"), + (MiniMoldormCave, "Mini-Moldorm Cave"), + (FiftyRupeeCaveBonkCave, "50 Rupee Cave / Bonk Cave"), + (TwentyRupeeCaveDarkHyliaSpikeHintCave, "20 Rupee Cave / Dark Hylia Spike Hint Cave"), + (CheckerBoardCave, "Checker Board Cave"), + (HammerPegCave, "Hammer Peg Cave"), + ]; + + HashMap::from_iter(names.into_iter()) + }; +} + +pub const NEED_KILLABLE_ROOMS: [SuperTile; 39] = [ + EasternPalaceBigKeyRoom, // For anti-fairy circle around pot switch + PalaceOfDarknessTurtleRoom, + PalaceOfDarknessMimicsMovingWallRoom, + PalaceOfDarknessWarpsSouthMimicRoom, + TurtleRockCrystalRollerRoom, // Greed room has a kill-all section + TurtleRockDoubleHokkuBokkuBigChestRoom, + TurtleRockChainChompsRoom, + SwampPalaceEntranceRoom, + IcePalaceEntranceRoom, // Not normally required to backtrack, except in doors intensity 3 + IcePalaceCompassRoom, + IcePalaceStalfosKnightConveyorHellway, + IcePalacePengatorsRoom, + TowerOfHeraHardhatBeetleRoom, + TowerOfHeraTileRoom, + ThievesTownBigChestRoom, // For trap room north of big chest + ThievesTownJailCellsRoom, // For room with stairs and big lift block + DesertPalacePopos2BeamosHellwayRoom, + DesertPalaceBigKeyChestRoom, + DesertPalaceEastEntranceRoom, + GanonsTowerTorchRoom2, + GanonsTowerGauntlet123, + GanonsTowerMimicsRoom, + GanonsTowerGauntlet45, + GanonsTowerMisc, + GanonsTowerWinderWarpMazeRoom, + GanonsTowerTileTorchPuzzleRoom, + GanonsTowerTorches1Room, + GanonsTowerWizzrobesRooms, + HyruleCastleBoomerangChestRoom, + EasternPalaceStalfosSpawnRoom, + EasternPalacePreArmosKnightsRoom, + AgahnimsTowerCircleOfPots, + AgahnimsTowerDarkBridgeRoom, // Not normally required to go backwards, except in basic/crossed doors, or doing glitch shenanigans. + AgahnimsTowerEntranceRoom, + MiseryMireSlugRoom, + MiseryMireWizzrobesRoom, + CaveCrystalSwitch5ChestsRoom, + MimicCave, + MiniMoldormCave, +]; + +// All the rooms that require special handling. + +/// These rooms need to be locked to GFX ID 28. +pub const FREEZOR_ROOMS: [SuperTile; 5] = [ + IcePalaceEntranceRoom, + IcePalaceHiddenChestBombableFloorRoom, + IcePalaceBlobsWithTetrisBarrier, + IcePalaceBigChestRoom, + IcePalaceBlockPuzzleRoom, +]; + +/// These rooms need to be locked to GFX ID 17. +pub const WATER_ROOMS: [SuperTile; 33] = [ + GanonsTowerEntranceRoom, + GanonsTowerAgahnim2, + SwampPalaceSwimmingTreadmill, + GanonsTowerIceArmos, + GanonsTowerFinalHallway, + SwampPalaceEntranceRoom, + SwampPalacePushBlockPuzzlePreBigKeyRoom, + SwampPalaceBigChestRoom, + SwampPalaceKeyPotRoom, + GanonsTowerTorchRoom2, + SwampPalaceCompassChestRoom, + GanonsTowerMiniHelmasaurConveyorRoom, + GanonsTowerMoldormRoom, + GanonsTowerSpikePitRoom, + GanonsTowerGanonBallZ, + GanonsTowerGauntlet123, + SwampPalaceHiddenChestHiddenDoorRoom, + GanonsTowerMimicsRoom, + GanonsTowerLanmolasRoom, + GanonsTowerGauntlet45, + GanonsTowerMisc, + GanonsTowerEastSideCollapsingBridgeExplodingWallRoom, + GanonsTowerWinderWarpMazeRoom, + GanonsTowerBlockPuzzleSpikeSkipMapChestRoom, + GanonsTowerEastAndWestDownstairsBigChestRoom, + GanonsTowerTileTorchPuzzleRoom, + GanonsTowerFinalCollapsingBridgeRoom, + GanonsTowerTorches1Room, + GanonsTowerManySpikesWarpMazeRoom, + GanonsTowerInvisibleFloorMazeRoom, + GanonsTowerCompassChestInvisibleFloorRoom, + GanonsTowerWizzrobesRooms, + GanonsTowerMoldormFallRoom, +]; + +pub const SHADOW_ROOMS: [SuperTile; 2] = [ + IcePalaceStalfosKnightConveyorHellway, + IcePalaceIceFloorPotsKeyAndSwitch, +]; + +pub const WALL_MASTER_ROOMS: [SuperTile; 5] = [ + SkullWoodsGibdoKeyMothulaHoleRoom, + SkullWoodsGibdoTorchPuzzleRoom, + SkullWoodsKeyPotTrapRoom, + SkullWoodsKeyChestTrapRoom, + GanonsTowerTileTorchPuzzleRoom, +]; + +pub const BUMPER_CRYSTAL_LASER_ROOMS: [SuperTile; 42] = [ + TurtleRockCrystalRollerRoom, + PalaceOfDarknessTurtleRoom, + TurtleRockHokkuBokkuKeyRoom2, + TowerOfHeraMoldormFallRoom, + PalaceOfDarknessMimicsMovingWallRoom, + IcePalaceBombFloorBariRoom, + PalaceOfDarknessBigHubRoom, + PalaceOfDarknessMapChestFairyRoom, + TowerOfHeraHardhatBeetleRoom, + GanonsTowerTorchRoom2, + AgahnimsTowerFinalBridgeRoom, + ThievesTownBigChestRoom, + PalaceOfDarknessEntranceRoom, + GanonsTowerMiniHelmasaurConveyorRoom, + SkullWoodsBigChestRoom, + SkullWoodsFinalSectionEntranceRoom, + GanonsTowerSpikePitRoom, + SkullWoodsCompassChestRoom, + SkullWoodsKeyChestTrapRoom, + GanonsTowerMimicsRoom, + TowerOfHeraEntranceRoom, + IcePalaceHiddenChestBombableFloorRoom, + TowerOfHeraTileRoom, + GanonsTowerBlockPuzzleSpikeSkipMapChestRoom, + MiseryMireFinalSwitchRoom, + MiseryMireDarkBombWallSwitchesRoom, + GanonsTowerTorches1Room, + GanonsTowerManySpikesWarpMazeRoom, + GanonsTowerCompassChestInvisibleFloorRoom, + MiseryMireFishRoom, + GanonsTowerWizzrobesRooms, + ThievesTownMovingSpikesKeyPotRoom, + TurtleRockChainChompsRoom, + IcePalaceCloneRoomSwitchRoom, + MiseryMireCompassChestTileRoom, + MiseryMireBigChestRoom, + TurtleRockFinalCrystalSwitchPuzzleRoom, + TurtleRockLaserBridge, + TurtleRockLaserKeyRoom, + TurtleRockEntranceRoom, + CaveBumperCaveTop, + CaveCrystalSwitch5ChestsRoom, +]; + +pub const PULL_SWITCH_ROOMS: [SuperTile; 4] = [ + HyruleCastleSwitchRoom, + SkullWoodsBigChestRoom, + ThievesTownWestAtticRoom, + Dam, +]; + +pub const TONGUE_ROOMS: [SuperTile; 7] = [ + TurtleRockCrystalRollerRoom, + TurtleRockWestExitToBalcony, + SwampPalaceBigKeyBsRoom, + SwampPalaceMapChestWaterFillRoom, + IcePalaceMapChestRoom, + SwampPalaceWaterDrainRoom, + IcePalaceHoleToKholdstareRoom, +]; + +pub const NO_STATUE_ROOMS: [SuperTile; 12] = [ + SwampPalaceSwimmingTreadmill, + SwampPalaceStatueRoom, + SwampPalaceEntranceRoom, + PalaceOfDarknessMapChestFairyRoom, + SwampPalacePushBlockPuzzlePreBigKeyRoom, + SwampPalaceBigChestRoom, + SwampPalaceCompassChestRoom, + SkullWoodsBigKeyRoom, + SwampPalaceWaterDrainRoom, + MiseryMireHourglassRoom, + MiseryMireBigHubRoom, + AgahnimsTowerDarkMaze, +]; + +pub const MOVING_CANNONS_ROOMS: [SuperTile; 2] = [ + GanonsTowerGanonBallZ, + DesertPalaceBigKeyChestRoom, +]; + +pub const CANNON_ROOMS: [SuperTile; 3] = [ + EasternPalaceLobbyCannonballsRoom, + EasternPalaceCannonballRoom, + MimicCave, // ??? +]; + +/// These rooms have no enemies to randomize, or are special bosses that can't be switched out. +pub const DONT_RANDOMIZE_ROOMS: [SuperTile; 8] = [ + Ganon, + HyruleCastleNorthCorridor, + HoulihanRoom, + GanonsTowerAgahnim2, + TurtleRockBigKeyRoom, + AgahnimsTowerAgahnim, + AgahnimsTowerMaidenSacrificeChamber, + IcePalaceBigSpikeTrapsRoom, // Anything else would be too easy? +]; + +pub const NO_SPECIAL_ENEMIES_IN_STANDARD_MODE_ROOMS: [SuperTile; 21] = [ + HyruleCastleNorthCorridor, + HyruleCastleSwitchRoom, + HyruleCastleBombableStockRoom, + HyruleCastleKeyRatRoom, + HyruleCastleSewerTextTriggerRoom, + HyruleCastleSewerKeyChestRoom, + HyruleCastleFirstDarkRoom, + HyruleCastle6RopesRoom, + HyruleCastleWestCorridor, + HyruleCastleThroneRoom, + HyruleCastleEastCorridor, + CastleSecretEntranceUncleRoom, + HyruleCastleWestEntranceRoom, + HyruleCastleMainEntranceRoom, + HyruleCastleEastEntranceRoom, + HyruleCastleSmallCorridorToJailCells, + HyruleCastleBoomerangChestRoom, + HyruleCastleMapChestRoom, + HyruleCastleJailCellRoom, + HyruleCastleNextToChasmRoom, + HyruleCastleBasementChasmRoom, +]; + +pub const BOSS_ROOMS: [SuperTile; 16] = [ + Ganon, + SwampPalaceArrghus, + TowerOfHeraMoldorm, + GanonsTowerAgahnim2, + GanonsTowerIceArmos, + AgahnimsTowerAgahnim, + SkullWoodsMothula, + DesertPalaceLanmolas, + GanonsTowerMoldormRoom, + PalaceOfDarknessHelmasaurKing, + GanonsTowerLanmolasRoom, + MiseryMireVitreous, + TurtleRockTrinexx, + ThievesTownBlindTheThief, + EasternPalaceArmosKnights, + IcePalaceKholdstare, +]; + +pub const DONT_USE_IMMOVABLE_ENEMIES: [SuperTile; 57] = [ + PalaceOfDarknessTurtleRoom, // for single terrorpin in L shaped section + SwampPalaceSwimmingTreadmill, + PalaceOfDarknessDarkMaze, // top placement will probably block maze + IcePalaceBombFloorBariRoom, // TODO: test + SwampPalaceStatueRoom, + TowerOfHeraBigChest, // TODO: test, top left placement before stairs up + SwampPalaceBigChestRoom, // bottom left waterbug + IcePalaceMapChestRoom, // spikes block stuff + HyruleCastle6RopesRoom, // only if two of them stack, but why chance it? + AgahnimsTowerFinalBridgeRoom, // spikes are bad here + SwampPalaceCompassChestRoom, + SkullWoodsGibdoTorchPuzzleRoom, + PalaceOfDarknessWarpsSouthMimicRoom, + IcePalaceBombJumpRoom, // for the zols in the small next room + CastleSecretEntranceUncleRoom, // TODO: test + SkullWoodsBigKeyRoom, + IcePalaceHiddenChestSpikeFloorRoom, // TODO: would this cause problems in OHKO since you can't hookshot across if middle mob is beamos, etc? + ThievesTownEastAtticRoom, // only if both bottom rats + PalaceOfDarknessRupeeRoom, // only if two terrorpins next to each other (but can happen twice) + DesertPalaceMapChestRoom, // only if both antlions in hallway to the south of chest + SwampPalaceWaterDrainRoom, // would need 3 mobs to be impassible to possibly softlock... + GanonsTowerWinderWarpMazeRoom, // very low chance + IcePalaceBigSpikeTrapsRoom, // TODO: what happens to beamos over a pit? + DesertPalaceWestEntranceRoom, // TODO: test + DesertPalaceMainEntranceRoom, // TODO: test + DesertPalaceEastEntranceRoom, // TODO: test + GanonsTowerEastAndWestDownstairsBigChestRoom, // TODO: test, probably safe? + GanonsTowerTileTorchPuzzleRoom, // TODO: test + MiseryMireDarkBombWallSwitchesRoom, // TODO: test + GanonsTowerFinalCollapsingBridgeRoom, // TODO: test, probably safe because of conveyor belts + MiseryMireEntranceRoom, + GanonsTowerManySpikesWarpMazeRoom, // TODO: test, middle spike covers warp, are we randomizing those? + GanonsTowerInvisibleFloorMazeRoom, // TODO: test + GanonsTowerCompassChestInvisibleFloorRoom, // TODO: test + IcePalaceBigChestRoom, // Big spikes will block + MiseryMirePreVitreousRoom, // TODO: test + EasternPalaceMapChestRoom, // TODO: test + IcePalaceIceBridgeRoom, + MiseryMireSpikeKeyChestRoom, // TODO: test lower stalfos blocking door + EasternPalaceDarkAntifairyKeyPotRoom, // TODO: test lower stalfos blocking door + ThievesTownHellway, // TODO: test, but should be ok + ThievesTownConveyorToilet, // TODO: test + TurtleRockSomariaHub, // technically a door is blocked off, but who would ever go there? (Door rando would) + ThievesTownNorthWestEntranceRoom, + IcePalaceHoleToKholdstareRoom, // spikes block stuff + AgahnimsTowerDarkMaze, // TODO: test + MiseryMireWizzrobesRoom, + TurtleRockLaserKeyRoom, + EasternPalacePreArmosKnightsRoom, + ThievesTownSouthEastEntranceRoom, // TODO: test + CaveBackwardsDeathMountainTopFloor, + CaveLostOldManHouse, + Cave0xE7, + CaveSpiralCave, + CaveSpectacleRockExit, // TODO: test, probably can get past + Cave0xFD, + MimicCave, +]; + +pub const DONT_USE_FLYING_ENEMIES: [SuperTile; 2] = [ + MiseryMireWizzrobesRoom, + MimicCave, +]; diff --git a/enemize/src/graph/mod.rs b/enemize/src/graph/mod.rs new file mode 100644 index 0000000..eb2d62b --- /dev/null +++ b/enemize/src/graph/mod.rs @@ -0,0 +1,3 @@ +pub mod dungeons; +pub mod item; +pub mod requirement; diff --git a/enemize/src/graph/requirement.rs b/enemize/src/graph/requirement.rs new file mode 100644 index 0000000..2bbece0 --- /dev/null +++ b/enemize/src/graph/requirement.rs @@ -0,0 +1,16 @@ +use std::collections::HashSet; + +use super::item::Item; + +pub struct Requirement(HashSet); + +impl Requirement { + pub fn new(items: &[Item]) -> Requirement { + Requirement(HashSet::from_iter(items.into_iter().map(|i| *i))) + } + + pub fn is_met(&self, items: &[Item]) -> bool { + let items = HashSet::from_iter(items.into_iter().map(|i| *i)); + self.0.is_subset(&items) + } +} diff --git a/enemize/src/lib.rs b/enemize/src/lib.rs index d2ba5b0..d130be2 100644 --- a/enemize/src/lib.rs +++ b/enemize/src/lib.rs @@ -1,41 +1,80 @@ -use std::io::Read; +use std::fs::File; +use std::marker::PhantomData; use std::path::{Path, PathBuf}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use crate::rom::RomData; + +pub mod asar; +pub mod bosses; +pub mod constants; +pub mod graph; +pub mod option_flags; +pub mod randomize; pub mod rom; -#[derive(Serialize)] +#[derive(Debug, Error)] +#[error("Not a valid value for {0:?}")] +pub struct InvalidEnumError(PhantomData); + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct Patch { pub address: usize, pub patch_data: Vec, } -#[derive(Serialize)] +#[derive(Default)] pub struct PatchSet { filename: PathBuf, - patches: Vec + patches: Vec, } impl PatchSet { - pub fn load(filename: Path) -> Result { + pub fn load(filename: &Path) -> Result { let patches = { - let mut file = File::open(filename)?; - let mut buffer = std::io::BufReader::new(file); + let file = File::open(filename)?; + let buffer = std::io::BufReader::new(file); serde_json::from_reader(buffer)? }; - PatchSet { + Ok(PatchSet { filename: filename.into(), - patches: patches - } + patches: patches, + }) } - pub fn filename(&self) -> Path { + pub fn save(&self) -> anyhow::Result<()> { + let file = File::create(&self.filename)?; + let buffer = std::io::BufWriter::new(file); + serde_json::to_writer(buffer, &self.patches)?; + + Ok(()) + } + + pub fn save_to_file(&self, filename: &Path) -> anyhow::Result<()> { + let file = File::create(filename)?; + let buffer = std::io::BufWriter::new(file); + serde_json::to_writer(buffer, &self.patches)?; + + Ok(()) + } + + pub fn add_patch(&mut self, patch: Patch) { + self.patches.push(patch); + } + + pub fn add_patches(&mut self, patches: Vec) { + self.patches.extend(patches); + } + + pub fn filename(&self) -> &Path { self.filename.as_path() } - pub fn patchRom(&self, &mut rom: RomData) { - for patch in self.patches { + pub fn patch_rom(&self, rom: &mut RomData) { + for patch in self.patches.iter() { rom.patch_data(patch); } } diff --git a/enemize/src/main.rs b/enemize/src/main.rs index e7a11a9..fcb4f5e 100644 --- a/enemize/src/main.rs +++ b/enemize/src/main.rs @@ -1,3 +1,88 @@ -fn main() { - println!("Hello, world!"); +use std::fs::File; +use std::io::prelude::*; +use std::path::PathBuf; +use std::time::Instant; + +use anyhow::ensure; +use clap::Parser; + +use enemize::{asar, rom::RomData}; + +const ASAR_SYMBOLS: &'static str = include_str!("../../asar_symbols.txt"); +const ENEMIZER_BASE_PATCH: &'static str = include_str!("../../enemizer_base_patch.json"); + +/// Randomizes enemy placements in The Legend of Zelda: A Link to the Past for the Super Nintendo +/// Entertainment System +#[derive(Debug, Parser)] +#[clap(author, version, about)] +struct Args { + /// path to the base rom file + #[clap(short, long)] + rom: PathBuf, + /// path to the enemizerOptions.json + #[clap(short, long)] + enemizer: PathBuf, + /// seed number + #[clap(short, long)] + seed: Option, + /// path to the intended output file + #[clap(short, long)] + output: PathBuf, + /// operate in binary mode (takes already randomized SFC and applies enemizer directly to ROM) + #[clap(long)] + binary: bool, + /// path to base2patched.json (not used) + #[clap(long)] + base: Option, + /// path to the randomizerPatch.json (not used) + #[clap(long)] + randomizer: Option, +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + let stopwatch = Instant::now(); + + let options = { + let mut opts_file = File::open(args.enemizer)?; + let mut json = String::new(); + opts_file.read_to_string(&mut json)?; + serde_json::from_str(&json)? + }; + let symbols = load_symbols()?; + + let mut raw_data = vec![]; + let mut rom_file = File::open(args.rom)?; + rom_file.read_to_end(&mut raw_data)?; + + raw_data.resize(2 * 1024 * 1024, 0); + + let mut rom = RomData::new(symbols, raw_data); + ensure!(!rom.is_enemizer(), "It appears that the provided base ROM is already enemized. Please ensure you are using an original game ROM."); + + // Oh noes! The max seed number is twice as likely to show up as any other! + // (That is, 2 out of 2 billion instead of 1.) + let seed = args.seed.unwrap_or_else(|| rand::random()).saturating_abs(); + + rom.randomize(ENEMIZER_BASE_PATCH, options, seed)?; + + let mut out_file = File::create(&args.output)?; + + if args.binary { + out_file.write_all(rom.get_rom_bytes())?; + out_file.flush()?; + println!("Generated SFC file {}", args.output.to_string_lossy()); + } else { + let patches = rom.generate_patch(); + serde_json::to_writer(out_file, &patches)?; + } + + println!("Seed generated in: {}ms", stopwatch.elapsed().as_millis()); + + Ok(()) +} + +fn load_symbols() -> anyhow::Result { + asar::load_symbols(ASAR_SYMBOLS) } diff --git a/enemize/src/option_flags.rs b/enemize/src/option_flags.rs new file mode 100644 index 0000000..0f683e3 --- /dev/null +++ b/enemize/src/option_flags.rs @@ -0,0 +1,774 @@ +use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::bosses::BossType; +use crate::InvalidEnumError; + +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct ManualBosses { + pub eastern_palace: String, + pub desert_palace: String, + pub tower_of_hera: String, + pub agahnims_tower: String, + pub palace_of_darkness: String, + pub swamp_palace: String, + pub skull_woods: String, + pub thieves_town: String, + pub ice_palace: String, + pub misery_mire: String, + pub turtle_rock: String, + pub ganons_tower1: String, + pub ganons_tower2: String, + pub ganons_tower3: String, + pub ganons_tower4: String, + pub ganon: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum RandomizeEnemiesType { + Basic, + Normal, + Hard, + Chaos, + Insanity, +} + +impl From for u8 { + fn from(t: RandomizeEnemiesType) -> u8 { + use RandomizeEnemiesType::*; + + match t { + Basic => 0, + Normal => 1, + Hard => 2, + Chaos => 3, + Insanity => 4, + } + } +} + +impl TryFrom for RandomizeEnemiesType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Basic), + 1 => Ok(Self::Normal), + 2 => Ok(Self::Hard), + 3 => Ok(Self::Chaos), + 4 => Ok(Self::Insanity), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum RandomizeEnemyHpType { + Easy, + Medium, + Hard, + Patty, +} + +impl From for u8 { + fn from(t: RandomizeEnemyHpType) -> u8 { + use RandomizeEnemyHpType::*; + + match t { + Easy => 0, + Medium => 1, + Hard => 2, + Patty => 3, + } + } +} + +impl TryFrom for RandomizeEnemyHpType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Easy), + 1 => Ok(Self::Medium), + 2 => Ok(Self::Hard), + 3 => Ok(Self::Patty), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum RandomizeBossesType { + Basic, + Normal, + Chaos, +} + +impl From for u8 { + fn from(t: RandomizeBossesType) -> u8 { + use RandomizeBossesType::*; + + match t { + Basic => 0, + Normal => 1, + Chaos => 2, + } + } +} + +impl TryFrom for RandomizeBossesType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Basic), + 1 => Ok(Self::Normal), + 2 => Ok(Self::Chaos), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum SwordType { + Normal, +} + +impl From for u8 { + fn from(_t: SwordType) -> u8 { + 0 + } +} + +impl TryFrom for SwordType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Normal), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum ShieldType { + Normal, +} + +impl From for u8 { + fn from(_t: ShieldType) -> u8 { + 0 + } +} + +impl TryFrom for ShieldType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Normal), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum AbsorbableType { + Heart, + GreenRupee, + BlueRupee, + RedRupee, + #[serde(alias = "Bomb_1")] + Bomb1, + #[serde(alias = "Bomb_4")] + Bomb4, + #[serde(alias = "Bomb_8")] + Bomb8, + SmallMagic, + FullMagic, + #[serde(alias = "Arrow_5")] + Arrow5, + #[serde(alias = "Arrow_10")] + Arrow10, + Fairy, + Key, + BigKey, +} + +impl From for u8 { + fn from(t: AbsorbableType) -> u8 { + use AbsorbableType::*; + + match t { + Heart => 0, + GreenRupee => 1, + BlueRupee => 2, + RedRupee => 3, + Bomb1 => 4, + Bomb4 => 5, + Bomb8 => 6, + SmallMagic => 7, + FullMagic => 8, + Arrow5 => 9, + Arrow10 => 10, + Fairy => 11, + Key => 12, + BigKey => 13, + } + } +} + +impl TryFrom for AbsorbableType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Heart), + 1 => Ok(Self::GreenRupee), + 2 => Ok(Self::BlueRupee), + 3 => Ok(Self::RedRupee), + 4 => Ok(Self::Bomb1), + 5 => Ok(Self::Bomb4), + 6 => Ok(Self::Bomb8), + 7 => Ok(Self::SmallMagic), + 8 => Ok(Self::FullMagic), + 9 => Ok(Self::Arrow5), + 10 => Ok(Self::Arrow10), + 11 => Ok(Self::Fairy), + 12 => Ok(Self::Key), + 13 => Ok(Self::BigKey), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +impl fmt::Display for AbsorbableType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use AbsorbableType::*; + + let description = match self { + Heart => "Heart", + GreenRupee => "Green Rupee", + BlueRupee => "Blue Rupee", + RedRupee => "Red Rupee", + Bomb1 => "Bomb (1)", + Bomb4 => "Bomb (4)", + Bomb8 => "Bomb (8)", + SmallMagic => "Small Magic", + FullMagic => "Full Magic", + Arrow5 => "Arrow (5)", + Arrow10 => "Arrow (10)", + Fairy => "Fairy", + Key => "Key", + BigKey => "Big Key", + }; + + write!(f, "{}", description) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum HeartBeepSpeed { + Normal, + Half, + Quarter, + Off, +} + +impl Default for HeartBeepSpeed { + fn default() -> Self { + HeartBeepSpeed::Normal + } +} + +impl From for u8 { + fn from(speed: HeartBeepSpeed) -> u8 { + use HeartBeepSpeed::*; + + match speed { + Normal => 0, + Half => 1, + Quarter => 2, + Off => 3, + } + } +} + +impl TryFrom for HeartBeepSpeed { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Normal), + 1 => Ok(Self::Half), + 2 => Ok(Self::Quarter), + 3 => Ok(Self::Off), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum BeeLevel { + Level1, + Level2, + Level3, + Level4, +} + +impl fmt::Display for BeeLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use BeeLevel::*; + + let description = match self { + Level1 => "Bees??", + Level2 => "Bees!", + Level3 => "Beeeeees!?", + Level4 => "Beeeeeeeeeeeeeeeeeeeees", + }; + + write!(f, "{}", description) + } +} + +impl From for u8 { + fn from(level: BeeLevel) -> u8 { + use BeeLevel::*; + + match level { + Level1 => 0, + Level2 => 1, + Level3 => 2, + Level4 => 3, + } + } +} + +impl TryFrom for BeeLevel { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Level1), + 1 => Ok(Self::Level2), + 2 => Ok(Self::Level3), + 3 => Ok(Self::Level4), + _ => Err(InvalidEnumError(PhantomData)), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(default, rename_all = "PascalCase")] +pub struct OptionFlags { + pub randomize_enemies: bool, + pub randomize_enemies_type: RandomizeEnemiesType, + pub randomize_bush_enemy_chance: bool, + + pub randomize_enemy_health_range: bool, + pub randomize_enemy_health_type: RandomizeEnemyHpType, + + pub randomize_enemy_damage: bool, + pub allow_enemy_zero_damage: bool, + pub shuffle_enemy_damage_groups: bool, + pub enemy_damage_chaos_mode: bool, + + //pub easy_mode_escape: bool, + pub enemies_absorbable: bool, + pub absorbable_spawn_rate: u8, + pub absorbable_types: HashMap, + + //pub boss_madness: bool, + pub randomize_bosses: bool, + pub randomize_bosses_type: RandomizeBossesType, + + //pub randomize_boss_health: bool, + //pub randomize_boss_health_min_amount: u8, + //pub randomize_boss_health_max_amount: u8, + + //pub randomize_boss_damage: bool, + //pub randomize_boss_damage_min_amount: u8, + //pub randomize_boss_damage_max_amount: u8, + + //pub randomize_boss_behavior: bool, + pub randomize_dungeon_palettes: bool, + pub set_blackout_mode: bool, + + pub randomize_overworld_palettes: bool, + + pub randomize_sprite_palettes: bool, + pub set_advanced_sprite_palettes: bool, + pub puke_mode: bool, + pub negative_mode: bool, + pub grayscale_mode: bool, + + pub generate_spoilers: bool, + pub randomize_link_sprite_palette: bool, + pub randomize_pots: bool, + pub shuffle_music: bool, + pub bootleg_magic: bool, + pub debug_mode: bool, + //pub custom_bosses: bool, + pub heart_beep_speed: HeartBeepSpeed, + pub alternate_gfx: bool, + pub shield_graphics: PathBuf, + pub sword_graphics: PathBuf, + pub bee_mizer: bool, + pub bees_level: BeeLevel, + pub debug_force_enemy: bool, + pub debug_force_enemy_id: u8, + pub debug_force_boss: bool, + pub debug_force_boss_id: BossType, + pub debug_open_shutter_doors: bool, + pub debug_force_enemy_damage_zero: bool, + pub debug_show_room_id_in_rupee_counter: bool, + pub o_h_k_o: bool, + pub randomize_tile_trap_pattern: bool, + pub randomize_tile_trap_floor_tile: bool, + pub allow_killable_thief: bool, + pub randomize_sprite_on_hit: bool, + pub hero_mode: bool, + pub increase_brightness: bool, + pub mute_music_enable_msu_1: bool, + pub agahnim_bounce_balls: bool, + + pub use_manual_bosses: bool, + pub manual_bosses: ManualBosses, +} + +impl OptionFlags { + pub fn try_from_bytes(bytes: &[u8]) -> anyhow::Result { + let mut absorbable_types = HashMap::new(); + absorbable_types.insert(AbsorbableType::Heart, bytes[10] != 0); + absorbable_types.insert(AbsorbableType::GreenRupee, bytes[11] != 0); + absorbable_types.insert(AbsorbableType::BlueRupee, bytes[12] != 0); + absorbable_types.insert(AbsorbableType::RedRupee, bytes[13] != 0); + absorbable_types.insert(AbsorbableType::Bomb1, bytes[14] != 0); + absorbable_types.insert(AbsorbableType::Bomb4, bytes[15] != 0); + absorbable_types.insert(AbsorbableType::Bomb8, bytes[16] != 0); + absorbable_types.insert(AbsorbableType::SmallMagic, bytes[17] != 0); + absorbable_types.insert(AbsorbableType::FullMagic, bytes[18] != 0); + absorbable_types.insert(AbsorbableType::Arrow5, bytes[19] != 0); + absorbable_types.insert(AbsorbableType::Arrow10, bytes[20] != 0); + absorbable_types.insert(AbsorbableType::Fairy, bytes[21] != 0); + absorbable_types.insert(AbsorbableType::Key, bytes[21] != 1); + absorbable_types.insert(AbsorbableType::BigKey, bytes[23] != 0); + + Ok(OptionFlags { + randomize_enemies: bytes[0] != 0, + randomize_enemies_type: bytes[1].try_into()?, + randomize_bush_enemy_chance: bytes[2] != 0, + randomize_enemy_health_range: bytes[3] != 0, + randomize_enemy_health_type: bytes[4].try_into()?, + randomize_enemy_damage: bytes[5] != 0, + allow_enemy_zero_damage: bytes[6] != 0, + //easy_mode_escape: bytes[7] != 0, + enemies_absorbable: bytes[8] != 0, + absorbable_spawn_rate: bytes[9], + absorbable_types, + //boss_madness: bytes[24] != 0, + randomize_bosses: bytes[25] != 0, + randomize_bosses_type: bytes[26].try_into()?, + //randomize_boss_health: bytes[27] != 0, + //randomize_boss_health_min_amount: bytes[28], + //randomize_boss_health_max_amount: bytes[29], + //randomize_boss_damage: bytes[30] != 0, + //randomize_boss_damage_min_amount: bytes[31], + //randomize_boss_damage_max_amount: bytes[32], + //randomize_boss_behavior: bytes[33] != 0, + randomize_dungeon_palettes: bytes[34] != 0, + set_blackout_mode: bytes[35] != 0, + randomize_overworld_palettes: bytes[36] != 0, + randomize_sprite_palettes: bytes[37] != 0, + set_advanced_sprite_palettes: bytes[38] != 0, + puke_mode: bytes[39] != 0, + negative_mode: bytes[40] != 0, + grayscale_mode: bytes[41] != 0, + generate_spoilers: bytes[42] != 0, + randomize_link_sprite_palette: bytes[43] != 0, + randomize_pots: bytes[44] != 0, + shuffle_music: bytes[45] != 0, + bootleg_magic: bytes[46] != 0, + debug_mode: bytes[47] != 0, + //custom_bosses: bytes[48] != 0, + heart_beep_speed: bytes[49].try_into()?, + alternate_gfx: bytes[50] != 0, + // Skip byte 51 (shield_graphics) + shuffle_enemy_damage_groups: bytes[52] != 0, + enemy_damage_chaos_mode: bytes[53] != 0, + // Skip byte 54 (sword_graphics) + bee_mizer: bytes[55] != 0, + bees_level: bytes[56].try_into()?, + debug_force_enemy: bytes[57] != 0, + debug_force_enemy_id: bytes[58], + debug_force_boss: bytes[59] != 0, + debug_force_boss_id: bytes[60].try_into()?, + debug_open_shutter_doors: bytes[61] != 0, + debug_force_enemy_damage_zero: bytes[62] != 0, + debug_show_room_id_in_rupee_counter: bytes[63] != 0, + o_h_k_o: bytes[64] != 0, + randomize_tile_trap_pattern: bytes[65] != 0, + randomize_tile_trap_floor_tile: bytes[66] != 0, + allow_killable_thief: bytes[67] != 0, + randomize_sprite_on_hit: bytes[68] != 0, + hero_mode: bytes[69] != 0, + increase_brightness: bytes[70] != 0, + mute_music_enable_msu_1: bytes[71] != 0, + agahnim_bounce_balls: bytes[72] != 0, + ..Default::default() + }) + } + + pub fn into_bytes(self) -> Vec { + let mut bytes = Vec::with_capacity(crate::rom::ENEMIZER_INFO_FLAGS_LENGTH); + + bytes.push(self.randomize_enemies as u8); + bytes.push(self.randomize_enemies_type as u8); + bytes.push(self.randomize_bush_enemy_chance as u8); + bytes.push(self.randomize_enemy_health_range as u8); + bytes.push(self.randomize_enemy_health_type as u8); + bytes.push(self.randomize_enemy_damage as u8); + bytes.push(self.allow_enemy_zero_damage as u8); + bytes.push(0); //bytes.push(self.easy_mode_escape as u8); + bytes.push(self.enemies_absorbable as u8); + bytes.push(self.absorbable_spawn_rate); + + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Heart) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::GreenRupee) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::BlueRupee) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::RedRupee) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Bomb1) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Bomb4) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Bomb8) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::SmallMagic) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::FullMagic) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Arrow5) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Arrow10) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Fairy) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::Key) + .copied() + .unwrap_or(false) as u8, + ); + bytes.push( + self.absorbable_types + .get(&AbsorbableType::BigKey) + .copied() + .unwrap_or(false) as u8, + ); + + bytes.push(0); //bytes.push(self.boss_madness as u8); + bytes.push(self.randomize_bosses as u8); + bytes.push(self.randomize_bosses_type as u8); + bytes.push(0); //bytes.push(self.randomize_boss_health as u8); + bytes.push(0); //bytes.push(self.randomize_boss_health_min_amount); + bytes.push(0); //bytes.push(self.randomize_boss_health_max_amount); + bytes.push(0); //bytes.push(self.randomize_boss_damage as u8); + bytes.push(0); //bytes.push(self.randomize_boss_damage_min_amount); + bytes.push(0); //bytes.push(self.randomize_boss_damage_max_amount); + bytes.push(0); //bytes.push(self.randomize_boss_behavior as u8); + bytes.push(self.randomize_dungeon_palettes as u8); + bytes.push(self.set_blackout_mode as u8); + bytes.push(self.randomize_overworld_palettes as u8); + bytes.push(self.randomize_sprite_palettes as u8); + bytes.push(self.set_advanced_sprite_palettes as u8); + bytes.push(self.puke_mode as u8); + bytes.push(self.negative_mode as u8); + bytes.push(self.grayscale_mode as u8); + bytes.push(self.generate_spoilers as u8); + bytes.push(self.randomize_link_sprite_palette as u8); + bytes.push(self.randomize_pots as u8); + bytes.push(self.shuffle_music as u8); + bytes.push(self.bootleg_magic as u8); + bytes.push(self.debug_mode as u8); + bytes.push(0); //bytes.push(self.custom_bosses as u8); + bytes.push(self.heart_beep_speed as u8); + bytes.push(self.alternate_gfx as u8); + bytes.push(0); // self.shield_graphics + bytes.push(self.shuffle_enemy_damage_groups as u8); + bytes.push(self.enemy_damage_chaos_mode as u8); + bytes.push(0); // self.sword_graphics + bytes.push(self.bee_mizer as u8); + bytes.push(self.bees_level as u8); + + bytes.push(self.debug_force_enemy as u8); + bytes.push(self.debug_force_enemy_id as u8); + bytes.push(self.debug_force_boss as u8); + bytes.push(self.debug_force_boss_id as u8); + bytes.push(self.debug_open_shutter_doors as u8); + bytes.push(self.debug_force_enemy_damage_zero as u8); + bytes.push(self.debug_show_room_id_in_rupee_counter as u8); + bytes.push(self.o_h_k_o as u8); + bytes.push(self.randomize_tile_trap_pattern as u8); + bytes.push(self.randomize_tile_trap_floor_tile as u8); + bytes.push(self.allow_killable_thief as u8); + bytes.push(self.randomize_sprite_on_hit as u8); + bytes.push(self.hero_mode as u8); + bytes.push(self.increase_brightness as u8); + bytes.push(self.mute_music_enable_msu_1 as u8); + bytes.push(self.agahnim_bounce_balls as u8); + + bytes + } +} + +impl Default for OptionFlags { + fn default() -> Self { + Self { + randomize_enemies: true, + randomize_enemies_type: RandomizeEnemiesType::Chaos, + randomize_bush_enemy_chance: true, + randomize_enemy_health_range: false, + randomize_enemy_health_type: RandomizeEnemyHpType::Medium, + randomize_enemy_damage: false, + allow_enemy_zero_damage: false, + shuffle_enemy_damage_groups: false, + enemy_damage_chaos_mode: false, + //easy_mode_escape: false, + enemies_absorbable: false, + absorbable_spawn_rate: 0, + absorbable_types: HashMap::new(), + //boss_madness: false, + randomize_bosses: true, + randomize_bosses_type: RandomizeBossesType::Chaos, + //randomize_boss_health: false, + //randomize_boss_health_min_amount: 0, + //randomize_boss_health_max_amount: 0, + //randomize_boss_damage: false, + //randomize_boss_damage_min_amount: 0, + //randomize_boss_damage_max_amount: 0, + //randomize_boss_behavior: false, + randomize_dungeon_palettes: true, + set_blackout_mode: false, + randomize_overworld_palettes: true, + randomize_sprite_palettes: true, + set_advanced_sprite_palettes: false, + puke_mode: false, + negative_mode: false, + grayscale_mode: false, + generate_spoilers: true, + randomize_link_sprite_palette: false, + randomize_pots: true, + shuffle_music: false, + bootleg_magic: false, + debug_mode: false, + //custom_bosses: false, + heart_beep_speed: HeartBeepSpeed::Half, + alternate_gfx: false, + shield_graphics: ["shield_gfx", "normal.gfx"].iter().collect(), + sword_graphics: ["sword_gfx", "normal.gfx"].iter().collect(), + bee_mizer: false, + bees_level: BeeLevel::Level1, + debug_force_enemy: false, + debug_force_enemy_id: 0, + debug_force_boss: false, + debug_force_boss_id: BossType::Trinexx, + debug_open_shutter_doors: false, + debug_force_enemy_damage_zero: false, + debug_show_room_id_in_rupee_counter: false, + o_h_k_o: false, + randomize_tile_trap_pattern: false, + randomize_tile_trap_floor_tile: false, + allow_killable_thief: false, + randomize_sprite_on_hit: false, + hero_mode: false, + increase_brightness: false, + mute_music_enable_msu_1: false, + agahnim_bounce_balls: false, + use_manual_bosses: false, + manual_bosses: Default::default(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_option_flags_serde() { + let empty = "{}"; + let actual: OptionFlags = serde_json::from_str(empty).expect("Can't deserialize empty"); + let expected = + serde_json::to_string(&OptionFlags::default()).expect("Can't serialize default"); + + assert_eq!( + serde_json::to_string(&actual).expect("Can't roundtrip"), + expected + ); + } +} diff --git a/enemize/src/randomize.rs b/enemize/src/randomize.rs new file mode 100644 index 0000000..a42438e --- /dev/null +++ b/enemize/src/randomize.rs @@ -0,0 +1,38 @@ +use anyhow::ensure; + +use crate::option_flags::OptionFlags; +use crate::rom::RomData; +use crate::{Patch, PatchSet}; + +impl RomData { + pub fn randomize( + &mut self, + base_patch: &str, + option_flags: OptionFlags, + seed: i32, + ) -> anyhow::Result<()> { + ensure!( + self.is_randomizer(), + "Enemizer only supports randomizer ROMs for input." + ); + ensure!(!self.is_race(), "Enemizer does not support race roms."); + + if self.is_enemizer() { + // Reuse seed in ROM, not the one given. + self.reset_enemizer(); + } else { + self.set_enemizer_seed(seed); + } + + self.expand_rom(); + self.set_info_flags(option_flags)?; + + let patches: Vec = serde_json::from_str(base_patch)?; + + let mut patch_set = PatchSet::default(); + patch_set.add_patches(patches); + patch_set.patch_rom(self); + + Ok(()) + } +} diff --git a/enemize/src/rom.rs b/enemize/src/rom.rs index 11a9874..b4e8700 100644 --- a/enemize/src/rom.rs +++ b/enemize/src/rom.rs @@ -1,7 +1,307 @@ -const ENEMIZER_INFO_SEED_OFFSET: usize = 0; -const ENEMIZER_INFO_SEED_LENGTH: usize = 12; -const ENEMIZER_INFO_VERSION_OFFSET: usize = ENEMIZER_INFO_SEED_OFFSET + ENEMIZER_INFO_SEED_LENGTH; -const ENEMIZER_INFO_VERSION_LENGTH: usize = 8; -const ENEMIZER_INFO_FLAGS_OFFSET: usize = ENEMIZER_INFO_VERSION_OFFSET + ENEMIZER_INFO_VERSION_LENGTH; -const ENEMIZER_INFO_FLAGS_LENGTH: usize = 0x50; +use std::collections::BTreeMap; +use std::iter; +use std::ops::{Index, IndexMut, Range}; +use anyhow::bail; + +use crate::asar::{pc_to_snes_address, snes_to_pc_address, Symbols}; +use crate::constants::*; +use crate::option_flags::OptionFlags; +use crate::Patch; + +pub const ENEMIZER_INFO_SEED_OFFSET: usize = 0; +pub const ENEMIZER_INFO_SEED_LENGTH: usize = 12; +pub const ENEMIZER_INFO_VERSION_OFFSET: usize = + ENEMIZER_INFO_SEED_OFFSET + ENEMIZER_INFO_SEED_LENGTH; +pub const ENEMIZER_INFO_VERSION_LENGTH: usize = 8; +pub const ENEMIZER_INFO_FLAGS_OFFSET: usize = + ENEMIZER_INFO_VERSION_OFFSET + ENEMIZER_INFO_VERSION_LENGTH; +pub const ENEMIZER_INFO_FLAGS_LENGTH: usize = 0x50; + +pub const RANDOMIZER_HIDDEN_ENEMIES_FLAG: usize = 0x00; +pub const CLOSE_BLIND_DOOR_FLAG: usize = 0x01; +pub const MOLDORM_EYES_FLAG: usize = 0x02; +pub const RANDOM_SPRITE_FLAG: usize = 0x03; +pub const AGAHNIM_BOUNCE_BALLS_FLAG: usize = 0x04; +pub const ENABLE_MIMIC_OVERRIDE_FLAG: usize = 0x05; +pub const ENABLE_TERRORPIN_AI_FIX_FLAG: usize = 0x06; +pub const CHECKSUM_COMPLIMENT_ADDRESS: usize = 0x7fdc; +pub const CHECKSUM_ADDRESS: usize = 0x7fde; +pub const RANDOMIZER_MODE_FLAG: usize = 0x180032; + +pub struct RomData { + asar_symbols: Symbols, + spoiler: String, + pub(crate) rom_data: Vec, + patch_data: BTreeMap, + pub(crate) seed: i32, +} + +impl RomData { + pub fn spoiler(&self) -> &str { + self.spoiler.as_str() + } + + pub fn reset_enemizer(&mut self) -> i32 { + use crate::constants::*; + + let seed = self.get_enemizer_seed(); + + self.rom_data[ROOM_HEADER_BANK_LOCATION] = 0x04; + + let dungeon_header_range = + DUNGEON_HEADER_POINTER_TABLE..(DUNGEON_HEADER_POINTER_TABLE + 640); + self.rom_data[dungeon_header_range].copy_from_slice(&ORIGINAL_ROOM_POINTERS); + + let room_range = 0x5b97..(0x5b97 + 576); + self.rom_data[room_range].copy_from_slice(&ORIGINAL_ROOM_BLOCKS); + + let ow_gfx_range = OVERWORLD_AREA_GRAPHICS_BLOCK..(OVERWORLD_AREA_GRAPHICS_BLOCK + 272); + self.rom_data[ow_gfx_range].copy_from_slice(&ORIGINAL_OVERWORLD_BLOCKS); + + seed + } + + pub fn generate_patch(&self) -> Vec { + let mut patches = vec![]; + + for (&addr, &byte) in self.patch_data.iter() { + match patches + .last_mut() + .filter(|p: &&mut Patch| p.address + 1 == addr) + { + None => patches.push(Patch { + address: addr, + patch_data: vec![byte], + }), + Some(patch) => patch.patch_data.push(byte), + } + } + + patches + } + + pub fn get_rom_bytes(&self) -> &[u8] { + &self.rom_data + } + + fn set_rom_bytes(&mut self, bytes: &[u8], range: Range) { + self.rom_data.splice(range, bytes.into_iter().map(|&b| b)); + } + + fn set_patch_bytes(&mut self, range: Range) { + let slice = &self.rom_data[range.clone()]; + self.patch_data + .extend(iter::zip(range, slice.into_iter().map(|&b| b))); + } + + pub fn is_enemizer(&self) -> bool { + self.rom_data.len() == ENEMIZER_FILE_LENGTH + && self.rom_data[self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET] + == b'E' + && self.rom_data + [self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET + 1] + == b'N' + } + + pub fn is_randomizer(&self) -> bool { + let acceptable = [ + b"VT", // item rando + b"ER", // entrance rando + b"DR", // door rando + b"BM", // Berserker's multiworld + b"BD", // Berserker's multiworld doors + b"AP", // Archipelago + b"AD", // Archipelago with door rando + ]; + + acceptable + .iter() + .any(|abbr| &abbr[..] == &self.rom_data[0x7fc0..0x7fc2]) + || (self.rom_data.len() >= 0x20_0000 + && &self.rom_data[0x7fc0..0x7fcf] == b"ZELDANODENSETSU") + } + + pub fn is_race(&self) -> bool { + self.is_randomizer() + && (&self.rom_data[0x180213..0x180214] == &[1, 0] + || &self.rom_data[0x7fc0..0x7fca] == b"VT TOURNEY") + } + + fn assert_rom_length(&self) { + assert!( + self.rom_data.len() >= ENEMIZER_FILE_LENGTH, + "You need to expand the rom before you can use Enemizer features." + ); + } + + pub fn get_enemizer_seed(&self) -> i32 { + self.seed + } + + pub fn derive_enemizer_seed(&mut self) -> anyhow::Result { + if self.seed < 0 && self.is_enemizer() { + let seed_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET; + let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH; + let seed_bytes = &self.rom_data[seed_start..seed_end]; + let seed_str = &std::str::from_utf8(seed_bytes)?.trim_end_matches('\0')[2..]; + + self.seed = i32::from_str_radix(seed_str, 10)?; + } + + Ok(self.seed) + } + + pub fn set_enemizer_seed(&mut self, seed: i32) { + self.assert_rom_length(); + + let seed_str = format!("EN{:<10}", seed); + let bytes = seed_str.as_bytes().to_owned(); + + let seed_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET; + let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH; + self.seed = seed; + + self.set_rom_bytes(&bytes, seed_start..seed_end); + self.set_patch_bytes(seed_start..seed_end); + } + + pub fn expand_rom(&mut self) { + self.rom_data.resize(0x40_0000, 0); + // Update header length. + self.rom_data[0x7fd7] = 0x0c; + self.set_patch_bytes(0x7fd7..0x7fd8); + + self.set_enemizer_version("6.0.32".to_owned()); + } + + pub fn get_enemizer_version(&self) -> anyhow::Result<&str> { + if self.is_enemizer() { + let version_start = + self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_VERSION_OFFSET; + let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH; + Ok(std::str::from_utf8( + &self.rom_data[version_start..version_end], + )?) + } else { + bail!("Not Enemizer Rom") + } + } + + pub fn set_enemizer_version(&mut self, version: String) { + self.assert_rom_length(); + + let mut bytes = version.into_bytes(); + bytes.resize(ENEMIZER_INFO_VERSION_LENGTH, 0); + + let version_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_VERSION_OFFSET; + let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH; + self.set_rom_bytes(&bytes, version_start..version_end); + self.set_patch_bytes(version_start..version_end) + } + + pub fn set_info_flags(&mut self, flags: OptionFlags) -> anyhow::Result<()> { + let bytes = flags.into_bytes(); + if bytes.len() > 0x100 - ENEMIZER_INFO_FLAGS_OFFSET { + bail!("Option flags is too long to fit in the space allocated. Need to move data/code in asm file."); + } + + let flags_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_FLAGS_OFFSET; + let flags_end = flags_start + bytes.len(); + self.set_rom_bytes(&bytes, flags_start..flags_end); + self.set_patch_bytes(flags_start..flags_end); + + Ok(()) + } + + pub fn get_info_flags(&self) -> Option { + if self.is_enemizer() { + let flags_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_FLAGS_OFFSET; + let flags_end = flags_start + ENEMIZER_INFO_FLAGS_LENGTH; + OptionFlags::try_from_bytes(&self.rom_data[flags_start..flags_end]).ok() + } else { + None + } + } + + pub fn new(asar_symbols: Symbols, rom_data: Vec) -> Self { + Self { + asar_symbols, + spoiler: String::new(), + rom_data: rom_data, + patch_data: BTreeMap::new(), + seed: -1, + } + } + + fn get_flag(&self, offset: usize) -> bool { + let flag_idx = self.asar_symbols["EnemizerFlags"] + offset; + self.rom_data[flag_idx] == 1 + } + + fn set_flag(&mut self, offset: usize, val: bool) { + let flag_idx = self.asar_symbols["EnemizerFlags"] + offset; + self.rom_data[flag_idx] = if val { 1 } else { 0 }; + self.set_patch_bytes(flag_idx..(flag_idx + 1)); + } + + pub fn patch_data(&mut self, patch: &Patch) { + self.set_rom_bytes( + &patch.patch_data, + patch.address..(patch.address + patch.patch_data.len()), + ); + } + + pub fn move_room_headers(&mut self) { + let table_base = DUNGEON_HEADER_POINTER_TABLE; + let header_base = self.asar_symbols["room_header_table"]; + + // Change room header bank (at 0xb5e7) to 0x04. + let new_room_bank = + self.rom_data[self.asar_symbols["moved_room_header_bank_value_address"]]; + self.rom_data[ROOM_HEADER_BANK_LOCATION] = new_room_bank; + + // Copy header table. + for i in 0..320 { + // Get i'th room's pointer. + // Pointers are 16bits, with a hard-coded bank. + let room_pointer = [ + self.rom_data[table_base + (i * 2)], + self.rom_data[table_base + (i * 2) + 1], + 4, + 0, + ]; + let snes_address = u32::from_le_bytes(room_pointer); + let pc_address = snes_to_pc_address(snes_address); + + // Copy i'th room's headers to new room_header_table. + let header_start = header_base + (i * 14); + self.rom_data + .copy_within(pc_address..(pc_address + 14), header_start); + } + + // Repoint the pointer table to the new header table. + for i in 0..320 { + let snes = pc_to_snes_address(header_base + (i * 14)).to_le_bytes(); + + assert_eq!(snes[2], new_room_bank, "We changed banks in the middle of moving the room headers! This should have been caught by dev team, unless you were playing with files you shouldn't touch."); + + self.rom_data[(table_base + (i * 2))..(table_base + (i * 2) + 1)] + .copy_from_slice(&snes[0..1]); + } + } +} + +impl Index for RomData { + type Output = u8; + + fn index(&self, index: usize) -> &u8 { + &self.rom_data[index] + } +} + +impl IndexMut for RomData { + fn index_mut(&mut self, index: usize) -> &mut u8 { + &mut self.rom_data[index] + } +} diff --git a/prepare.sh b/prepare.sh new file mode 100644 index 0000000..d532d49 --- /dev/null +++ b/prepare.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [[ ! -s base_patch.json || ! -s asar_symbols.txt ]]; then +cargo run -p bin_comp -- assembly/src/main.asm base_patch.json asar_symbols.txt +fi +if [[ ! -s enemizer_base_patch.json ]]; then +cargo run -p base_patch_generator -- "$1" base_patch.json asar_symbols.txt enemizer_base_patch.json +fi +cp enemizer_base_patch.json target/debug/enemizerBasePatch.json