diff --git a/Cargo.lock b/Cargo.lock index d1c609f..77fdf28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "anyhow", "serde", "serde_json", + "thiserror", ] [[package]] @@ -173,6 +174,26 @@ dependencies = [ "winapi", ] +[[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" diff --git a/enemize/Cargo.toml b/enemize/Cargo.toml index a2cf87f..a612509 100644 --- a/enemize/Cargo.toml +++ b/enemize/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" anyhow = "1" 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..6324302 --- /dev/null +++ b/enemize/src/asar.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; + +pub type Symbols = HashMap; + +impl Symbols { + pub fn load(filename: Path) -> anyhow::Result { + let file = File::open(filename)?; + let mut reader = BufReader::new(file); + + reader.lines().filter_map(|l| l.ok().and_then(|line| { + let words: Vec = line.split_ascii_whitespace().collect(); + if words.len() == 2 { + Some((words[1], words[0])) + } else { + None + } + })) + .filter_map(|(symbol, mut address)| { + if let Some(colon_at) = address.find(':') { + address.remove(colon_at); + } + + let snes_address = u32::from_str_radix(&address, 16).ok()?; + + let pc_address = (snes_address & 0x7FFF) + ((addr / 2) & 0xFF8000); + + Some((symbol, pc_address)) + }).collect() + } +} diff --git a/enemize/src/bosses/mod.rs b/enemize/src/bosses/mod.rs new file mode 100644 index 0000000..872412d --- /dev/null +++ b/enemize/src/bosses/mod.rs @@ -0,0 +1,48 @@ +use std::marker::PhantomData; + +use crate::InvalidEnumError; + +#[derive(Debug)] +#[repr(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 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..716dfde --- /dev/null +++ b/enemize/src/constants.rs @@ -0,0 +1,12 @@ +pub const ROM_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; diff --git a/enemize/src/lib.rs b/enemize/src/lib.rs index eda29ef..201bba8 100644 --- a/enemize/src/lib.rs +++ b/enemize/src/lib.rs @@ -1,10 +1,21 @@ use std::fs::File; +use std::marker::PhantomData; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; +use thiserror::Error; +use crate::rom::RomData; + +pub mod bosses; +pub mod constants; +pub mod option_flags; pub mod rom; +#[derive(Debug, Error)] +#[error("Not a valid value for {0:?}")] +pub struct InvalidEnumError(PhantomData); + #[derive(Deserialize, Serialize)] pub struct Patch { pub address: usize, diff --git a/enemize/src/option_flags.rs b/enemize/src/option_flags.rs new file mode 100644 index 0000000..2170c52 --- /dev/null +++ b/enemize/src/option_flags.rs @@ -0,0 +1,571 @@ +use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; +use std::path::PathBuf; + +use crate::bosses::BossType; +use crate::InvalidEnumError; + +#[derive(Debug, Default)] +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(Debug)] +pub enum RandomizeEnemiesType { + Basic, + Normal, + Hard, + Chaos, + Insanity, +} + +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(Debug)] +pub enum RandomizeEnemyHpType { + Easy, + Medium, + Hard, + Patty +} + +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(Debug)] +pub enum RandomizeBossesType { + Basic, + Normal, + Chaos +} + +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(Debug)] +pub enum SwordType { + Normal +} + +impl TryFrom for SwordType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Normal), + _ => Err(InvalidEnumError(PhantomData)) + } + } +} + +#[derive(Debug)] +pub enum ShieldType { + Normal +} + +impl TryFrom for ShieldType { + type Error = InvalidEnumError; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Normal), + _ => Err(InvalidEnumError(PhantomData)) + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum AbsorbableType { + Heart, + GreenRupee, + BlueRupee, + RedRupee, + Bomb1, + Bomb4, + Bomb8, + SmallMagic, + FullMagic, + Arrow5, + Arrow10, + Fairy, + Key, + BigKey, +} + +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::Bomb1), + 4 => Ok(Self::Bomb4), + 5 => Ok(Self::Bomb8), + 6 => Ok(Self::SmallMagic), + 7 => Ok(Self::FullMagic), + 8 => Ok(Self::Arrow5), + 9 => Ok(Self::Arrow10), + 10 => Ok(Self::Fairy), + 11 => Ok(Self::Key), + 12 => 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(Debug)] +pub enum HeartBeepSpeed { + Normal, + Half, + Quarter, + Off, +} + +impl Default for HeartBeepSpeed { + fn default() -> Self { + HeartBeepSpeed::Normal + } +} + +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(Debug)] +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 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)] +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::Bomb1, bytes[13] != 0); + absorbable_types.insert(AbsorbableType::Bomb4, bytes[14] != 0); + absorbable_types.insert(AbsorbableType::Bomb8, bytes[15] != 0); + absorbable_types.insert(AbsorbableType::SmallMagic, bytes[16] != 0); + absorbable_types.insert(AbsorbableType::FullMagic, bytes[17] != 0); + absorbable_types.insert(AbsorbableType::Arrow5, bytes[18] != 0); + absorbable_types.insert(AbsorbableType::Arrow10, bytes[19] != 0); + absorbable_types.insert(AbsorbableType::Fairy, bytes[20] != 0); + absorbable_types.insert(AbsorbableType::Key, bytes[21] != 0); + absorbable_types.insert(AbsorbableType::BigKey, bytes[22] != 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[23] != 0, + randomize_bosses: bytes[24] != 0, + randomize_bosses_type: bytes[25].try_into()?, + randomize_boss_health: bytes[26] != 0, + randomize_boss_health_min_amount: bytes[27], + randomize_boss_health_max_amount: bytes[28], + randomize_boss_damage: bytes[29] != 0, + randomize_boss_damage_min_amount: bytes[30], + randomize_boss_damage_max_amount: bytes[31], + randomize_boss_behavior: bytes[32] != 0, + randomize_dungeon_palettes: bytes[33] != 0, + set_blackout_mode: bytes[34] != 0, + randomize_overworld_palettes: bytes[35] != 0, + randomize_sprite_palettes: bytes[36] != 0, + set_advanced_sprite_palettes: bytes[37] != 0, + puke_mode: bytes[38] != 0, + negative_mode: bytes[39] != 0, + grayscale_mode: bytes[40] != 0, + generate_spoilers: bytes[41] != 0, + randomize_link_sprite_palette: bytes[42] != 0, + randomize_pots: bytes[43] != 0, + shuffle_music: bytes[44] != 0, + bootleg_magic: bytes[45] != 0, + debug_mode: bytes[46] != 0, + custom_bosses: bytes[47] != 0, + heart_beep_speed: bytes[48].try_into()?, + alternate_gfx: bytes[49] != 0, + // Skip byte 50 (shield_graphics) + shuffle_enemy_damage_groups: bytes[51] != 0, + enemy_damage_chaos_mode: bytes[52] != 0, + // Skip byte 53 (sword_graphics) + bee_mizer: bytes[54] != 0, + bees_level: bytes[55].try_into()?, + debug_force_enemy: bytes[56] != 0, + debug_force_enemy_id: bytes[57], + debug_force_boss: bytes[58] != 0, + debug_force_boss_id: bytes[59].try_into()?, + debug_open_shutter_doors: bytes[60] != 0, + debug_force_enemy_damage_zero: bytes[61] != 0, + debug_show_room_id_in_rupee_counter: bytes[62] != 0, + o_h_k_o: bytes[63] != 0, + randomize_tile_trap_pattern: bytes[64] != 0, + randomize_tile_trap_floor_tile: bytes[65] != 0, + allow_killable_thief: bytes[66] != 0, + randomize_sprite_on_hit: bytes[67] != 0, + hero_mode: bytes[68] != 0, + increase_brightness: bytes[69] != 0, + mute_music_enable_msu_1: bytes[70] != 0, + agahnim_bounce_balls: bytes[71] != 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(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(self.boss_madness as u8); + bytes.push(self.randomize_bosses as u8); + bytes.push(self.randomize_bosses_type as u8); + bytes.push(self.randomize_boss_health as u8); + bytes.push(self.randomize_boss_health_min_amount); + bytes.push(self.randomize_boss_health_max_amount); + bytes.push(self.randomize_boss_damage as u8); + bytes.push(self.randomize_boss_damage_min_amount); + bytes.push(self.randomize_boss_damage_max_amount); + 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(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(), + } + } +} diff --git a/enemize/src/rom.rs b/enemize/src/rom.rs index 11a9874..2807194 100644 --- a/enemize/src/rom.rs +++ b/enemize/src/rom.rs @@ -1,7 +1,164 @@ -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::Range; +use anyhow::bail; + +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 { + pub enemizer_info_table_base_address: usize, + pub enemizer_option_flags_base_address: usize, + spoiler: String, + rom_data: Vec, + patch_data: BTreeMap, + seed: i32, +} + +impl RomData { + pub fn spoiler(&self) -> &str { + self.spoiler.as_str() + } + + 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 + } + + 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.enemizer_info_table_base_address + ENEMIZER_INFO_SEED_OFFSET] == b'E' + && self.rom_data[self.enemizer_info_table_base_address + ENEMIZER_INFO_SEED_OFFSET + 1] == b'N' + } + + 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.enemizer_info_table_base_address + 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.enemizer_info_table_base_address + 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 get_enemizer_version(&self) -> anyhow::Result<&str> { + if self.is_enemizer() { + let version_start = self.enemizer_info_table_base_address + 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.enemizer_info_table_base_address + 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.enemizer_info_table_base_address + 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.enemizer_info_table_base_address + 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 patch_bytes(&mut self, address: usize, patch_data: Vec) { + self.rom_data + .splice(address..(address + patch_data.len()), patch_data); + } + + pub fn patch_data(&mut self, patch: Patch) { + self.set_rom_bytes( + &patch.patch_data, + patch.address..(patch.address + patch.patch_data.len()), + ); + } +}