diff options
| author | Miquel Sabaté Solà <mssola@mssola.com> | 2026-07-15 22:11:50 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mssola@mssola.com> | 2026-07-15 22:11:50 +0200 |
| commit | 9d790bb4acddae689c5dda667f9d160041bdd4b8 (patch) | |
| tree | 2d719d596a21e63bfe465e2c72f9caababec70c7 | |
| parent | 61b9ce33d54bc643b0ece227101d8bf571fd86e5 (diff) | |
| download | tools.nes-9d790bb4acddae689c5dda667f9d160041bdd4b8.tar.gz tools.nes-9d790bb4acddae689c5dda667f9d160041bdd4b8.zip | |
readrom: implement the -d/--disassemble option
This option can also be coupled with -n/--nasm-directory, and you can
then get a human-readable disassembling of any proc or label you might
be thinking on.
Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
| -rw-r--r-- | Cargo.lock | 3 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | crates/readrom/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/readrom/README.md | 136 | ||||
| -rw-r--r-- | crates/readrom/src/main.rs | 275 | ||||
| -rw-r--r-- | lib/xixanta/src/assembler.rs | 2 | ||||
| -rw-r--r-- | lib/xixanta/src/opcodes.rs | 2180 | ||||
| -rwxr-xr-x | scripts/test-e2e.sh | 26 | ||||
| -rw-r--r-- | tests/expected/readrom/a3b2-address.txt | 1 | ||||
| -rw-r--r-- | tests/expected/readrom/header.txt | 11 | ||||
| -rw-r--r-- | tests/expected/readrom/jetpac-full-nmi.txt | 123 |
11 files changed, 2752 insertions, 13 deletions
@@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "header" @@ -19,6 +19,7 @@ name = "readrom" version = "1.0.0" dependencies = [ "header", + "xixanta", ] [[package]] @@ -22,10 +22,9 @@ will invoke `nasm` with more pedantic features like its address sanitizer. ## `readrom` -The `readrom` program reads a given ROM file and shows all the information that -can be gathered from it. For now this only applies to information on the header, -but in the future we might want to add disassembling user-specified segments, -for example. +`readrom` is an objdump-like utility that reads a given ROM file and shows +information about it. Read more about it in +[./crates/readrom/README.md](./crates/readrom/README.md). ## License diff --git a/crates/readrom/Cargo.toml b/crates/readrom/Cargo.toml index ff2f04b..90f02ea 100644 --- a/crates/readrom/Cargo.toml +++ b/crates/readrom/Cargo.toml @@ -7,3 +7,4 @@ authors.workspace = true [dependencies] header.workspace = true +xixanta.workspace = true diff --git a/crates/readrom/README.md b/crates/readrom/README.md new file mode 100644 index 0000000..5252cb7 --- /dev/null +++ b/crates/readrom/README.md @@ -0,0 +1,136 @@ +`readrom` is an objdump-like utility that reads a given NES/Famicom ROM file and +shows information about it. You can run it by simply passing a ROM file to +it. For example, for the [Jetpac NTSC ROM +file](https://github.com/mssola/jetpac.nes/releases/tag/v1.0): + +``` +$ readrom jetpac.NTSC.nes +# => output +Header: + Kind: NES 2.0 + PRG ROM size: 32768 bytes (32KB) + CHR ROM size: 8192 bytes (8KB) + Mapper: NROM + Mirroring: Horizontal + CPU/PPU timing: NTSC +Vectors: + NMI: 0xa2f8 + Reset: 0xa3c2 + IRQ: 0xa3c1 +``` + +But this utility can do some more complex things. For example, you can tell it +to disassemble a subroutine via the `-d/--disassemble` flag. If the above output +is telling us that NMI code starts at 0xa2f8, we can try: + +``` +$ readrom -d '$a2f8' jetpac.NTSC.nes +# => output +$A2F8: 24 20 bit $20 +``` + +Note that hexadecimal values can also be formatted like this `0xa2fb` or simply +`a2fb`, whatever feels more convenient to use. In any case, they should be +written as a 16-bit address. + +All of that being said, poking for addresses can be tedious. That's why you can +also pass names that you already know. For that, you will need to also pass the +`-n/--nasm-directory`, pointing to the path to your `.nasm/` directory. With +that, if you have assembled the ROM file with `nasm` with the `--write-info` +flag, you will have something like this: + +``` +# On the jetpac.nes repository +$ nasm --write-info -o jetpac.NTSC.nes src/jetpac.s +$ readrom -d nmi -n .nasm/ jetpac.NTSC.nes +# => output +$A2F8: 24 20 bit $20 +$A2FA: 30 01 bmi @save_registers +$A2FC: 40 rti + + @save_registers: +$A2FD: 48 pha +$A2FE: 8A txa +$A2FF: 48 pha +$A300: 98 tya +$A301: 48 pha +$A302: A9 00 lda #$00 +$A304: 8D 03 20 sta $2003 +$A307: A9 02 lda #$02 +$A309: 8D 14 40 sta $4014 +$A30C: 24 28 bit $28 +$A30E: 10 03 bpl @check_pause +$A310: 20 EE 8C jsr nmi_update_scores + + @check_pause: +$A313: 24 38 bit $38 +$A315: 50 03 bvc @increase_rand +$A317: 20 62 A2 jsr nmi_hud_toggle_pause + +# and much more... +``` + +Moreover, if you have built your ROM file with the `--asan` flag from `nasm`, +then you will have the `memory.txt` file inside of `.nasm/`. This tool is able +to pick up this file and decorate some of the values from the previous +output. Hence: + +``` +# On the jetpac.nes repository +$ nasm --write-info --asan -o jetpac.NTSC.nes src/jetpac.s +$ readrom -d nmi -n .nasm/ jetpac.NTSC.nes +# => output +$A2F8: 24 20 bit Globals::zp_flags +$A2FA: 30 01 bmi @save_registers +$A2FC: 40 rti + + @save_registers: +$A2FD: 48 pha +$A2FE: 8A txa +$A2FF: 48 pha +$A300: 98 tya +$A301: 48 pha +$A302: A9 00 lda #$00 +$A304: 8D 03 20 sta OAM::m_address +$A307: A9 02 lda #$02 +$A309: 8D 14 40 sta OAM::m_dma +$A30C: 24 28 bit Globals::zp_extra_flags +$A30E: 10 03 bpl @check_pause +$A310: 20 EE 8C jsr nmi_update_scores + + @check_pause: +$A313: 24 38 bit Driver::zp_flags +$A315: 50 03 bvc @increase_rand +$A317: 20 62 A2 jsr nmi_hud_toggle_pause + +# and much more... +``` + +Now you can see the same output as before, but the first instruction reads as +`bit Globals::zp_flags` instead of `bit $20`, because `readrom` now knows that +the memory address `$20` is associated with this variable. + +Moreover, and as you can tell, by default `readrom` will print things in a +human-readable format. But you can also tell it to just write all the bytes with +the `--raw` flag so you can further manipulate the related bytes with another +tool. For example: + +``` +# On the jetpac.nes repository +$ nasm --write-info -o jetpac.NTSC.nes src/jetpac.s +$ readrom -d nmi -n .nasm/ --raw jetpac.NTSC.nes | hexdump -C +# => output +00000000 24 20 30 01 40 48 8a 48 98 48 a9 00 8d 03 20 a9 |$ 0.@H.H.H.... .| +00000010 02 8d 14 40 24 28 10 03 20 ee 8c 24 38 50 03 20 |...@$(.. ..$8P. | +00000020 62 a2 e6 0a a9 08 25 20 d0 6e 24 2e 10 0b a9 00 |b.....% .n$.....| +00000030 50 02 a9 70 85 04 20 bc a2 a5 50 29 08 f0 34 2c |P..p.. ...P)..4,| +00000040 02 20 a9 28 8d 06 20 a9 4b 8d 06 20 a5 53 18 69 |. .(.. .K.. .S.i| +00000050 10 8d 07 20 24 27 10 15 2c 02 20 a9 28 8d 06 20 |... $'..,. .(.. | +00000060 a9 56 8d 06 20 a5 54 18 69 10 8d 07 20 a5 50 29 |.V.. .T.i... .P)| +00000070 f7 85 50 a5 20 aa 29 20 f0 13 a5 cb d0 06 20 78 |..P. .) ...... x| +00000080 89 4c 7f a3 20 59 91 a5 20 29 df 85 20 8a 29 01 |.L.. Y.. ).. .).| +00000090 d0 06 a5 30 f0 02 c6 30 24 20 50 13 a9 bf 25 20 |...0...0$ P...% | +000000a0 85 20 2c 02 20 a5 81 8d 01 20 a5 80 8d 00 20 2c |. ,. .... .... ,| +000000b0 02 20 a9 00 8d 05 20 8d 05 20 20 1d 8b a9 7f 25 |. .... .. ....%| +000000c0 20 85 20 68 a8 68 aa 68 40 | . h.h.h@| +``` diff --git a/crates/readrom/src/main.rs b/crates/readrom/src/main.rs index 3c87417..e2f4343 100644 --- a/crates/readrom/src/main.rs +++ b/crates/readrom/src/main.rs @@ -1,6 +1,9 @@ use header::{Header, Kind}; +use std::collections::HashMap; use std::fs::File; -use std::io::{ErrorKind, Read}; +use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Read, Write}; +use std::path::PathBuf; +use xixanta::opcodes::OPCODES; /// Version for this program. const VERSION: &str = "0.1.0"; @@ -9,15 +12,21 @@ const VERSION: &str = "0.1.0"; struct Args { file: String, header: bool, + disassemble: Option<String>, + nasm: Option<String>, + raw: bool, } fn print_help() { println!("Display information about NES/Famicom ROM files.\n"); println!("usage: readrom [OPTIONS] <FILE>\n"); println!("Options:"); - println!(" -h, --help\t\tPrint this message."); - println!(" -H, --header\t\tJust print the ROM header and quit."); - println!(" -v, --version\t\tPrint the version of this program."); + println!(" -d, --disassemble <ADDRESS>\tDisassemble starting from the given ADDRESS."); + println!(" -h, --help\t\t\tPrint this message."); + println!(" -H, --header\t\t\tJust print the ROM header and quit."); + println!(" -n, --nasm-directory <PATH>\tPath to the .nasm/ directory."); + println!(" -r, --raw\t\t\tPrint bytes with no formatting at all when disassembling."); + println!(" -v, --version\t\t\tPrint the version of this program."); std::process::exit(0); } @@ -30,8 +39,14 @@ fn parse_arguments() -> Args { // Skip command name. args.next(); - for arg in args { + while let Some(arg) = args.next() { match arg.as_str() { + "-d" | "--disassemble" => match args.next() { + Some(a) => res.disassemble = Some(a), + None => die( + "you need to specify an address for the '-d/--disassemble' flag".to_string(), + ), + }, "-h" | "--help" => print_help(), "-H" | "--header" => { if res.header { @@ -39,6 +54,11 @@ fn parse_arguments() -> Args { } res.header = true; } + "-n" | "--nasm" => match args.next() { + Some(a) => res.nasm = Some(a), + None => die("you need to specify a file for the '-n/--nasm' flag".to_string()), + }, + "-r" | "--raw" => res.raw = true, "-v" | "--version" => { println!("readrom {VERSION}"); std::process::exit(0); @@ -140,6 +160,240 @@ fn die(message: String) { std::process::exit(1); } +// Print a block of code from the given open ROM 'file'. The range is indicated +// by 'start' and an optional 'end'. If 'end' is None, then the block of code +// will only span a single instruction. Moreover, the 'memories' and the +// 'addresses' maps can help assist the printing with the information taken from +// the .nasm/memory.txt and .nasm/addresses.txt files respectively. Finally, set +// 'raw' to true if you want all bytes to be printed directly into the stdout, +// otherwise a human-readable format will be used. +fn print_range( + mut file: File, + start: usize, + end: Option<usize>, + memories: HashMap<usize, String>, + addresses: HashMap<usize, String>, + raw: bool, + filter: Option<&str>, +) -> Result<(), String> { + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).map_err(|e| e.to_string())?; + + // Fetch the bytes to be printed. + + // NOTE: minus 0x8000 to account for non-ROM address, plus 0x10 to skip the + // header from the file. + let range_start = start + .checked_sub(0x8000) + .map(|val| val + 0x10) + .ok_or_else(|| format!("bad start address {:#x}", start))?; + + // The 'end' of the range depends on whether the user is just printing a + // single instruction or it's really trying to print a proper range. + let res = match end { + Some(e) => { + let range_end = e + .checked_sub(0x8000) + .map(|val| val + 0x10) + .ok_or_else(|| format!("bad end address {:#x}", e))?; + bytes.get(range_start..range_end).unwrap_or(&[]) + } + // If it's just one instruction, take the opcode byte and some more to + // account for the maximum size of an instruction on this platform. + None => bytes.get(range_start..range_start + 3).unwrap_or(&[]), + }; + + // Printing raw: blindessly spit bytes to stdout. + if raw { + let mut writer = BufWriter::new(std::io::stdout()); + writer + .write_all(&res[..res.len()]) + .map_err(|_| "cannot write to the stdout".to_string())?; + return Ok(()); + } + + // Print into a more human-readable shape. + + let mut iter = res.iter(); + let mut current_address = start; + while let Some(byte) = iter.next() { + // Given the opcode, fetch the instruction object for it, and how much + // the address should be advanced after printing the instruction. + let (instr, size, formatted) = match OPCODES.get(byte) { + Some(tpl) => { + let mut ins = tpl.clone(); + let mut formatted = format!("{:02X}\t", byte); + + match ins.size { + 2 => { + ins.bytes[0] = *iter.next().unwrap_or(&0); + formatted = format!("{:02X} {:02X}\t", byte, ins.bytes[0]); + } + 3 => { + ins.bytes[0] = *iter.next().unwrap_or(&0); + ins.bytes[1] = *iter.next().unwrap_or(&0); + formatted = + format!("{:02X} {:02X} {:02X}", byte, ins.bytes[0], ins.bytes[1]); + } + _ => {} + } + + ( + ins.to_human(current_address, filter, &memories, &addresses), + ins.size, + formatted, + ) + } + None => (byte.to_string(), 1, byte.to_string()), + }; + + // Do we actually know of a label which points to the current address? + // If so, show it now. + if let Some(address_name) = addresses.get(¤t_address) + && current_address != start + { + println!( + "\n {}:", + address_name.split("::").last().unwrap_or(address_name) + ); + } + + // And print our awesome line :) + println!("${:4X}:\t{}\t{} ", current_address, formatted, instr); + current_address += size as usize; + + if end.is_none() { + break; + } + } + + // Sometimes there is a label marking the end of the code, which is set + // after the last instruction. Show these labels too as some branch + // instructions can use it. + if let Some(address_name) = addresses.get(¤t_address) { + println!( + "\n {}:", + address_name.split("::").last().unwrap_or(address_name) + ); + } + + Ok(()) +} + +fn parse_hex_value(address: &str) -> Option<usize> { + match address.len() { + // Simple 'a2fb' format. + 4 => { + if let Ok(val) = usize::from_str_radix(address, 16) { + return Some(val); + } + } + // nasm's '$a2fb' format. + 5 => { + if let Some(addr) = address.get(1..) + && let Ok(val) = usize::from_str_radix(addr, 16) + { + return Some(val); + } + } + // Standard '0xa2fb' format. + 6 => { + if let Some(addr) = address.get(2..) + && let Ok(val) = usize::from_str_radix(addr, 16) + { + return Some(val); + } + } + _ => {} + } + + None +} + +fn do_disassemble( + input: File, + address: &str, + nasm_path: &Option<String>, + raw: bool, +) -> Result<(), String> { + let mut start = None; + let mut end = None; + let mut is_nasm_path = true; + let mut addresses: HashMap<usize, String> = HashMap::default(); + let mut memories: HashMap<usize, String> = HashMap::default(); + + // Fill up the 'addresses' and the 'memories' maps. + if let Some(path) = nasm_path { + if let Ok(file) = File::open(PathBuf::from(path).join("addresses.txt")) { + let reader = BufReader::new(file); + for line in reader.lines() { + let line = line.map_err(|e| e.to_string())?; + let columns: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); + if columns.len() != 3 { + return Err("badly formatted address file".to_string()); + } + + let parsed_start = usize::from_str_radix(columns[1], 16) + .map_err(|_| format!("invalid hex value: '{}'", columns[1]))?; + addresses.insert(parsed_start, columns[0].to_string()); + + if columns[0] == address { + start = Some(parsed_start); + end = Some( + usize::from_str_radix(columns[2], 16) + .map_err(|_| format!("invalid hex value: '{}'", columns[2]))?, + ); + } + } + } + + // If the memory.txt file is available, fill up the 'memories' hash. + if let Ok(file) = File::open(PathBuf::from(path).join("memory.txt")) { + let reader = BufReader::new(file); + for line in reader.lines() { + let line = line.map_err(|e| e.to_string())?; + if line.is_empty() || line.starts_with("---") { + break; + } + + let (left, right) = line.split_once(':').unwrap(); + let start = match left.trim().split_once('-') { + Some((start, _)) => usize::from_str_radix(start.get(1..).unwrap(), 16).unwrap(), + None => usize::from_str_radix(left.get(1..).unwrap(), 16).unwrap(), + }; + memories.insert(start, right.trim().to_string()); + } + } + } else { + is_nasm_path = false; + } + + // If this is just a numeric value, take it as is. + if let Some(start) = parse_hex_value(address) { + return print_range(input, start, None, memories, addresses, raw, None); + } + + // Otherwise, print the full range if possible. + if !is_nasm_path { + Err("you need to use the '-n/--nasm-directory' on disassembly".to_string()) + } else if addresses.is_empty() { + Err("failed to open the .nasm/addresses.txt file".to_string()) + } else { + match start { + Some(s) => print_range( + input, + s, + Some(end.unwrap()), + memories, + addresses, + raw, + Some(format!("{address}::").as_str()), + ), + None => Err(format!("could not find address '{address}'")), + } + } +} + fn main() { let args = parse_arguments(); @@ -148,7 +402,16 @@ fn main() { return; }; - // Header. + // Check whether the user wanted to disassemble something. + + if let Some(address) = args.disassemble { + if let Err(e) = do_disassemble(input, &address, &args.nasm, args.raw) { + die(e); + } + std::process::exit(0); + } + + // Nope. Then let's just print information about it. First the header. let mut buf = vec![0u8; 0x10]; if let Err(e) = input.read_exact(&mut buf) { diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs index 959840d..91f1e14 100644 --- a/lib/xixanta/src/assembler.rs +++ b/lib/xixanta/src/assembler.rs @@ -1429,7 +1429,7 @@ impl<'a> Assembler<'a> { // Using unwrap() is safe as it only returns an error on // badly formatted addresses. This will not happen at // this point. - let resolved = self.context.resolve_label(&self.mappings, &bundle).unwrap(); + let resolved = self.context.resolve_label(&self.mappings, bundle).unwrap(); let val = resolved.bundle.value() as usize; let end = match self.address_ends.get(&full_name) { diff --git a/lib/xixanta/src/opcodes.rs b/lib/xixanta/src/opcodes.rs index bc4f1d4..d5bb97a 100644 --- a/lib/xixanta/src/opcodes.rs +++ b/lib/xixanta/src/opcodes.rs @@ -37,6 +37,69 @@ impl fmt::Display for AddressingMode { } } +/// All the regular instructions that the NES/Famicom can actually run. +#[derive(Clone, Debug)] +pub enum InstructionIdentifier { + Adc, + And, + Asl, + Bcc, + Bcs, + Bit, + Beq, + Bmi, + Bne, + Bpl, + Brk, + Bvc, + Bvs, + Clc, + Cld, + Cli, + Clv, + Cmp, + Cpx, + Cpy, + Dec, + Dex, + Dey, + Eor, + Inc, + Inx, + Iny, + Jmp, + Jsr, + Lda, + Ldx, + Ldy, + Lsr, + Nop, + Ora, + Pha, + Php, + Pla, + Plp, + Rti, + Rts, + Rol, + Ror, + Sbc, + Sec, + Sed, + Sei, + Sta, + Stx, + Sty, + Tax, + Tay, + Tsx, + Txa, + Txs, + Tya, +} + +/// An entry to the 'INSTRUCTIONS' map, which holds some values relevant for +/// understanding the associated instruction. #[derive(Debug)] pub struct ShortEntry { pub cycles: u8, @@ -45,6 +108,202 @@ pub struct ShortEntry { pub affected_on_page: bool, } +/// An Instruction from the Ricoh 2A03 chip. This is mainly used in the +/// 'OPCODES' map, and it's an expansion of the 'ShortEntry' struct. +#[derive(Clone, Debug)] +pub struct Instruction { + pub identifier: InstructionIdentifier, + pub addressing_mode: AddressingMode, + pub cycles: u8, + pub opcode: u8, + pub size: u8, + pub affected_on_page: bool, + pub bytes: [u8; 2], +} + +impl Instruction { + /// Returns the value that can be obtained by evaluating the 'bytes' member + /// as a little endian decimal. + pub fn value(&self) -> usize { + match self.size { + 2 => self.bytes[0] as usize, + 3 => self.bytes[0] as usize + ((self.bytes[1] as usize) << 8), + _ => 0, + } + } + + /// Returns true if this is a branching instruction. + pub fn is_branch(&self) -> bool { + matches!( + self.identifier, + InstructionIdentifier::Bcc + | InstructionIdentifier::Bcs + | InstructionIdentifier::Beq + | InstructionIdentifier::Bmi + | InstructionIdentifier::Bne + | InstructionIdentifier::Bpl + | InstructionIdentifier::Bvc + | InstructionIdentifier::Bvs + ) + } + + /// Given the 'current' target address, and the helper 'memories' and + /// 'addresses' maps, attempt to return a String representation of this + /// instruction which is as human-readable as possible. This involves trying + /// to resolve addresses/values from either maps. + pub fn to_human( + &self, + current: usize, + filter: Option<&str>, + memories: &HashMap<usize, String>, + addresses: &HashMap<usize, String>, + ) -> String { + let id = match self.identifier { + InstructionIdentifier::Adc => "adc", + InstructionIdentifier::And => "and", + InstructionIdentifier::Asl => "asl", + InstructionIdentifier::Bcc => "bcc", + InstructionIdentifier::Bcs => "bcs", + InstructionIdentifier::Bit => "bit", + InstructionIdentifier::Beq => "beq", + InstructionIdentifier::Bne => "bne", + InstructionIdentifier::Bpl => "bpl", + InstructionIdentifier::Bmi => "bmi", + InstructionIdentifier::Brk => "brk", + InstructionIdentifier::Bvc => "bvc", + InstructionIdentifier::Bvs => "bvs", + InstructionIdentifier::Clc => "clc", + InstructionIdentifier::Cld => "cld", + InstructionIdentifier::Cli => "cli", + InstructionIdentifier::Clv => "clv", + InstructionIdentifier::Cmp => "cmp", + InstructionIdentifier::Cpx => "cpx", + InstructionIdentifier::Cpy => "cpy", + InstructionIdentifier::Dec => "dec", + InstructionIdentifier::Dex => "dex", + InstructionIdentifier::Dey => "dey", + InstructionIdentifier::Eor => "eor", + InstructionIdentifier::Inc => "inc", + InstructionIdentifier::Inx => "inx", + InstructionIdentifier::Iny => "iny", + InstructionIdentifier::Jmp => "jmp", + InstructionIdentifier::Jsr => "jsr", + InstructionIdentifier::Lda => "lda", + InstructionIdentifier::Ldx => "ldx", + InstructionIdentifier::Ldy => "ldy", + InstructionIdentifier::Lsr => "lsr", + InstructionIdentifier::Nop => "nop", + InstructionIdentifier::Ora => "ora", + InstructionIdentifier::Pha => "pha", + InstructionIdentifier::Php => "php", + InstructionIdentifier::Pla => "pla", + InstructionIdentifier::Plp => "plp", + InstructionIdentifier::Rol => "rol", + InstructionIdentifier::Ror => "ror", + InstructionIdentifier::Rti => "rti", + InstructionIdentifier::Rts => "rts", + InstructionIdentifier::Sbc => "sbc", + InstructionIdentifier::Sec => "sec", + InstructionIdentifier::Sed => "sed", + InstructionIdentifier::Sei => "sei", + InstructionIdentifier::Sta => "sta", + InstructionIdentifier::Stx => "stx", + InstructionIdentifier::Sty => "sty", + InstructionIdentifier::Tax => "tax", + InstructionIdentifier::Tay => "tay", + InstructionIdentifier::Tsx => "tsx", + InstructionIdentifier::Txa => "txa", + InstructionIdentifier::Txs => "txs", + InstructionIdentifier::Tya => "tya", + }; + + // On most addressing modes, if the current value is actually found on + // the map of known reserved memory addresses, then we can blindly try + // to fetch it first. For relative, zeropage and absolute things are + // more nuanced, so we delay this check in these scenarios. + if matches!( + self.addressing_mode, + AddressingMode::IndexedX + | AddressingMode::IndexedY + | AddressingMode::ZeropageIndexedX + | AddressingMode::ZeropageIndexedY + | AddressingMode::Indirect + | AddressingMode::IndirectX + | AddressingMode::IndirectY + ) { + let val = self.value(); + if let Some(name) = memories.get(&val) { + return format!("{id} {name}"); + } + } + + match self.addressing_mode { + AddressingMode::Immediate => { + let val = self.value(); + if val > u8::MAX as usize { + format!("{id} #${:04X}", val) + } else { + format!("{id} #${:02X}", val) + } + } + AddressingMode::RelativeOrZeropage | AddressingMode::Absolute => { + let val = self.value(); + + // If this is a branch instruction, then we compute the final + // address via the two's complement encoding of the given byte + // plus the current address + 2 bytes to account for the branch + // instruction itself. If all of that gives us a known address, + // then use it, otherwise treat it as a relative/zeropage + // nomenclature. + if self.is_branch() { + let target = current as isize + (val as i8) as isize + 2; + + if let Some(name) = addresses.get(&(target as usize)) { + let clean_name = match filter { + Some(f) => name.strip_prefix(f).unwrap_or(name), + None => name, + }; + return format!("{id} {clean_name}"); + } + return format!("{id} ${:02X}", val); + } + + // There's no relative address to compute, so we check whether + // this is a known absolute address. + if let Some(name) = addresses.get(&val) { + // let last_name = name.split("::").last().unwrap_or(name); + // return format!("{id} {name}"); + let clean_name = match filter { + Some(f) => name.strip_prefix(f).unwrap_or(name), + None => name, + }; + return format!("{id} {clean_name}"); + } + + // Is it on the map ok known memory addresses? + let val = self.value(); + if let Some(name) = memories.get(&val) { + return format!("{id} {name}"); + } + + if val > u8::MAX as usize { + format!("{id} ${:04X}", val) + } else { + format!("{id} ${:02X}", val) + } + } + AddressingMode::IndexedX => format!("{} ${:04X}, x", id, self.value()), + AddressingMode::IndexedY => format!("{} ${:04X}, y", id, self.value()), + AddressingMode::ZeropageIndexedX => format!("{} ${:02X}, x", id, self.value()), + AddressingMode::ZeropageIndexedY => format!("{} ${:02X}, y", id, self.value()), + AddressingMode::Indirect => format!("{} (${:02X})", id, self.value()), + AddressingMode::IndirectX => format!("{} (${:02X}, x)", id, self.value()), + AddressingMode::IndirectY => format!("{} (${:02X}), y", id, self.value()), + AddressingMode::Implied => id.to_string(), + } + } +} + /// The representation of a Control statement/expression. #[derive(Debug)] pub struct Control { @@ -2062,3 +2321,1924 @@ pub static CONTROL_FUNCTIONS: LazyLock<HashMap<String, Control>> = LazyLock::new functions }); + +/// For a given byte opcode, return the Instruction associated with it. +pub static OPCODES: LazyLock<HashMap<u8, Instruction>> = LazyLock::new(|| { + let mut ops = HashMap::new(); + + // adc + ops.insert( + 0x69, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0x69, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x65, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x65, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x75, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x75, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x6D, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x6D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x7D, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0x7D, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x79, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0x79, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x61, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0x61, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x71, + Instruction { + identifier: InstructionIdentifier::Adc, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0x71, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // and + ops.insert( + 0x29, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0x29, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x25, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x25, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x35, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x35, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x2D, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x2D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x3D, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0x3D, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x39, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0x39, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x21, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0x21, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x31, + Instruction { + identifier: InstructionIdentifier::And, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0x31, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // asl + ops.insert( + 0x0A, + Instruction { + identifier: InstructionIdentifier::Asl, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x0A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x06, + Instruction { + identifier: InstructionIdentifier::Asl, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0x06, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x16, + Instruction { + identifier: InstructionIdentifier::Asl, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0x16, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x0E, + Instruction { + identifier: InstructionIdentifier::Asl, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0x0E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + ops.insert( + 0x1E, + Instruction { + identifier: InstructionIdentifier::Asl, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0x1E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // bit + ops.insert( + 0x24, + Instruction { + identifier: InstructionIdentifier::Bit, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x24, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x2C, + Instruction { + identifier: InstructionIdentifier::Bit, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x2C, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // bcc + ops.insert( + 0x90, + Instruction { + identifier: InstructionIdentifier::Bcc, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0x90, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // bcs + ops.insert( + 0xB0, + Instruction { + identifier: InstructionIdentifier::Bcs, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0xB0, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // beq + ops.insert( + 0xF0, + Instruction { + identifier: InstructionIdentifier::Beq, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0xF0, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // bne + ops.insert( + 0xD0, + Instruction { + identifier: InstructionIdentifier::Bne, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0xD0, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // bpl + ops.insert( + 0x10, + Instruction { + identifier: InstructionIdentifier::Bpl, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0x10, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // bmi + ops.insert( + 0x30, + Instruction { + identifier: InstructionIdentifier::Bmi, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0x30, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // brk + ops.insert( + 0x00, + Instruction { + identifier: InstructionIdentifier::Brk, + addressing_mode: AddressingMode::Implied, + cycles: 7, + size: 1, + opcode: 0x00, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // bvs + ops.insert( + 0x70, + Instruction { + identifier: InstructionIdentifier::Bvs, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0x70, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // bvc + ops.insert( + 0x50, + Instruction { + identifier: InstructionIdentifier::Bvc, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 2, + opcode: 0x50, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // clc + ops.insert( + 0x18, + Instruction { + identifier: InstructionIdentifier::Clc, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x18, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // cld + ops.insert( + 0xD8, + Instruction { + identifier: InstructionIdentifier::Cld, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xD8, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // cli + ops.insert( + 0x58, + Instruction { + identifier: InstructionIdentifier::Cli, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x58, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // clv + ops.insert( + 0xB8, + Instruction { + identifier: InstructionIdentifier::Clv, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xB8, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // cmp + ops.insert( + 0xC9, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xC9, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xC5, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xC5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xD5, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0xD5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xCD, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xCD, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xDD, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0xDD, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xD9, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0xD9, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0xC1, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0xC1, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xD1, + Instruction { + identifier: InstructionIdentifier::Cmp, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0xD1, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // cpx + ops.insert( + 0xE0, + Instruction { + identifier: InstructionIdentifier::Cpx, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xE0, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xE4, + Instruction { + identifier: InstructionIdentifier::Cpx, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xE4, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xEC, + Instruction { + identifier: InstructionIdentifier::Cpx, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xEC, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // cpy + ops.insert( + 0xC0, + Instruction { + identifier: InstructionIdentifier::Cpy, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xC0, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xC4, + Instruction { + identifier: InstructionIdentifier::Cpy, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xC4, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xCC, + Instruction { + identifier: InstructionIdentifier::Cpy, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xCC, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // dec + ops.insert( + 0xC6, + Instruction { + identifier: InstructionIdentifier::Dec, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0xC6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xD6, + Instruction { + identifier: InstructionIdentifier::Dec, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0xD6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xCE, + Instruction { + identifier: InstructionIdentifier::Dec, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0xCE, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xDE, + Instruction { + identifier: InstructionIdentifier::Dec, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0xDE, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // dex + ops.insert( + 0xCA, + Instruction { + identifier: InstructionIdentifier::Dex, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xCA, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // dey + ops.insert( + 0x88, + Instruction { + identifier: InstructionIdentifier::Dey, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x88, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // eor + ops.insert( + 0x49, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0x49, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x45, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x45, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x55, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x55, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x4D, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x4D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x5D, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0x5D, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x59, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0x59, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x41, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0x41, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x51, + Instruction { + identifier: InstructionIdentifier::Eor, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0x51, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // inc + ops.insert( + 0xE6, + Instruction { + identifier: InstructionIdentifier::Inc, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0xE6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xF6, + Instruction { + identifier: InstructionIdentifier::Inc, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0xF6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xEE, + Instruction { + identifier: InstructionIdentifier::Inc, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0xEE, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xFE, + Instruction { + identifier: InstructionIdentifier::Inc, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0xFE, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // inx + ops.insert( + 0xE8, + Instruction { + identifier: InstructionIdentifier::Inx, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xE8, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // iny + ops.insert( + 0xC8, + Instruction { + identifier: InstructionIdentifier::Iny, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xC8, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // jmp + ops.insert( + 0x4C, + Instruction { + identifier: InstructionIdentifier::Jmp, + addressing_mode: AddressingMode::Absolute, + cycles: 3, + opcode: 0x4C, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // jsr + ops.insert( + 0x20, + Instruction { + identifier: InstructionIdentifier::Jsr, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0x20, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // lda + ops.insert( + 0xA9, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xA9, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xA5, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xA5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xB5, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0xB5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xAD, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xAD, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xBD, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0xBD, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0xB9, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0xB9, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0xA1, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0xA1, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xB1, + Instruction { + identifier: InstructionIdentifier::Lda, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0xB1, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // ldx + ops.insert( + 0xA2, + Instruction { + identifier: InstructionIdentifier::Ldx, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xA2, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xA6, + Instruction { + identifier: InstructionIdentifier::Ldx, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xA6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xB6, + Instruction { + identifier: InstructionIdentifier::Ldx, + addressing_mode: AddressingMode::ZeropageIndexedY, + cycles: 4, + opcode: 0xB6, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xAE, + Instruction { + identifier: InstructionIdentifier::Ldx, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xAE, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xBE, + Instruction { + identifier: InstructionIdentifier::Ldx, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0xBE, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // ldy + ops.insert( + 0xA0, + Instruction { + identifier: InstructionIdentifier::Ldy, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xA0, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xA4, + Instruction { + identifier: InstructionIdentifier::Ldy, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xA4, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xB4, + Instruction { + identifier: InstructionIdentifier::Ldy, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0xB4, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xAC, + Instruction { + identifier: InstructionIdentifier::Ldy, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xAC, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xBC, + Instruction { + identifier: InstructionIdentifier::Ldy, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0xBC, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // lsr + ops.insert( + 0x4A, + Instruction { + identifier: InstructionIdentifier::Lsr, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x4A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x46, + Instruction { + identifier: InstructionIdentifier::Lsr, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0x46, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x56, + Instruction { + identifier: InstructionIdentifier::Lsr, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0x56, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x4E, + Instruction { + identifier: InstructionIdentifier::Lsr, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0x4E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + ops.insert( + 0x5E, + Instruction { + identifier: InstructionIdentifier::Lsr, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0x5E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // nop + ops.insert( + 0xEA, + Instruction { + identifier: InstructionIdentifier::Nop, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xEA, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // ora + ops.insert( + 0x09, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0x09, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x05, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x05, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x15, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x15, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x0D, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x0D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x1D, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0x1D, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x19, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0x19, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0x01, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0x01, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x11, + Instruction { + identifier: InstructionIdentifier::Ora, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0x11, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // pha + ops.insert( + 0x48, + Instruction { + identifier: InstructionIdentifier::Pha, + addressing_mode: AddressingMode::Implied, + cycles: 3, + size: 1, + opcode: 0x48, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // php + ops.insert( + 0x08, + Instruction { + identifier: InstructionIdentifier::Php, + addressing_mode: AddressingMode::Implied, + cycles: 3, + size: 1, + opcode: 0x08, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // pla + ops.insert( + 0x68, + Instruction { + identifier: InstructionIdentifier::Pla, + addressing_mode: AddressingMode::Implied, + cycles: 3, + size: 1, + opcode: 0x68, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // plp + ops.insert( + 0x28, + Instruction { + identifier: InstructionIdentifier::Plp, + addressing_mode: AddressingMode::Implied, + cycles: 3, + size: 1, + opcode: 0x28, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // rol + ops.insert( + 0x2A, + Instruction { + identifier: InstructionIdentifier::Rol, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x2A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x26, + Instruction { + identifier: InstructionIdentifier::Rol, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0x26, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x36, + Instruction { + identifier: InstructionIdentifier::Rol, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0x36, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x2E, + Instruction { + identifier: InstructionIdentifier::Rol, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0x2E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x3E, + Instruction { + identifier: InstructionIdentifier::Rol, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0x3E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // ror + ops.insert( + 0x6A, + Instruction { + identifier: InstructionIdentifier::Ror, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x6A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x66, + Instruction { + identifier: InstructionIdentifier::Ror, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 5, + opcode: 0x66, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x76, + Instruction { + identifier: InstructionIdentifier::Ror, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 6, + opcode: 0x76, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x6E, + Instruction { + identifier: InstructionIdentifier::Ror, + addressing_mode: AddressingMode::Absolute, + cycles: 6, + opcode: 0x6E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x7E, + Instruction { + identifier: InstructionIdentifier::Ror, + addressing_mode: AddressingMode::IndexedX, + cycles: 7, + opcode: 0x7E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // rti + ops.insert( + 0x40, + Instruction { + identifier: InstructionIdentifier::Rti, + addressing_mode: AddressingMode::Implied, + cycles: 6, + opcode: 0x40, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // rts + ops.insert( + 0x60, + Instruction { + identifier: InstructionIdentifier::Rts, + addressing_mode: AddressingMode::Implied, + cycles: 6, + opcode: 0x60, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // sbc + ops.insert( + 0xE9, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::Immediate, + cycles: 2, + opcode: 0xE9, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xE5, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0xE5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xF5, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0xF5, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xED, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0xED, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xFD, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::IndexedX, + cycles: 4, + opcode: 0xFD, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0xF9, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::IndexedY, + cycles: 4, + opcode: 0xF9, + size: 3, + affected_on_page: true, + bytes: [0, 0], + }, + ); + ops.insert( + 0xE1, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0xE1, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0xF1, + Instruction { + identifier: InstructionIdentifier::Sbc, + addressing_mode: AddressingMode::IndirectY, + cycles: 5, + opcode: 0xF1, + size: 2, + affected_on_page: true, + bytes: [0, 0], + }, + ); + + // sec + ops.insert( + 0x38, + Instruction { + identifier: InstructionIdentifier::Sec, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x38, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // sed + ops.insert( + 0x38, + Instruction { + identifier: InstructionIdentifier::Sed, + addressing_mode: AddressingMode::Implied, + cycles: 2, + size: 1, + opcode: 0x38, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // sei + ops.insert( + 0x78, + Instruction { + identifier: InstructionIdentifier::Sei, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x78, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // sta + ops.insert( + 0x85, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x85, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x95, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x95, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x8D, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x8D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x9D, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::IndexedX, + cycles: 5, + opcode: 0x9D, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x99, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::IndexedY, + cycles: 5, + opcode: 0x99, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x81, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::IndirectX, + cycles: 6, + opcode: 0x81, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x91, + Instruction { + identifier: InstructionIdentifier::Sta, + addressing_mode: AddressingMode::IndirectY, + cycles: 6, + opcode: 0x91, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // stx + ops.insert( + 0x86, + Instruction { + identifier: InstructionIdentifier::Stx, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x86, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x96, + Instruction { + identifier: InstructionIdentifier::Stx, + addressing_mode: AddressingMode::ZeropageIndexedY, + cycles: 4, + opcode: 0x96, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x8E, + Instruction { + identifier: InstructionIdentifier::Stx, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x8E, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // sty + ops.insert( + 0x84, + Instruction { + identifier: InstructionIdentifier::Sty, + addressing_mode: AddressingMode::RelativeOrZeropage, + cycles: 3, + opcode: 0x84, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x94, + Instruction { + identifier: InstructionIdentifier::Sty, + addressing_mode: AddressingMode::ZeropageIndexedX, + cycles: 4, + opcode: 0x94, + size: 2, + affected_on_page: false, + bytes: [0, 0], + }, + ); + ops.insert( + 0x8C, + Instruction { + identifier: InstructionIdentifier::Sty, + addressing_mode: AddressingMode::Absolute, + cycles: 4, + opcode: 0x8C, + size: 3, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // tax + ops.insert( + 0xAA, + Instruction { + identifier: InstructionIdentifier::Tax, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xAA, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // tay + ops.insert( + 0xA8, + Instruction { + identifier: InstructionIdentifier::Tay, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xA8, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // tsx + ops.insert( + 0xBA, + Instruction { + identifier: InstructionIdentifier::Tsx, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0xBA, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // txa + ops.insert( + 0x8A, + Instruction { + identifier: InstructionIdentifier::Txa, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x8A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // txs + ops.insert( + 0x9A, + Instruction { + identifier: InstructionIdentifier::Txs, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x9A, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + // tya + ops.insert( + 0x98, + Instruction { + identifier: InstructionIdentifier::Tya, + addressing_mode: AddressingMode::Implied, + cycles: 2, + opcode: 0x98, + size: 1, + affected_on_page: false, + bytes: [0, 0], + }, + ); + + ops +}); diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 2a72d38..c32d427 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -236,13 +236,37 @@ echo "LEVEL = 0" >> tests/jetpac.nes/config/generated.s # NOTE: we use '--stats' as otherwise the 'segments.txt' file would be removed # from the jetpac.nes repository, and the submodule would then be marked with # "-dirty". -$AS65 --no-errors --allow-unused --strict --stats -b ./target/debug/nasm -C tests/jetpac.nes/config/nrom.cfg -o /dev/null tests/jetpac.nes/src/jetpac.s 1>/dev/null +$AS65 --no-errors --allow-unused --strict --stats -b ./target/debug/nasm -C tests/jetpac.nes/config/nrom.cfg -o tests/out/jetpac.NTSC.nes tests/jetpac.nes/src/jetpac.s 1>/dev/null # Remove the generated.s file to avoid git from thinking we have modified the # project in any meaningful way. rm -f tests/jetpac.nes/config/generated.s ## +# readrom + +echo "test: readrom: header (jetpac.nes)" +./target/debug/readrom tests/out/jetpac.NTSC.nes > tests/out/jetpac.header.txt +diff tests/out/jetpac.header.txt tests/expected/readrom/header.txt +exit_code=$((exit_code + $?)) + +echo "test: readrom: hexadecimal address (jetpac.nes)" +./target/debug/readrom -d '$a3b2' -n tests/jetpac.nes/.nasm/ tests/out/jetpac.NTSC.nes > tests/out/jetpac-a3b2-address.txt +diff tests/out/jetpac-a3b2-address.txt tests/expected/readrom/a3b2-address.txt +exit_code=$((exit_code + $?)) + +echo "test: readrom: full NMI disassembling (jetpac.nes)" +./target/debug/readrom -d nmi -n tests/jetpac.nes/.nasm/ tests/out/jetpac.NTSC.nes > tests/out/jetpac-full-nmi.txt +diff tests/out/jetpac-full-nmi.txt tests/expected/readrom/jetpac-full-nmi.txt +exit_code=$((exit_code + $?)) + +# Remove the dangling jetpac.nes ROM file. +rm tests/out/jetpac.NTSC.nes + +# This file wasn't available originally on jetpac.nes +rm -f tests/jetpac.nes/.nasm/addresses.txt + +## # Done! exit $exit_code diff --git a/tests/expected/readrom/a3b2-address.txt b/tests/expected/readrom/a3b2-address.txt new file mode 100644 index 0000000..fd86b61 --- /dev/null +++ b/tests/expected/readrom/a3b2-address.txt @@ -0,0 +1 @@ +$A3B2: 20 1D 8B jsr Sound::tick diff --git a/tests/expected/readrom/header.txt b/tests/expected/readrom/header.txt new file mode 100644 index 0000000..a2d4a81 --- /dev/null +++ b/tests/expected/readrom/header.txt @@ -0,0 +1,11 @@ +Header: + Kind: NES 2.0 + PRG ROM size: 32768 bytes (32KB) + CHR ROM size: 8192 bytes (8KB) + Mapper: NROM + Mirroring: Horizontal + CPU/PPU timing: NTSC +Vectors: + NMI: 0xa2f8 + Reset: 0xa3c2 + IRQ: 0xa3c1 diff --git a/tests/expected/readrom/jetpac-full-nmi.txt b/tests/expected/readrom/jetpac-full-nmi.txt new file mode 100644 index 0000000..45c9bce --- /dev/null +++ b/tests/expected/readrom/jetpac-full-nmi.txt @@ -0,0 +1,123 @@ +$A2F8: 24 20 bit Globals::zp_flags +$A2FA: 30 01 bmi @save_registers +$A2FC: 40 rti + + @save_registers: +$A2FD: 48 pha +$A2FE: 8A txa +$A2FF: 48 pha +$A300: 98 tya +$A301: 48 pha +$A302: A9 00 lda #$00 +$A304: 8D 03 20 sta OAM::m_address +$A307: A9 02 lda #$02 +$A309: 8D 14 40 sta OAM::m_dma +$A30C: 24 28 bit Globals::zp_extra_flags +$A30E: 10 03 bpl @check_pause +$A310: 20 EE 8C jsr Score::nmi_update_scores + + @check_pause: +$A313: 24 38 bit Driver::zp_flags +$A315: 50 03 bvc @increase_rand +$A317: 20 62 A2 jsr Driver::nmi_hud_toggle_pause + + @increase_rand: +$A31A: E6 0A inc Prng::zp_rand +$A31C: A9 08 lda #$08 +$A31E: 25 20 and Globals::zp_flags +$A320: D0 6E bne @ppu_registers +$A322: 24 2E bit Driver::zp_blink_status +$A324: 10 0B bpl @update_lifes +$A326: A9 00 lda #$00 +$A328: 50 02 bvc @set_blinking +$A32A: A9 70 lda #$70 + + @set_blinking: +$A32C: 85 04 sta Globals::zp_nmi_reserved +$A32E: 20 BC A2 jsr Driver::nmi_blink_player_selection + + @update_lifes: +$A331: A5 50 lda Player::zp_state +$A333: 29 08 and #$08 +$A335: F0 34 beq @shuttle_update +$A337: 2C 02 20 bit PPU::m_status +$A33A: A9 28 lda #$28 +$A33C: 8D 06 20 sta PPU::m_address +$A33F: A9 4B lda #$4B +$A341: 8D 06 20 sta PPU::m_address +$A344: A5 53 lda Player::zp_lifes +$A346: 18 clc +$A347: 69 10 adc #$10 +$A349: 8D 07 20 sta PPU::m_data +$A34C: 24 27 bit Globals::zp_multiplayer +$A34E: 10 15 bpl @unset_life_flag +$A350: 2C 02 20 bit PPU::m_status +$A353: A9 28 lda #$28 +$A355: 8D 06 20 sta PPU::m_address +$A358: A9 56 lda #$56 +$A35A: 8D 06 20 sta PPU::m_address +$A35D: A5 54 lda $54 +$A35F: 18 clc +$A360: 69 10 adc #$10 +$A362: 8D 07 20 sta PPU::m_data + + @unset_life_flag: +$A365: A5 50 lda Player::zp_state +$A367: 29 F7 and #$F7 +$A369: 85 50 sta Player::zp_state + + @shuttle_update: +$A36B: A5 20 lda Globals::zp_flags +$A36D: AA tax +$A36E: 29 20 and #$20 +$A370: F0 13 beq @game_status +$A372: A5 CB lda Items::zp_collected +$A374: D0 06 bne @do_update_shuttle +$A376: 20 78 89 jsr Background::nmi_clear_shuttle +$A379: 4C 7F A3 jmp @unset_shuttle_flag + + @do_update_shuttle: +$A37C: 20 59 91 jsr Items::update_shuttle + + @unset_shuttle_flag: +$A37F: A5 20 lda Globals::zp_flags +$A381: 29 DF and #$DF +$A383: 85 20 sta Globals::zp_flags + + @game_status: +$A385: 8A txa +$A386: 29 01 and #$01 +$A388: D0 06 bne @ppu_registers +$A38A: A5 30 lda Title::zp_title_timer +$A38C: F0 02 beq @ppu_registers +$A38E: C6 30 dec Title::zp_title_timer + + @ppu_registers: +$A390: 24 20 bit Globals::zp_flags +$A392: 50 13 bvc @scroll +$A394: A9 BF lda #$BF +$A396: 25 20 and Globals::zp_flags +$A398: 85 20 sta Globals::zp_flags +$A39A: 2C 02 20 bit PPU::m_status +$A39D: A5 81 lda PPU::zp_mask +$A39F: 8D 01 20 sta PPU::m_mask +$A3A2: A5 80 lda PPU::zp_control +$A3A4: 8D 00 20 sta PPU::m_control + + @scroll: +$A3A7: 2C 02 20 bit PPU::m_status +$A3AA: A9 00 lda #$00 +$A3AC: 8D 05 20 sta PPU::m_scroll +$A3AF: 8D 05 20 sta PPU::m_scroll +$A3B2: 20 1D 8B jsr Sound::tick +$A3B5: A9 7F lda #$7F +$A3B7: 25 20 and Globals::zp_flags +$A3B9: 85 20 sta Globals::zp_flags +$A3BB: 68 pla +$A3BC: A8 tay +$A3BD: 68 pla +$A3BE: AA tax +$A3BF: 68 pla +$A3C0: 40 rti + + irq: |
