Add RomData, OptionFlags, and enums for them.
Fun error handling with automatic, fallible conversion to and from u8s for the enums.
This commit is contained in:
parent
c81348fb10
commit
a161d9090e
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -9,3 +9,4 @@ edition = "2021"
|
|||
anyhow = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1"
|
||||
|
|
33
enemize/src/asar.rs
Normal file
33
enemize/src/asar.rs
Normal file
|
@ -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<String, u32>;
|
||||
|
||||
impl Symbols {
|
||||
pub fn load(filename: Path) -> anyhow::Result<Symbols> {
|
||||
let file = File::open(filename)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
reader.lines().filter_map(|l| l.ok().and_then(|line| {
|
||||
let words: Vec<String> = 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()
|
||||
}
|
||||
}
|
48
enemize/src/bosses/mod.rs
Normal file
48
enemize/src/bosses/mod.rs
Normal file
|
@ -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<u8> for BossType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
enemize/src/constants.rs
Normal file
12
enemize/src/constants.rs
Normal file
|
@ -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;
|
|
@ -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<T>(PhantomData<T>);
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Patch {
|
||||
pub address: usize,
|
||||
|
|
571
enemize/src/option_flags.rs
Normal file
571
enemize/src/option_flags.rs
Normal file
|
@ -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<u8> for RandomizeEnemiesType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for RandomizeEnemyHpType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for RandomizeBossesType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for SwordType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
match byte {
|
||||
0 => Ok(Self::Normal),
|
||||
_ => Err(InvalidEnumError(PhantomData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ShieldType {
|
||||
Normal
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ShieldType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for AbsorbableType {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for HeartBeepSpeed {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<u8> for BeeLevel {
|
||||
type Error = InvalidEnumError<Self>;
|
||||
|
||||
fn try_from(byte: u8) -> Result<Self, Self::Error> {
|
||||
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<AbsorbableType, bool>,
|
||||
|
||||
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<Self> {
|
||||
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<u8> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<u8>,
|
||||
patch_data: BTreeMap<usize, u8>,
|
||||
seed: i32,
|
||||
}
|
||||
|
||||
impl RomData {
|
||||
pub fn spoiler(&self) -> &str {
|
||||
self.spoiler.as_str()
|
||||
}
|
||||
|
||||
pub fn generate_patch(&self) -> Vec<Patch> {
|
||||
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<usize>) {
|
||||
self.rom_data.splice(range, bytes.into_iter().map(|&b| b));
|
||||
}
|
||||
|
||||
fn set_patch_bytes(&mut self, range: Range<usize>) {
|
||||
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<i32> {
|
||||
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<OptionFlags> {
|
||||
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<u8>) {
|
||||
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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue