diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7a5aed4f1cc537bcaa094886a3f522b7102f47..4b0a3186241ed2f065358cd2f15fe17d5bd7002b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,37 +19,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * -## [0.9.12] - 2023-06-20 +## [0.9.13] - 2023-08-01 + +### Changed + +* Improved command line parsing with positional ROM path value +* Better CI/CD for releases + +### Fixed + +* Small issue with command line arguments + +## [0.9.12] - 2023-08-01 ### Added * New WASM build -## [0.9.11] - 2023-06-20 +## [0.9.11] - 2023-08-01 ### Fixed * Build of a new release -## [0.9.10] - 2023-06-20 +## [0.9.10] - 2023-08-01 ### Fixed * Issue with release life-cycle -## [0.9.9] - 2023-06-20 +## [0.9.9] - 2023-08-01 ### Fixed * Issue with release life-cycle -## [0.9.8] - 2023-06-20 +## [0.9.8] - 2023-08-01 ### Added * Better release life-cycle -## [0.9.7] - 2023-06-20 +## [0.9.7] - 2023-08-01 ### Added diff --git a/Cargo.toml b/Cargo.toml index c0bfcc629f5d29b176f3fdc656b135badde546e8..c1119a157a0d5c48bd281ef09d7e10d980f1585a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "boytacean" description = "A Game Boy emulator that is written in Rust." -version = "0.9.12" +version = "0.9.13" authors = ["João Magalhães <joamag@gmail.com>"] license = "Apache-2.0" repository = "https://github.com/joamag/boytacean" diff --git a/README.md b/README.md index 7cc71d37fc85ec4eb49aeffeafffb23cdfacb42a..7b8c7221724bfc06b16551753d6f4ccd8e7626c3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A Game Boy emulator that is written in Rust 🦀. * Serial Data Transfer ([Link Cable](https://en.wikipedia.org/wiki/Game_Link_Cable)) support * Game Boy Printer emulation * Support for multiple MBCs: MBC1, MBC2, MBC3, and MBC5 +* Cheat support using [Game Genie](https://en.wikipedia.org/wiki/Game_Genie) codes * Variable CPU clock speed * Accurate PPU - passes [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) and [cgb-acid2](https://github.com/mattcurrie/cgb-acid2) tests diff --git a/frontends/libretro/Cargo.toml b/frontends/libretro/Cargo.toml index 859dff1f1dfe4f0d31da9cb9109938b7d49136a2..9b993f4a2166f01c586ef808e7565ef1e43f80ba 100644 --- a/frontends/libretro/Cargo.toml +++ b/frontends/libretro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boytacean-libretro" -version = "0.9.12" +version = "0.9.13" authors = ["João Magalhães <joamag@gmail.com>"] description = "A Lib Retro frontend for Boytacen" license = "Apache-2.0" @@ -11,5 +11,10 @@ readme = "README.md" [lib] crate-type = ["cdylib"] +[features] +debug = ["boytacean/debug"] +pedantic = ["boytacean/pedantic"] +cpulog = ["boytacean/cpulog"] + [dependencies.boytacean] path = "../.." diff --git a/frontends/libretro/res/boytacean_libretro.info b/frontends/libretro/res/boytacean_libretro.info index 3f40e722b1afc13221c1cd03f434910695efad3a..2966bc690d4219eda144abe4a9ef676ed1f34807 100644 --- a/frontends/libretro/res/boytacean_libretro.info +++ b/frontends/libretro/res/boytacean_libretro.info @@ -6,7 +6,7 @@ corename = "Boytacean" categories = "Emulator" license = "Apache-2." permissions = "" -display_version = "0.9.12" +display_version = "0.9.13" # Hardware Information manufacturer = "Nintendo" @@ -18,7 +18,7 @@ supports_no_game = "false" database = "Nintendo - Game Boy|Nintendo - Game Boy Color" savestate = "false" savestate_features = "null" -cheats = "false" +cheats = "true" input_descriptors = "true" memory_descriptors = "false" libretro_saves = "false" diff --git a/frontends/libretro/src/lib.rs b/frontends/libretro/src/lib.rs index 07b32d9dd8fb81252e7ab58ebd30d3f974bc41de..408b95e477b597e24f87224db5557777e422c48e 100644 --- a/frontends/libretro/src/lib.rs +++ b/frontends/libretro/src/lib.rs @@ -4,6 +4,7 @@ pub mod consts; use std::{ collections::HashMap, + ffi::CStr, fmt::{self, Display, Formatter}, os::raw::{c_char, c_float, c_uint, c_void}, slice::from_raw_parts, @@ -173,7 +174,7 @@ pub extern "C" fn retro_reset() { pub unsafe extern "C" fn retro_get_system_info(info: *mut RetroSystemInfo) { debugln!("retro_get_system_info()"); (*info).library_name = "Boytacean\0".as_ptr() as *const c_char; - (*info).library_version = "v0.9.12\0".as_ptr() as *const c_char; + (*info).library_version = "v0.9.13\0".as_ptr() as *const c_char; (*info).valid_extensions = "gb|gbc\0".as_ptr() as *const c_char; (*info).need_fullpath = false; (*info).block_extract = false; @@ -359,11 +360,21 @@ pub extern "C" fn retro_unserialize() { #[no_mangle] pub extern "C" fn retro_cheat_reset() { debugln!("retro_cheat_reset()"); + println!("retro_cheat_reset()"); + let emulator = unsafe { EMULATOR.as_mut().unwrap() }; + emulator.reset_cheats(); } #[no_mangle] -pub extern "C" fn retro_cheat_set() { +pub extern "C" fn retro_cheat_set(_index: c_uint, enabled: bool, code: *const c_char) { debugln!("retro_cheat_set()"); + if !enabled { + return; + } + let emulator = unsafe { EMULATOR.as_mut().unwrap() }; + let code_c = unsafe { CStr::from_ptr(code) }; + let code_s = code_c.to_string_lossy().into_owned(); + emulator.add_cheat_code(&code_s).unwrap(); } #[no_mangle] diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml index 04a7dab9c06c61f4c01fdb5f8be1ecdf49dac7d6..2ffea6e3d67bd0884989c0c87dd8594d9293d307 100644 --- a/frontends/sdl/Cargo.toml +++ b/frontends/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boytacean-sdl" -version = "0.9.12" +version = "0.9.13" authors = ["João Magalhães <joamag@gmail.com>"] description = "An SDL frontend for Boytacen" license = "Apache-2.0" diff --git a/frontends/web/package.json b/frontends/web/package.json index 9110434efb1640de9f471299ce1de51115121cc7..8fe26e28dc6e9528861e29e1f0f504c8c51aa5d9 100644 --- a/frontends/web/package.json +++ b/frontends/web/package.json @@ -1,6 +1,6 @@ { "name": "boytacean-web", - "version": "0.9.12", + "version": "0.9.13", "description": "The web version of Boytacean", "repository": { "type": "git", diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts index 6ec9010a9a7d8099e4c715093e38cbf130dfcf2b..88127a04f34a00346d54f184d59f9ca8f477d40e 100644 --- a/frontends/web/react/components/index.ts +++ b/frontends/web/react/components/index.ts @@ -3,4 +3,5 @@ export * from "./debug/debug"; export * from "./help/help"; export * from "./registers-gb/registers-gb"; export * from "./serial-section/serial-section"; +export * from "./test-section/test-section"; export * from "./tiles-gb/tiles-gb"; diff --git a/frontends/web/react/components/test-section/test-section.css b/frontends/web/react/components/test-section/test-section.css new file mode 100644 index 0000000000000000000000000000000000000000..c84791736aba1e86d93cc0ba3af0a0f88bcbad56 --- /dev/null +++ b/frontends/web/react/components/test-section/test-section.css @@ -0,0 +1,2 @@ +.test-section { +} diff --git a/frontends/web/react/components/test-section/test-section.tsx b/frontends/web/react/components/test-section/test-section.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d086e27c1ac37c325d23220650e07508e69e292e --- /dev/null +++ b/frontends/web/react/components/test-section/test-section.tsx @@ -0,0 +1,19 @@ +import React, { FC } from "react"; +import { TextInput } from "emukit"; + +import "./test-section.css"; + +type TestSectionProps = { + style?: string[]; +}; + +export const TestSection: FC<TestSectionProps> = ({ style = [] }) => { + const classes = () => ["test-section", ...style].join(" "); + return ( + <div className={classes()}> + <TextInput size="small" placeholder="XXX-XXX-XXX" /> + </div> + ); +}; + +export default TestSection; diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts index 6857cce7dffd58a3bbf8c50b953752be2f8d315d..8126bdf681a23720285a368a2a08ee83c846e342 100644 --- a/frontends/web/ts/gb.ts +++ b/frontends/web/ts/gb.ts @@ -24,7 +24,8 @@ import { DebugGeneral, HelpFaqs, HelpKeyboard, - SerialSection + SerialSection, + TestSection } from "../react"; import { @@ -531,6 +532,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { name: "Serial", icon: require("../res/serial.svg"), node: SerialSection({ emulator: this }) + }, + { + name: "Test", + node: TestSection({}) } ]; } diff --git a/src/gb.rs b/src/gb.rs index dee5b7dfd7b090d316dfc7bf2fba8ccbf3b3aded..6627cdee6680bdc44e18888c2ebb09d7835b4fe9 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -12,6 +12,7 @@ use crate::{ devices::{printer::PrinterDevice, stdout::StdoutDevice}, dma::Dma, gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION, VERSION}, + genie::{GameGenie, GameGenieCode}, mmu::Mmu, pad::{Pad, PadKey}, ppu::{ @@ -404,6 +405,7 @@ impl GameBoy { self.serial().reset(); self.mmu().reset(); self.cpu.reset(); + self.reset_cheats(); } pub fn reload(&mut self) { @@ -1027,6 +1029,34 @@ impl GameBoy { pub fn set_speed_callback(&mut self, callback: fn(speed: GameBoySpeed)) { self.mmu().set_speed_callback(callback); } + + pub fn reset_cheats(&mut self) { + self.reset_game_genie(); + } + + pub fn add_cheat_code(&mut self, code: &str) -> Result<bool, String> { + match self.add_game_genie_code(code) { + Ok(_) => Ok(true), + Err(message) => Err(message), + } + } + + pub fn reset_game_genie(&mut self) { + let rom = self.mmu().rom(); + if rom.game_genie().is_some() { + rom.game_genie().clone().unwrap().reset(); + } + } + + pub fn add_game_genie_code(&mut self, code: &str) -> Result<&GameGenieCode, String> { + let rom = self.mmu().rom(); + if rom.game_genie().is_none() { + let game_genie = GameGenie::default(); + rom.attach_genie(game_genie); + } + let game_genie = rom.game_genie_mut().as_mut().unwrap(); + game_genie.add_code(code) + } } #[cfg(feature = "wasm")] diff --git a/src/genie.rs b/src/genie.rs new file mode 100644 index 0000000000000000000000000000000000000000..f9a4b89bcc207be3e0475243781ecff4dbb47a47 --- /dev/null +++ b/src/genie.rs @@ -0,0 +1,260 @@ +use std::{ + collections::HashMap, + fmt::{self, Display, Formatter}, +}; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +#[derive(Clone)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub struct GameGenie { + /// Hash map that contains the complete set of Game Genie + /// codes that have been registered for the current ROM. + /// These codes are going to apply a series of patches to + /// the ROM effectively allowing the user to cheat. + codes: HashMap<u16, GameGenieCode>, +} + +impl GameGenie { + pub fn new() -> Self { + Self { + codes: HashMap::new(), + } + } + + pub fn reset(&mut self) { + self.codes.clear(); + } + + pub fn contains_addr(&self, addr: u16) -> bool { + self.codes.contains_key(&addr) + } + + pub fn get_addr(&self, addr: u16) -> &GameGenieCode { + self.codes.get(&addr).unwrap() + } + + pub fn add_code(&mut self, code: &str) -> Result<&GameGenieCode, String> { + let genie_code = match GameGenieCode::from_code(code, None) { + Ok(genie_code) => genie_code, + Err(message) => return Err(message), + }; + let addr = genie_code.addr; + self.codes.insert(addr, genie_code); + Ok(self.get_addr(addr)) + } +} + +impl Default for GameGenie { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone)] +pub struct GameGenieCode { + code: String, + addr: u16, + new_data: u8, + old_data: u8, + + /// A boolean value indicating whether the provided cheat code + /// was additive or not. If the code is additive, the new data + /// will be added to the old data, otherwise the new data will + /// replace the old data. + additive: bool, + + /// A boolean value indicating whether the provided cheat code + /// was condensed (7 characters) or extended (11 characters). + condensed: bool, +} + +impl GameGenieCode { + /// Creates a new Game Genie code structure from the provided string + /// in the ABC-DEF-GHI or ABC-DEF format. + /// Note that the additive mode (ex: ABC+DEF+GHI) can be optionally + /// handled or ignored using the `handle_additive` parameter. + pub fn from_code(code: &str, handle_additive: Option<bool>) -> Result<Self, String> { + let code_length = code.len(); + + if code_length != 11 && code_length != 7 { + return Err(format!( + "Invalid Game Genie code length: {} digits", + code_length + )); + } + let code_u = code.to_uppercase(); + + let additive = if handle_additive.unwrap_or(false) { + code_u.chars().nth(3).unwrap() == '+' + } else { + false + }; + let condensed = code_length == 7; + + let new_data_slice = &code_u[0..=1]; + let new_data = u8::from_str_radix(new_data_slice, 16).unwrap(); + + let old_data = if code_length == 11 { + let old_data_slice: String = format!("{}{}", &code_u[8..=8], &code_u[10..=10]); + u8::from_str_radix(old_data_slice.as_str(), 16) + .unwrap() + .rotate_right(2) + ^ 0xba + } else { + 0x00 + }; + + let addr_slice = format!("{}{}{}", &code_u[6..=6], &code_u[2..=2], &code_u[4..=5]); + let addr = u16::from_str_radix(addr_slice.as_str(), 16).unwrap() ^ 0xf000; + + Ok(Self { + code: code_u, + addr, + new_data, + old_data, + additive, + condensed, + }) + } + + /// Tests whether the provided value is valid for the current + /// Game Genie code. A value is valid if it matches the old + /// data or if the code is condensed. + pub fn is_valid(&self, value: u8) -> bool { + self.condensed || self.old_data == value + } + + /// Patches the provided value with the new data according to + /// the Game Genie code. If the code is additive, the new data + /// is added to the current value otherwise the new data is + /// returned (simple and normal patching operation). + pub fn patch_data(&self, value: u8) -> u8 { + if self.additive() { + value.saturating_add(self.new_data()) + } else { + self.new_data() + } + } + + pub fn code(&self) -> &str { + &self.code + } + + pub fn set_code(&mut self, code: String) { + self.code = code; + } + + pub fn addr(&self) -> u16 { + self.addr + } + + pub fn set_addr(&mut self, addr: u16) { + self.addr = addr; + } + + pub fn new_data(&self) -> u8 { + self.new_data + } + + pub fn set_new_data(&mut self, new_data: u8) { + self.new_data = new_data; + } + + pub fn old_data(&self) -> u8 { + self.old_data + } + + pub fn set_old_data(&mut self, old_data: u8) { + self.old_data = old_data; + } + + pub fn additive(&self) -> bool { + self.additive + } + + pub fn set_additive(&mut self, additive: bool) { + self.additive = additive; + } + + pub fn short_description(&self) -> String { + self.code.to_string() + } + + pub fn description(&self) -> String { + format!( + "Code: {}, Address: 0x{:04x}, New Data: 0x{:04x}, Old Data: 0x{:04x}", + self.code, self.addr, self.new_data, self.old_data + ) + } +} + +impl Display for GameGenieCode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.short_description()) + } +} + +#[cfg(test)] +mod tests { + use crate::genie::GameGenieCode; + + #[test] + fn test_from_code() { + let mut game_genie_code = GameGenieCode::from_code("00A-17B-C49", None).unwrap(); + assert_eq!(game_genie_code.code, "00A-17B-C49"); + assert_eq!(game_genie_code.addr, 0x4a17); + assert_eq!(game_genie_code.new_data, 0x00); + assert_eq!(game_genie_code.old_data, 0xc8); + assert_eq!(game_genie_code.additive, false); + assert_eq!(game_genie_code.condensed, false); + assert_eq!(game_genie_code.is_valid(0xc8), true); + assert_eq!(game_genie_code.is_valid(0xc9), false); + assert_eq!(game_genie_code.patch_data(0x12), 0x00); + + game_genie_code = GameGenieCode::from_code("00A+17B+C49", None).unwrap(); + assert_eq!(game_genie_code.code, "00A+17B+C49"); + assert_eq!(game_genie_code.addr, 0x4a17); + assert_eq!(game_genie_code.new_data, 0x00); + assert_eq!(game_genie_code.old_data, 0xc8); + assert_eq!(game_genie_code.additive, false); + assert_eq!(game_genie_code.condensed, false); + assert_eq!(game_genie_code.is_valid(0xc8), true); + assert_eq!(game_genie_code.is_valid(0xc9), false); + assert_eq!(game_genie_code.patch_data(0x12), 0x00); + + game_genie_code = GameGenieCode::from_code("00A+17B+C49", Some(true)).unwrap(); + assert_eq!(game_genie_code.code, "00A+17B+C49"); + assert_eq!(game_genie_code.addr, 0x4a17); + assert_eq!(game_genie_code.new_data, 0x00); + assert_eq!(game_genie_code.old_data, 0xc8); + assert_eq!(game_genie_code.additive, true); + assert_eq!(game_genie_code.condensed, false); + assert_eq!(game_genie_code.is_valid(0xc8), true); + assert_eq!(game_genie_code.is_valid(0xc9), false); + assert_eq!(game_genie_code.patch_data(0x12), 0x12); + + game_genie_code = GameGenieCode::from_code("00A+17B", None).unwrap(); + assert_eq!(game_genie_code.code, "00A+17B"); + assert_eq!(game_genie_code.addr, 0x4a17); + assert_eq!(game_genie_code.new_data, 0x00); + assert_eq!(game_genie_code.old_data, 0x00); + assert_eq!(game_genie_code.additive, false); + assert_eq!(game_genie_code.condensed, true); + assert_eq!(game_genie_code.is_valid(0xc8), true); + assert_eq!(game_genie_code.is_valid(0xc9), true); + assert_eq!(game_genie_code.patch_data(0x12), 0x00); + + game_genie_code = GameGenieCode::from_code("00A+17B", Some(true)).unwrap(); + assert_eq!(game_genie_code.code, "00A+17B"); + assert_eq!(game_genie_code.addr, 0x4a17); + assert_eq!(game_genie_code.new_data, 0x00); + assert_eq!(game_genie_code.old_data, 0x00); + assert_eq!(game_genie_code.additive, true); + assert_eq!(game_genie_code.condensed, true); + assert_eq!(game_genie_code.is_valid(0xc8), true); + assert_eq!(game_genie_code.is_valid(0xc9), true); + assert_eq!(game_genie_code.patch_data(0x12), 0x012); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0f8c9a49a05ec691e4c9fcacd0bc53de1e681123..f03d7b489cabbf079aa908f4e2d3db81a4945395 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod devices; pub mod dma; pub mod gb; pub mod gen; +pub mod genie; pub mod inst; pub mod macros; pub mod mmu; diff --git a/src/ppu.rs b/src/ppu.rs index 0639b11fccbcb6de4b947c2d29e1a33efbf5a634..587d93fdd084bca442168fa02134b7e4fdf7ef79 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -222,7 +222,7 @@ impl Display for ObjectData { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, - "Index => {}\nX => {}\nY => {}\nTile => {}", + "Index: {}, X: {}, Y: {}, Tile: {}", self.index, self.x, self.y, self.tile ) } @@ -260,7 +260,7 @@ impl Display for TileData { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, - "Palette => {}\nVRAM Bank => {}\nX Flip => {}\nY Flip => {}", + "Palette: {}, VRAM Bank: {}, X Flip: {}, Y Flip: {}", self.palette, self.vram_bank, self.xflip, self.yflip ) } diff --git a/src/rom.rs b/src/rom.rs index 031285e469cf01b762420a28fab28af592b60825..a781cf1d6bab4629e1ce5808869bc2f7104fb428 100644 --- a/src/rom.rs +++ b/src/rom.rs @@ -4,7 +4,7 @@ use std::{ fmt::{Display, Formatter}, }; -use crate::{debugln, gb::GameBoyMode, util::read_file, warnln}; +use crate::{debugln, gb::GameBoyMode, genie::GameGenie, util::read_file, warnln}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -227,6 +227,13 @@ pub struct Cartridge { /// RAM and ROM access on the current cartridge. mbc: &'static Mbc, + /// The current memory handler in charge of handling the + /// memory access for the current cartridge. + /// Typically this is the same as the MBC, but to allow + /// memory patching (ex: Game Genie) we may need another + /// level of indirection. + handler: &'static Mbc, + /// The number of ROM banks (of 8KB) that are available /// to the current cartridge, this is a computed value /// to allow improved performance. @@ -249,9 +256,9 @@ pub struct Cartridge { /// control of memory access to avoid corruption. ram_enabled: bool, - // The final offset of the last character of the title - // that is considered to be non zero (0x0) so that a - // proper safe conversion to UTF-8 string can be done. + /// The final offset of the last character of the title + /// that is considered to be non zero (0x0) so that a + /// proper safe conversion to UTF-8 string can be done. title_offset: usize, /// The current rumble state of the cartridge, this @@ -261,6 +268,11 @@ pub struct Cartridge { /// Callback function to be called whenever there's a new /// rumble vibration triggered or when it's disabled. rumble_cb: fn(active: bool), + + /// Optional reference to the Game Genie instance that + /// would be used for the "cheating" by patching the + /// current ROM's cartridge data. + game_genie: Option<GameGenie>, } impl Cartridge { @@ -269,6 +281,7 @@ impl Cartridge { rom_data: vec![], ram_data: vec![], mbc: &NO_MBC, + handler: &NO_MBC, rom_bank_count: 0, ram_bank_count: 0, rom_offset: 0x4000, @@ -277,6 +290,7 @@ impl Cartridge { title_offset: 0x0143, rumble_active: false, rumble_cb: |_| {}, + game_genie: None, } } @@ -294,9 +308,9 @@ impl Cartridge { pub fn read(&self, addr: u16) -> u8 { match addr & 0xf000 { 0x0000 | 0x1000 | 0x2000 | 0x3000 | 0x4000 | 0x5000 | 0x6000 | 0x7000 => { - (self.mbc.read_rom)(self, addr) + (self.handler.read_rom)(self, addr) } - 0xa000 | 0xb000 => (self.mbc.read_ram)(self, addr), + 0xa000 | 0xb000 => (self.handler.read_ram)(self, addr), _ => { debugln!("Reading from unknown Cartridge control 0x{:04x}", addr); 0x00 @@ -307,9 +321,9 @@ impl Cartridge { pub fn write(&mut self, addr: u16, value: u8) { match addr & 0xf000 { 0x0000 | 0x1000 | 0x2000 | 0x3000 | 0x4000 | 0x5000 | 0x6000 | 0x7000 => { - (self.mbc.write_rom)(self, addr, value) + (self.handler.write_rom)(self, addr, value) } - 0xa000 | 0xb000 => (self.mbc.write_ram)(self, addr, value), + 0xa000 | 0xb000 => (self.handler.write_ram)(self, addr, value), _ => debugln!("Writing to unknown Cartridge address 0x{:04x}", addr), } } @@ -396,6 +410,7 @@ impl Cartridge { fn set_mbc(&mut self) { self.mbc = self.get_mbc(); + self.handler = self.mbc; } fn set_computed(&mut self) { @@ -424,6 +439,18 @@ impl Cartridge { self.title_offset = 0x0134 + offset; } + pub fn game_genie(&self) -> &Option<GameGenie> { + &self.game_genie + } + + pub fn game_genie_mut(&mut self) -> &mut Option<GameGenie> { + &mut self.game_genie + } + + pub fn set_game_genie(&mut self, game_genie: Option<GameGenie>) { + self.game_genie = game_genie; + } + fn allocate_ram(&mut self) { let ram_banks = max(self.ram_size().ram_banks(), 1); self.ram_data = vec![0u8; ram_banks as usize * RAM_BANK_SIZE]; @@ -603,6 +630,16 @@ impl Cartridge { self.ram_data = vec![0u8; self.ram_data.len()]; } + pub fn attach_genie(&mut self, game_genie: GameGenie) { + self.game_genie = Some(game_genie); + self.handler = &GAME_GENIE; + } + + pub fn detach_genie(&mut self) { + self.game_genie = None; + self.handler = self.mbc; + } + pub fn description(&self, column_length: usize) -> String { let name_l = format!("{:width$}", "Name", width = column_length); let type_l = format!("{:width$}", "Type", width = column_length); @@ -856,6 +893,36 @@ pub static MBC5: Mbc = Mbc { }, }; +pub static GAME_GENIE: Mbc = Mbc { + name: "GameGenie", + read_rom: |rom: &Cartridge, addr: u16| -> u8 { + let game_genie = rom.game_genie.as_ref().unwrap(); + if game_genie.contains_addr(addr) { + // retrieves the Game Genie code that matches the current address + // keep in mind that this assumes that no more that one code is + // registered for the same memory address + let genie_code = game_genie.get_addr(addr); + + // obtains the current byte that is stored at the address using + // the MBC, this value will probably be patched + let data = (rom.mbc.read_rom)(rom, addr); + + // checks if the current data at the address is the same as the + // one that is expected by the Game Genie code, if that's the case + // applies the patch, otherwise returns the original strategy is + // going to be used + if genie_code.is_valid(data) { + debugln!("Applying Game Genie code: {}", genie_code); + return genie_code.patch_data(data); + } + } + (rom.mbc.read_rom)(rom, addr) + }, + write_rom: |rom: &mut Cartridge, addr: u16, value: u8| (rom.mbc.write_rom)(rom, addr, value), + read_ram: |rom: &Cartridge, addr: u16| -> u8 { (rom.mbc.read_ram)(rom, addr) }, + write_ram: |rom: &mut Cartridge, addr: u16, value: u8| (rom.mbc.write_ram)(rom, addr, value), +}; + #[cfg(test)] mod tests { use super::{Cartridge, RomType};