//! Super Mario Land RAM addresses, with a source for every one. //! //! Taken from `docs/design/platformer.md` §1, which took them from Kasper //! Meerts' disassembly (): //! `hram.asm` and `wram.asm` name the symbols, and the cited `bank0.asm` lines //! are the code that gives each byte its meaning. //! //! Two classes of address live here: //! //! - **named**, appearing as a label in `hram.asm` or `wram.asm`. Trusted. //! - **UNVERIFIED**, appearing in the disassembly only as a comment on a bare //! literal. Every one carries an `UNVERIFIED` marker and is listed in //! [`UNVERIFIED`] so a bring-up spike and the honesty panel can enumerate //! them. `docs/design/platformer.md` §8.1 asks for a bgb/mGBA watchpoint pass //! on each before this adapter ships against a real cartridge. //! //! Only WRAM (`0xC000..=0xDFFF`) and HRAM (`0xFF80..=0xFFFE`) are read. VRAM is //! deliberately never read: binjgb's public read path returns `0xFF` for VRAM //! during PPU mode 3 (`docs/design/platformer.md` §1, "Is reading VRAM //! acceptable?"), so a HUD read would be silently wrong one frame in three. #![allow(dead_code, non_upper_case_globals)] /// Disassembly this map was read from. pub const SML_DISASSEMBLY: &str = "kaspermeerts/supermarioland"; /// SHA-1 the disassembly builds to, i.e. the revision these addresses describe: /// Super Mario Land (World) (Rev A). Recorded for provenance only — the adapter /// pins a SHA-256 supplied by configuration, because the Rev A SHA-256 is not /// known here (`docs/design/platformer.md` §8.5). pub const SML_ROM_SHA1: &str = "418203621b887caa090215d97e3f509b79affd3e"; /// HRAM, from `hram.asm` unless marked otherwise. pub mod hram { /// 0 while the game runs, nonzero while paused. hram.asm; bank0.asm:1083-1090. pub const hGamePaused: u16 = 0xffb2; /// Game-state jump table index. hram.asm; table listed at bank0.asm:2A6. /// `0x00` normal gameplay, `0x01` dead, `0x02` reset to checkpoint, /// `0x03` pre-dying, `0x04` dying, `0x05` explosion / score countdown, /// `0x06` end of level, `0x07` end-of-level gate, `0x08` increment level, /// `0x09`-`0x0C` pipe transitions, `0x0D` autoscrolling level, /// `0x0E` init menu, `0x0F` start menu, `0x11` level start, `0x12` bonus /// game, `0x39` game-over text, `0x3A` game-over wait /// (bank0.asm:4291-4348, 2784-2786). pub const hGameState: u16 = 0xffb3; /// World and level as BCD nibbles: 1-1 is `0x11`. hram.asm; encoding at /// bank0.asm:711, 746. pub const hWorldAndLevel: u16 = 0xffb4; /// Nonzero while Superball Mario. hram.asm. pub const hSuperballMario: u16 = 0xffb5; /// Written straight to `rSCX`. hram.asm; bank0.asm:2775-2776. pub const hScrollX: u16 = 0xffa4; /// 0 small, 1 growing, 2 super, 3+ injury i-frames. hram.asm; /// bank0.asm:1116, 1380-1385, 1408 (`InjureMario`). pub const hSuperStatus: u16 = 0xff99; /// Game clears. hram.asm; incremented at bank0.asm:3141-3144 after /// "THE END". Mirrored by [`super::wram::wWinCount`]. pub const hWinCount: u16 = 0xff9a; /// Stomp-chain timer, 50 frames. hram.asm; bank0.asm:1285-1301. pub const hStompChainTimer: u16 = 0xff9c; /// Stomp chain, capped at 3. hram.asm; bank0.asm:1285-1301. pub const hStompChain: u16 = 0xff9d; /// Level index, 0..11. hram.asm; `cp a, $0C ; 12 levels in total` /// bank0.asm:700-702. pub const hLevelIndex: u16 = 0xffe4; /// Column-loader screen index, incremented every 20 columns. hram.asm; /// bank0.asm:5215-5223. Levels start at 3 (bank0.asm:2071). pub const hScreenIndex: u16 = 0xffe5; /// Column within the screen, 0..19. hram.asm; `cp a, $14 ; 20 columns per /// screen?` bank0.asm:5217. pub const hColumnIndex: u16 = 0xffe6; /// Coins, BCD, 0..99. hram.asm. pub const hCoins: u16 = 0xfffa; /// **UNVERIFIED.** Attract-demo gate. Unnamed in hram.asm; bank0.asm:341 /// comments it "Equal to 28 in menu and during demo", and `Call_2113` /// (bank0.asm:5060-5066) overwrites `hJoyHeld` from `0xC0DB` only while it /// is nonzero. The most load-bearing unverified byte in this map: without it /// the attract demo farms rewards. pub const UNNAMED_DEMO_GATE: u16 = 0xff9f; /// **UNVERIFIED.** Nonzero underground (pipe sub-room). Comment-only. /// Suppresses `band` payouts and snapshots, because pipe sub-rooms reuse /// `hScreenIndex` and would alias onto the main level's bands /// (`docs/design/platformer.md` §8.3). pub const UNNAMED_UNDERGROUND: u16 = 0xfff9; /// **UNVERIFIED.** Pipe-exit pair, comment-only. Unused by this adapter; /// recorded because §8.3 revisits underground banding once they are /// confirmed. pub const UNNAMED_PIPE_EXIT_LO: u16 = 0xfff4; /// **UNVERIFIED.** See [`UNNAMED_PIPE_EXIT_LO`]. pub const UNNAMED_PIPE_EXIT_HI: u16 = 0xfff5; } /// WRAM, from `wram.asm` unless marked otherwise. pub mod wram { /// Score, 3 bytes BCD, most significant byte first. wram.asm. pub const wScore: u16 = 0xc0a0; /// `+1` on a 1UP (bank0.asm:1399), `0xFF` on death (bank0.asm:925). wram.asm. pub const wLivesEarnedLost: u16 = 0xc0a3; /// Nonzero while the game-over window is up. wram.asm. pub const wGameOverWindowEnabled: u16 = 0xc0a5; /// Nonzero once the game-over timer has expired. wram.asm. pub const wGameOverTimerExpired: u16 = 0xc0ad; /// Invincibility (star) timer. wram.asm. pub const wInvincibilityTimer: u16 = 0xc0d3; /// Mirror of [`super::hram::hWinCount`]. wram.asm. pub const wWinCount: u16 = 0xc0e1; /// Game timer, 3 bytes BCD. wram.asm. pub const wGameTimer: u16 = 0xda00; /// Remaining lives. wram.asm. pub const wLives: u16 = 0xda15; /// Nonzero once the game timer is running out. wram.asm. pub const wGameTimerExpiringFlag: u16 = 0xda1d; /// **UNVERIFIED.** Jump status: 0 on ground, 1 ascending, 2 descending. /// Comment-only, bank0.asm:4409. Part of the safe-snapshot gate. pub const UNNAMED_JUMP_STATUS: u16 = 0xc207; /// **UNVERIFIED.** 1 while on the ground. Comment-only, bank0.asm:1258 /// (`ld hl, $C20A ; 1 if on ground`). Part of the safe-snapshot gate. pub const UNNAMED_ON_GROUND: u16 = 0xc20a; /// **UNVERIFIED.** Running momentum, 0..6. Comment-only, /// bank0.asm:4418-4429. Unused; recorded because it is the reason the /// platformer decoder preset holds a direction gaplessly. pub const UNNAMED_MOMENTUM: u16 = 0xc20c; /// **UNVERIFIED.** Set to `0x02` once momentum saturates. Comment-only, /// bank0.asm:4418-4429. Unused, same reason as [`UNNAMED_MOMENTUM`]. pub const UNNAMED_RUNNING: u16 = 0xc20e; /// **UNVERIFIED.** "sort of progress in the level in columns / 2", /// bank0.asm:3361. Unused: the two named HRAM bytes are preferred. pub const UNNAMED_COLUMN_PROGRESS: u16 = 0xc0ab; /// **UNVERIFIED.** End-of-level counter, comment-only. Unused. pub const UNNAMED_END_OF_LEVEL: u16 = 0xc0d2; } /// Every address this map could not verify against a disassembly label, as /// `(address, what it is believed to be)`. Ordered by address, so the list reads /// the same every time it is printed. pub const UNVERIFIED: [(u16, &str); 10] = [ (wram::UNNAMED_COLUMN_PROGRESS, "column progress (unused)"), (wram::UNNAMED_END_OF_LEVEL, "end-of-level counter (unused)"), (wram::UNNAMED_JUMP_STATUS, "jump status"), (wram::UNNAMED_ON_GROUND, "on the ground"), (wram::UNNAMED_MOMENTUM, "running momentum (unused)"), (wram::UNNAMED_RUNNING, "running flag (unused)"), (hram::UNNAMED_DEMO_GATE, "attract-demo gate"), (hram::UNNAMED_PIPE_EXIT_LO, "pipe exit low (unused)"), (hram::UNNAMED_PIPE_EXIT_HI, "pipe exit high (unused)"), (hram::UNNAMED_UNDERGROUND, "underground"), ]; /// Addresses the adapter actually reads and whose meaning is unverified. A /// subset of [`UNVERIFIED`]; the rest are recorded for the bring-up spike only. pub const UNVERIFIED_AND_USED: [u16; 4] = [ hram::UNNAMED_DEMO_GATE, hram::UNNAMED_UNDERGROUND, wram::UNNAMED_JUMP_STATUS, wram::UNNAMED_ON_GROUND, ]; /// Screens per level, from `levels/levels.asm`: one screen pointer per screen, /// terminated by `db $ff`. Indexed by `hLevelIndex` (0 = 1-1 .. 11 = 4-3). pub const LEVEL_SCREENS: [u32; 12] = [18, 17, 18, 19, 17, 21, 26, 19, 18, 26, 23, 27]; /// Levels in the game; `hLevelIndex < 12` (`cp a, $0C`, bank0.asm:700-702). pub const LEVEL_COUNT: u32 = 12; /// Every level starts at this screen (`ld a, $03 / ldh [hScreenIndex], a ; do /// all levels start on screen 3`, bank0.asm:2071). pub const FIRST_SCREEN: u32 = 3; /// Columns per screen (`cp a, $14`, bank0.asm:5217). pub const COLUMNS_PER_SCREEN: u32 = 20; /// Column index a level starts at: [`FIRST_SCREEN`] * [`COLUMNS_PER_SCREEN`]. pub const FIRST_COLUMN: u32 = FIRST_SCREEN * COLUMNS_PER_SCREEN; /// Columns per reward band. Ten columns is half a screen. pub const BAND_COLUMNS: u32 = 10; /// Checkpoint screens a death can restore to, from `GameState_02` /// (bank0.asm:952-978); the matching `0xC0AB` values are `0C, 34, 5C, 84, AC, /// D4`. Display only — the adapter does not depend on them. pub const CHECKPOINT_SCREENS: [u32; 6] = [3, 7, 11, 15, 19, 23]; /// Normal, controllable gameplay. pub const GAME_STATE_NORMAL: u8 = 0x00; /// Autoscrolling (vehicle) level. Playable, never safe to snapshot. pub const GAME_STATE_AUTOSCROLL: u8 = 0x0d; /// Prepares the game-over text (bank0.asm:4291-4348). pub const GAME_STATE_GAME_OVER_TEXT: u8 = 0x39; /// Game-over wait (`cp a, $3A / jr nz, .out ; game not over`, /// bank0.asm:2784-2786). pub const GAME_STATE_GAME_OVER_WAIT: u8 = 0x3a; /// Screens in `level`, or `None` for an out-of-range index. pub fn screens(level: u32) -> Option { LEVEL_SCREENS.get(level as usize).copied() } /// Columns of playable ground in `level`: `(screens - 3) * 20`. pub fn level_columns(level: u32) -> Option { screens(level).map(|screens| (screens - FIRST_SCREEN) * COLUMNS_PER_SCREEN) } /// Reward bands in `level`: `2 * (screens - 3)`, the cap in /// `docs/design/platformer.md` §2 (30 for 1-1, 48 for 4-3). pub fn level_bands(level: u32) -> Option { level_columns(level).map(|columns| columns / BAND_COLUMNS) } /// Decode a big-endian BCD field of one to three bytes. `None` when any nibble /// is above 9, which is how a mid-write or a wrong-revision read shows up. pub fn bcd(bytes: &[u8]) -> Option { let mut value = 0u32; for byte in bytes { let (high, low) = (byte >> 4, byte & 0x0f); if high > 9 || low > 9 { return None; } value = value * 100 + u32::from(high) * 10 + u32::from(low); } Some(value) } #[cfg(test)] mod tests { use super::*; #[test] fn the_level_table_matches_the_design_document() { assert_eq!(LEVEL_SCREENS.len(), LEVEL_COUNT as usize); // 1-1 and 4-3, the two band caps the design names explicitly. assert_eq!(level_bands(0), Some(30)); assert_eq!(level_bands(11), Some(48)); assert_eq!(level_columns(0), Some(300)); assert_eq!(screens(12), None); assert!(LEVEL_SCREENS.iter().all(|screens| *screens > FIRST_SCREEN)); } #[test] fn bcd_rejects_a_nibble_above_nine() { assert_eq!(bcd(&[0x99]), Some(99)); assert_eq!(bcd(&[0x00]), Some(0)); assert_eq!(bcd(&[0x01, 0x23, 0x45]), Some(12345)); assert_eq!(bcd(&[0x0a]), None); assert_eq!(bcd(&[0xf0]), None); } #[test] fn every_address_is_wram_or_hram_and_never_vram() { let addresses: Vec = UNVERIFIED .iter() .map(|(address, _)| *address) .chain([ hram::hGamePaused, hram::hGameState, hram::hWorldAndLevel, hram::hSuperballMario, hram::hScrollX, hram::hSuperStatus, hram::hWinCount, hram::hStompChain, hram::hStompChainTimer, hram::hLevelIndex, hram::hScreenIndex, hram::hColumnIndex, hram::hCoins, wram::wScore, wram::wLivesEarnedLost, wram::wGameOverWindowEnabled, wram::wGameOverTimerExpired, wram::wInvincibilityTimer, wram::wWinCount, wram::wGameTimer, wram::wLives, wram::wGameTimerExpiringFlag, ]) .collect(); for address in addresses { let wram = (0xc000..=0xdfff).contains(&address); let hram = (0xff80..=0xfffe).contains(&address); assert!(wram || hram, "{address:#06x} is neither WRAM nor HRAM"); } } #[test] fn the_unverified_list_is_sorted_and_covers_the_used_subset() { assert!(UNVERIFIED.windows(2).all(|pair| pair[0].0 < pair[1].0)); for address in UNVERIFIED_AND_USED { assert!( UNVERIFIED.iter().any(|(listed, _)| *listed == address), "{address:#06x} is used but not listed as unverified" ); } } }