Compare commits
3 commits
a161d9090e
...
4112573d85
Author | SHA1 | Date | |
---|---|---|---|
Lyle Mantooth | 4112573d85 | ||
Lyle Mantooth | 7cd01df7d6 | ||
Lyle Mantooth | 7a6689efd1 |
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -8,6 +8,16 @@ version = "1.0.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base_patch_generator"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"enemize",
|
||||||
|
"md5",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bin_comp"
|
name = "bin_comp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -3,4 +3,5 @@
|
||||||
members = [
|
members = [
|
||||||
"enemize",
|
"enemize",
|
||||||
"bin_comp",
|
"bin_comp",
|
||||||
|
"base_patch_generator",
|
||||||
]
|
]
|
||||||
|
|
12
base_patch_generator/Cargo.toml
Normal file
12
base_patch_generator/Cargo.toml
Normal file
|
@ -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"
|
56
base_patch_generator/src/main.rs
Normal file
56
base_patch_generator/src/main.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use enemize::PatchSet;
|
||||||
|
use enemize::asar;
|
||||||
|
use enemize::rom::RomData;
|
||||||
|
|
||||||
|
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 symbols = asar::load_symbols(Path::new(&symbols_path))?;
|
||||||
|
|
||||||
|
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);
|
||||||
|
let out_file = File::create(&output_path)?;
|
||||||
|
serde_json::to_writer(out_file, &patches)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,33 +1,44 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufReader, Read};
|
use std::io::{BufReader, BufRead};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub type Symbols = HashMap<String, u32>;
|
pub type Symbols = HashMap<String, usize>;
|
||||||
|
|
||||||
impl Symbols {
|
pub fn load_symbols(filename: &Path) -> anyhow::Result<Symbols> {
|
||||||
pub fn load(filename: Path) -> anyhow::Result<Symbols> {
|
|
||||||
let file = File::open(filename)?;
|
let file = File::open(filename)?;
|
||||||
let mut reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
reader.lines().filter_map(|l| l.ok().and_then(|line| {
|
let symbols = reader.lines().filter_map(|l| l.ok().and_then(|line| {
|
||||||
let words: Vec<String> = line.split_ascii_whitespace().collect();
|
let mut words = line.split_ascii_whitespace();
|
||||||
if words.len() == 2 {
|
match (words.next(), words.next(), words.next()) {
|
||||||
Some((words[1], words[0]))
|
// Get only two-word lines.
|
||||||
} else {
|
(Some(address), Some(symbol), None) =>
|
||||||
None
|
Some((symbol.to_owned(), address.to_owned())),
|
||||||
|
|
||||||
|
_ => None
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
.filter_map(|(symbol, mut address)| {
|
.and_then(|(symbol, mut address)| {
|
||||||
if let Some(colon_at) = address.find(':') {
|
if let Some(colon_at) = address.find(':') {
|
||||||
address.remove(colon_at);
|
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 snes_address = u32::from_str_radix(&address, 16).ok()?;
|
||||||
|
|
||||||
let pc_address = (snes_address & 0x7FFF) + ((addr / 2) & 0xFF8000);
|
let pc_address = snes_to_pc_address(snes_address);
|
||||||
|
|
||||||
Some((symbol, pc_address))
|
Some((symbol, pc_address))
|
||||||
}).collect()
|
})).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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pub const ROM_HEADER_BANK_LOCATION: usize = 0x0B5E7;
|
pub const ROOM_HEADER_BANK_LOCATION: usize = 0x0B5E7;
|
||||||
pub const DUNGEON_HEADER_POINTER_TABLE: usize = 0x271E2;
|
pub const DUNGEON_HEADER_POINTER_TABLE: usize = 0x271E2;
|
||||||
pub const DUNGEON_SPRITE_POINTER_TABLE: usize = 0x4D62E;
|
pub const DUNGEON_SPRITE_POINTER_TABLE: usize = 0x4D62E;
|
||||||
pub const OBJECT_DATA_POINTER_TABLE: usize = 0xF8000;
|
pub const OBJECT_DATA_POINTER_TABLE: usize = 0xF8000;
|
||||||
|
|
|
@ -7,6 +7,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use crate::rom::RomData;
|
use crate::rom::RomData;
|
||||||
|
|
||||||
|
pub mod asar;
|
||||||
pub mod bosses;
|
pub mod bosses;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod option_flags;
|
pub mod option_flags;
|
||||||
|
@ -41,12 +42,20 @@ impl PatchSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_patch(&mut self, patch: Patch) {
|
||||||
|
self.patches.push(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_patches(&mut self, patches: Vec<Patch>) {
|
||||||
|
self.patches.extend(patches);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn filename(&self) -> &Path {
|
pub fn filename(&self) -> &Path {
|
||||||
self.filename.as_path()
|
self.filename.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn patch_rom(self, rom: &mut RomData) {
|
pub fn patch_rom(&self, rom: &mut RomData) {
|
||||||
for patch in self.patches {
|
for patch in self.patches.iter() {
|
||||||
rom.patch_data(patch);
|
rom.patch_data(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::ops::Range;
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
|
|
||||||
|
use crate::asar::{Symbols, snes_to_pc_address, pc_to_snes_address};
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::option_flags::OptionFlags;
|
use crate::option_flags::OptionFlags;
|
||||||
use crate::Patch;
|
use crate::Patch;
|
||||||
|
@ -28,8 +29,7 @@ pub const CHECKSUM_ADDRESS: usize = 0x7FDE;
|
||||||
pub const RANDOMIZER_MODE_FLAG: usize = 0x180032;
|
pub const RANDOMIZER_MODE_FLAG: usize = 0x180032;
|
||||||
|
|
||||||
pub struct RomData {
|
pub struct RomData {
|
||||||
pub enemizer_info_table_base_address: usize,
|
asar_symbols: Symbols,
|
||||||
pub enemizer_option_flags_base_address: usize,
|
|
||||||
spoiler: String,
|
spoiler: String,
|
||||||
rom_data: Vec<u8>,
|
rom_data: Vec<u8>,
|
||||||
patch_data: BTreeMap<usize, u8>,
|
patch_data: BTreeMap<usize, u8>,
|
||||||
|
@ -65,8 +65,8 @@ impl RomData {
|
||||||
|
|
||||||
pub fn is_enemizer(&self) -> bool {
|
pub fn is_enemizer(&self) -> bool {
|
||||||
self.rom_data.len() == ENEMIZER_FILE_LENGTH
|
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.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET] == b'E'
|
||||||
&& self.rom_data[self.enemizer_info_table_base_address + ENEMIZER_INFO_SEED_OFFSET + 1] == b'N'
|
&& self.rom_data[self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET + 1] == b'N'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_rom_length(&self) {
|
fn assert_rom_length(&self) {
|
||||||
|
@ -79,7 +79,7 @@ impl RomData {
|
||||||
|
|
||||||
pub fn derive_enemizer_seed(&mut self) -> anyhow::Result<i32> {
|
pub fn derive_enemizer_seed(&mut self) -> anyhow::Result<i32> {
|
||||||
if self.seed < 0 && self.is_enemizer() {
|
if self.seed < 0 && self.is_enemizer() {
|
||||||
let seed_start = self.enemizer_info_table_base_address + ENEMIZER_INFO_SEED_OFFSET;
|
let seed_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET;
|
||||||
let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH;
|
let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH;
|
||||||
let seed_bytes = &self.rom_data[seed_start..seed_end];
|
let seed_bytes = &self.rom_data[seed_start..seed_end];
|
||||||
let seed_str = &std::str::from_utf8(seed_bytes)?.trim_end_matches('\0')[2..];
|
let seed_str = &std::str::from_utf8(seed_bytes)?.trim_end_matches('\0')[2..];
|
||||||
|
@ -96,7 +96,7 @@ impl RomData {
|
||||||
let seed_str = format!("EN{:<10}", seed);
|
let seed_str = format!("EN{:<10}", seed);
|
||||||
let bytes = seed_str.as_bytes().to_owned();
|
let bytes = seed_str.as_bytes().to_owned();
|
||||||
|
|
||||||
let seed_start = self.enemizer_info_table_base_address + ENEMIZER_INFO_SEED_OFFSET;
|
let seed_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_SEED_OFFSET;
|
||||||
let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH;
|
let seed_end = seed_start + ENEMIZER_INFO_SEED_LENGTH;
|
||||||
self.seed = seed;
|
self.seed = seed;
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ impl RomData {
|
||||||
|
|
||||||
pub fn get_enemizer_version(&self) -> anyhow::Result<&str> {
|
pub fn get_enemizer_version(&self) -> anyhow::Result<&str> {
|
||||||
if self.is_enemizer() {
|
if self.is_enemizer() {
|
||||||
let version_start = self.enemizer_info_table_base_address + ENEMIZER_INFO_VERSION_OFFSET;
|
let version_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_VERSION_OFFSET;
|
||||||
let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH;
|
let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH;
|
||||||
Ok(std::str::from_utf8(&self.rom_data[version_start..version_end])?)
|
Ok(std::str::from_utf8(&self.rom_data[version_start..version_end])?)
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,7 +120,7 @@ impl RomData {
|
||||||
let mut bytes = version.into_bytes();
|
let mut bytes = version.into_bytes();
|
||||||
bytes.resize(ENEMIZER_INFO_VERSION_LENGTH, 0);
|
bytes.resize(ENEMIZER_INFO_VERSION_LENGTH, 0);
|
||||||
|
|
||||||
let version_start = self.enemizer_info_table_base_address + ENEMIZER_INFO_VERSION_OFFSET;
|
let version_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_VERSION_OFFSET;
|
||||||
let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH;
|
let version_end = version_start + ENEMIZER_INFO_VERSION_LENGTH;
|
||||||
self.set_rom_bytes(&bytes, version_start..version_end);
|
self.set_rom_bytes(&bytes, version_start..version_end);
|
||||||
self.set_patch_bytes(version_start..version_end)
|
self.set_patch_bytes(version_start..version_end)
|
||||||
|
@ -132,7 +132,7 @@ impl RomData {
|
||||||
bail!("Option flags is too long to fit in the space allocated. Need to move data/code in asm file.");
|
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_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_FLAGS_OFFSET;
|
||||||
let flags_end = flags_start + bytes.len();
|
let flags_end = flags_start + bytes.len();
|
||||||
self.set_rom_bytes(&bytes, flags_start..flags_end);
|
self.set_rom_bytes(&bytes, flags_start..flags_end);
|
||||||
self.set_patch_bytes(flags_start..flags_end);
|
self.set_patch_bytes(flags_start..flags_end);
|
||||||
|
@ -142,7 +142,7 @@ impl RomData {
|
||||||
|
|
||||||
pub fn get_info_flags(&self) -> Option<OptionFlags> {
|
pub fn get_info_flags(&self) -> Option<OptionFlags> {
|
||||||
if self.is_enemizer() {
|
if self.is_enemizer() {
|
||||||
let flags_start = self.enemizer_info_table_base_address + ENEMIZER_INFO_FLAGS_OFFSET;
|
let flags_start = self.asar_symbols["enemizer_info_table"] + ENEMIZER_INFO_FLAGS_OFFSET;
|
||||||
let flags_end = flags_start + ENEMIZER_INFO_FLAGS_LENGTH;
|
let flags_end = flags_start + ENEMIZER_INFO_FLAGS_LENGTH;
|
||||||
OptionFlags::try_from_bytes(&self.rom_data[flags_start..flags_end]).ok()
|
OptionFlags::try_from_bytes(&self.rom_data[flags_start..flags_end]).ok()
|
||||||
} else {
|
} else {
|
||||||
|
@ -150,15 +150,72 @@ impl RomData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new(asar_symbols: Symbols, rom_data: Vec<u8>) -> 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_bytes(&mut self, address: usize, patch_data: Vec<u8>) {
|
pub fn patch_bytes(&mut self, address: usize, patch_data: Vec<u8>) {
|
||||||
self.rom_data
|
self.rom_data
|
||||||
.splice(address..(address + patch_data.len()), patch_data);
|
.splice(address..(address + patch_data.len()), patch_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn patch_data(&mut self, patch: Patch) {
|
pub fn patch_data(&mut self, patch: &Patch) {
|
||||||
self.set_rom_bytes(
|
self.set_rom_bytes(
|
||||||
&patch.patch_data,
|
&patch.patch_data,
|
||||||
patch.address..(patch.address + patch.patch_data.len()),
|
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["rom_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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue