1use std::fs::{File, OpenOptions};
47use std::io::Write;
48use std::path::PathBuf;
49use std::sync::atomic::{AtomicBool, Ordering};
50
51use serde::Serialize;
52
53use crate::cli::args::OutputFormat;
54
55#[derive(Debug, Clone, Serialize)]
67pub struct OamSprite {
68 pub index: u8,
70 pub x: u8,
72 pub y: u8,
74 pub tile: u8,
76 pub palette: u8,
78 pub behind_background: bool,
80 pub flip_h: bool,
82 pub flip_v: bool,
84 pub visible: bool,
86 pub raw: [u8; 4],
88}
89
90impl OamSprite {
91 pub fn from_bytes(index: u8, bytes: &[u8]) -> Self {
96 assert!(
97 bytes.len() >= 4,
98 "OAM sprite requires at least 4 bytes, got {}",
99 bytes.len()
100 );
101 let y = bytes[0];
102 let tile = bytes[1];
103 let attr = bytes[2];
104 let x = bytes[3];
105
106 Self {
107 index,
108 x,
109 y,
110 tile,
111 palette: attr & 0x03,
112 behind_background: (attr & 0x20) != 0,
113 flip_h: (attr & 0x40) != 0,
114 flip_v: (attr & 0x80) != 0,
115 visible: y < 0xEF,
116 raw: [bytes[0], bytes[1], bytes[2], bytes[3]],
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize)]
123pub struct InterpretedOam {
124 pub sprite_count: u8,
126 pub visible_count: u8,
128 pub sprites: Vec<OamSprite>,
130}
131
132impl InterpretedOam {
133 pub fn from_raw(data: &[u8]) -> Self {
136 let mut sprites = Vec::with_capacity(64);
137 let mut visible_count = 0u8;
138
139 for i in 0..64 {
140 let offset = i * 4;
141 if offset + 4 <= data.len() {
142 let sprite = OamSprite::from_bytes(i as u8, &data[offset..offset + 4]);
143 if sprite.visible {
144 visible_count += 1;
145 }
146 sprites.push(sprite);
147 }
148 }
149
150 Self {
151 sprite_count: sprites.len() as u8,
152 visible_count,
153 sprites,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize)]
164pub struct InterpretedNametable {
165 pub index: u8,
167 pub base_address: String,
169 pub tiles: Vec<Vec<u8>>,
171 pub attributes: Vec<u8>,
173 pub tile_palettes: Vec<Vec<u8>>,
175}
176
177impl InterpretedNametable {
178 pub fn from_raw(index: u8, data: &[u8]) -> Self {
184 let base_addresses = [0x2000u16, 0x2400, 0x2800, 0x2C00];
185 let base = base_addresses[index as usize % 4];
186
187 let mut tiles = Vec::with_capacity(30);
189 for row in 0..30 {
190 let start = row * 32;
191 let end = start + 32;
192 if end <= data.len() {
193 tiles.push(data[start..end].to_vec());
194 } else {
195 tiles.push(vec![0; 32]);
196 }
197 }
198
199 let attr_start = 0x3C0;
201 let attributes = if attr_start + 64 <= data.len() {
202 data[attr_start..attr_start + 64].to_vec()
203 } else {
204 vec![0; 64]
205 };
206
207 let mut tile_palettes = Vec::with_capacity(30);
215 for row in 0..30 {
216 let mut row_palettes = Vec::with_capacity(32);
217 for col in 0..32 {
218 let attr_col = col / 4;
220 let attr_row = row / 4;
221 let attr_index = attr_row * 8 + attr_col;
222
223 if attr_index < attributes.len() {
224 let attr = attributes[attr_index];
225 let quadrant_x = (col % 4) / 2;
227 let quadrant_y = (row % 4) / 2;
228 let shift = (quadrant_y * 2 + quadrant_x) * 2;
229 let palette = (attr >> shift) & 0x03;
230 row_palettes.push(palette);
231 } else {
232 row_palettes.push(0);
233 }
234 }
235 tile_palettes.push(row_palettes);
236 }
237
238 Self {
239 index,
240 base_address: format!("0x{:04X}", base),
241 tiles,
242 attributes,
243 tile_palettes,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Serialize)]
250pub struct InterpretedNametables {
251 pub total_size: usize,
253 pub nametables: Vec<InterpretedNametable>,
255}
256
257impl InterpretedNametables {
258 pub fn from_raw(data: &[u8]) -> Self {
262 let mut nametables = Vec::with_capacity(4);
263
264 for i in 0..4 {
265 let start = i * 0x400;
266 let end = start + 0x400;
267 if end <= data.len() {
268 nametables.push(InterpretedNametable::from_raw(i as u8, &data[start..end]));
269 } else if start < data.len() {
270 let partial = &data[start..];
272 let mut padded = partial.to_vec();
273 padded.resize(0x400, 0);
274 nametables.push(InterpretedNametable::from_raw(i as u8, &padded));
275 }
276 }
277
278 Self {
279 total_size: data.len(),
280 nametables,
281 }
282 }
283}
284
285#[derive(Debug, Clone)]
294pub struct MemoryDump {
295 pub mem_type: MemoryType,
297 pub start_addr: u16,
299 pub end_addr: u16,
301 pub data: Vec<u8>,
303 pub interpreted_oam: Option<InterpretedOam>,
305 pub interpreted_nametables: Option<InterpretedNametables>,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum MemoryType {
312 Cpu,
314 Ppu,
316 Oam,
318 Nametables,
320 PaletteRam,
322}
323
324impl MemoryType {
325 pub fn as_str(&self) -> &'static str {
327 match self {
328 MemoryType::Cpu => "cpu",
329 MemoryType::Ppu => "ppu",
330 MemoryType::Oam => "oam",
331 MemoryType::Nametables => "nametables",
332 MemoryType::PaletteRam => "palette_ram",
333 }
334 }
335}
336
337impl std::fmt::Display for MemoryType {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 write!(f, "{}", self.as_str())
340 }
341}
342
343impl MemoryDump {
344 pub fn new(mem_type: MemoryType, start_addr: u16, data: Vec<u8>) -> Self {
346 let end_addr = if data.is_empty() {
347 start_addr
348 } else {
349 start_addr.saturating_add((data.len() - 1) as u16)
350 };
351 Self {
352 mem_type,
353 start_addr,
354 end_addr,
355 data,
356 interpreted_oam: None,
357 interpreted_nametables: None,
358 }
359 }
360
361 pub fn cpu(start_addr: u16, data: Vec<u8>) -> Self {
363 Self::new(MemoryType::Cpu, start_addr, data)
364 }
365
366 pub fn ppu(start_addr: u16, data: Vec<u8>) -> Self {
368 Self::new(MemoryType::Ppu, start_addr, data)
369 }
370
371 pub fn oam(data: Vec<u8>) -> Self {
373 let interpreted = InterpretedOam::from_raw(&data);
374 let mut dump = Self::new(MemoryType::Oam, 0, data);
375 dump.interpreted_oam = Some(interpreted);
376 dump
377 }
378
379 pub fn nametables(data: Vec<u8>) -> Self {
381 let interpreted = InterpretedNametables::from_raw(&data);
382 let mut dump = Self::new(MemoryType::Nametables, 0x2000, data);
383 dump.interpreted_nametables = Some(interpreted);
384 dump
385 }
386
387 pub fn palette_ram(data: Vec<u8>) -> Self { Self::new(MemoryType::PaletteRam, 0x3F00, data) }
390}
391
392pub trait MemoryFormatter: Send + Sync {
400 fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String>;
402
403 fn file_extension(&self) -> &'static str;
405
406 fn is_text(&self) -> bool { true }
408}
409
410pub struct HexFormatter;
420
421impl MemoryFormatter for HexFormatter {
422 fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
423 let mut output = String::new();
424
425 if let Some(ref oam) = dump.interpreted_oam {
427 output.push_str("=== OAM Interpretation ===\n");
428 output.push_str(&format!(
429 "Total sprites: {}, Visible: {}\n\n",
430 oam.sprite_count, oam.visible_count
431 ));
432 output.push_str("Idx | X | Y | Tile | Pal | Pri | FlipH | FlipV | Visible\n");
433 output.push_str("----+-----+-----+------+-----+-----+-------+-------+--------\n");
434 for sprite in &oam.sprites {
435 output.push_str(&format!(
436 "{:3} | {:3} | {:3} | 0x{:02X} | {} | {} | {} | {} | {}\n",
437 sprite.index,
438 sprite.x,
439 sprite.y,
440 sprite.tile,
441 sprite.palette,
442 if sprite.behind_background { "B" } else { "F" },
443 if sprite.flip_h { "Y" } else { "N" },
444 if sprite.flip_v { "Y" } else { "N" },
445 if sprite.visible { "Yes" } else { "No" }
446 ));
447 }
448 output.push_str("\n=== Raw OAM Data ===\n");
449 }
450
451 if let Some(ref nt) = dump.interpreted_nametables {
453 output.push_str("=== Nametables Interpretation ===\n");
454 output.push_str(&format!("Total size: {} bytes\n\n", nt.total_size));
455 for nametable in &nt.nametables {
456 output.push_str(&format!(
457 "Nametable {} (base: {})\n",
458 nametable.index, nametable.base_address
459 ));
460 output.push_str(" Tiles (32x30 grid, showing first 8 rows):\n");
461 for (row_idx, row) in nametable.tiles.iter().take(8).enumerate() {
462 output.push_str(&format!(" Row {:2}: ", row_idx));
463 for tile in row.iter().take(32) {
464 output.push_str(&format!("{:02X} ", tile));
465 }
466 output.push('\n');
467 }
468 if nametable.tiles.len() > 8 {
469 output.push_str(" ... (22 more rows)\n");
470 }
471 output.push('\n');
472 }
473 output.push_str("=== Raw Nametable Data ===\n");
474 }
475
476 for (i, chunk) in dump.data.chunks(16).enumerate() {
478 let line = format!(
479 "{:04X}: {}\n",
480 dump.start_addr as usize + i * 16,
481 chunk
482 .iter()
483 .map(|b| format!("{:02X}", b))
484 .collect::<Vec<_>>()
485 .join(" ")
486 );
487 output.push_str(&line);
488 }
489 Ok(output.into_bytes())
490 }
491
492 fn file_extension(&self) -> &'static str { "hex" }
493}
494
495pub struct BinaryFormatter;
497
498impl MemoryFormatter for BinaryFormatter {
499 fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> { Ok(dump.data.clone()) }
500
501 fn file_extension(&self) -> &'static str { "bin" }
502
503 fn is_text(&self) -> bool { false }
504}
505
506pub struct JsonFormatter;
508
509#[derive(Serialize)]
511struct MemoryDumpOutput {
512 memory_dump: MemoryDumpData,
513}
514
515#[derive(Serialize)]
516struct MemoryDumpData {
517 #[serde(rename = "type")]
518 mem_type: String,
519 start: String,
520 end: String,
521 data: Vec<String>,
522}
523
524#[derive(Serialize)]
526struct OamDumpOutput {
527 oam_dump: OamDumpData,
528}
529
530#[derive(Serialize)]
531struct OamDumpData {
532 #[serde(rename = "type")]
533 mem_type: String,
534 size: usize,
535 raw_data: Vec<String>,
536 interpretation: InterpretedOam,
537}
538
539#[derive(Serialize)]
541struct NametablesDumpOutput {
542 nametables_dump: NametablesDumpData,
543}
544
545#[derive(Serialize)]
546struct NametablesDumpData {
547 #[serde(rename = "type")]
548 mem_type: String,
549 start: String,
550 end: String,
551 raw_data: Vec<String>,
552 interpretation: InterpretedNametables,
553}
554
555impl MemoryFormatter for JsonFormatter {
556 fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
557 let data_hex: Vec<String> = dump.data.iter().map(|b| format!("0x{:02X}", b)).collect();
558
559 let json_str = match dump.mem_type {
560 MemoryType::Oam => {
561 if let Some(ref interp) = dump.interpreted_oam {
562 let output = OamDumpOutput {
563 oam_dump: OamDumpData {
564 mem_type: dump.mem_type.to_string(),
565 size: dump.data.len(),
566 raw_data: data_hex,
567 interpretation: interp.clone(),
568 },
569 };
570 serde_json::to_string_pretty(&output)
571 .map_err(|e| format!("Failed to serialize JSON: {}", e))?
572 } else {
573 let output = MemoryDumpOutput {
575 memory_dump: MemoryDumpData {
576 mem_type: dump.mem_type.to_string(),
577 start: format!("0x{:04X}", dump.start_addr),
578 end: format!("0x{:04X}", dump.end_addr),
579 data: data_hex,
580 },
581 };
582 serde_json::to_string_pretty(&output)
583 .map_err(|e| format!("Failed to serialize JSON: {}", e))?
584 }
585 }
586 MemoryType::Nametables => {
587 if let Some(ref interp) = dump.interpreted_nametables {
588 let output = NametablesDumpOutput {
589 nametables_dump: NametablesDumpData {
590 mem_type: dump.mem_type.to_string(),
591 start: format!("0x{:04X}", dump.start_addr),
592 end: format!("0x{:04X}", dump.end_addr),
593 raw_data: data_hex,
594 interpretation: interp.clone(),
595 },
596 };
597 serde_json::to_string_pretty(&output)
598 .map_err(|e| format!("Failed to serialize JSON: {}", e))?
599 } else {
600 let output = MemoryDumpOutput {
602 memory_dump: MemoryDumpData {
603 mem_type: dump.mem_type.to_string(),
604 start: format!("0x{:04X}", dump.start_addr),
605 end: format!("0x{:04X}", dump.end_addr),
606 data: data_hex,
607 },
608 };
609 serde_json::to_string_pretty(&output)
610 .map_err(|e| format!("Failed to serialize JSON: {}", e))?
611 }
612 }
613 _ => {
614 let output = MemoryDumpOutput {
615 memory_dump: MemoryDumpData {
616 mem_type: dump.mem_type.to_string(),
617 start: format!("0x{:04X}", dump.start_addr),
618 end: format!("0x{:04X}", dump.end_addr),
619 data: data_hex,
620 },
621 };
622 serde_json::to_string_pretty(&output)
623 .map_err(|e| format!("Failed to serialize JSON: {}", e))?
624 }
625 };
626
627 Ok(format!("{}\n", json_str).into_bytes())
628 }
629
630 fn file_extension(&self) -> &'static str { "json" }
631}
632
633pub struct TomlFormatter;
635
636impl MemoryFormatter for TomlFormatter {
637 fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
638 let data_hex: Vec<String> = dump.data.iter().map(|b| format!("0x{:02X}", b)).collect();
639
640 let toml_str = match dump.mem_type {
641 MemoryType::Oam => {
642 if let Some(ref interp) = dump.interpreted_oam {
643 let output = OamDumpOutput {
644 oam_dump: OamDumpData {
645 mem_type: dump.mem_type.to_string(),
646 size: dump.data.len(),
647 raw_data: data_hex,
648 interpretation: interp.clone(),
649 },
650 };
651 toml::to_string_pretty(&output)
652 .map_err(|e| format!("Failed to serialize TOML: {}", e))?
653 } else {
654 let output = MemoryDumpOutput {
655 memory_dump: MemoryDumpData {
656 mem_type: dump.mem_type.to_string(),
657 start: format!("0x{:04X}", dump.start_addr),
658 end: format!("0x{:04X}", dump.end_addr),
659 data: data_hex,
660 },
661 };
662 toml::to_string_pretty(&output)
663 .map_err(|e| format!("Failed to serialize TOML: {}", e))?
664 }
665 }
666 MemoryType::Nametables => {
667 if let Some(ref interp) = dump.interpreted_nametables {
668 let output = NametablesDumpOutput {
669 nametables_dump: NametablesDumpData {
670 mem_type: dump.mem_type.to_string(),
671 start: format!("0x{:04X}", dump.start_addr),
672 end: format!("0x{:04X}", dump.end_addr),
673 raw_data: data_hex,
674 interpretation: interp.clone(),
675 },
676 };
677 toml::to_string_pretty(&output)
678 .map_err(|e| format!("Failed to serialize TOML: {}", e))?
679 } else {
680 let output = MemoryDumpOutput {
681 memory_dump: MemoryDumpData {
682 mem_type: dump.mem_type.to_string(),
683 start: format!("0x{:04X}", dump.start_addr),
684 end: format!("0x{:04X}", dump.end_addr),
685 data: data_hex,
686 },
687 };
688 toml::to_string_pretty(&output)
689 .map_err(|e| format!("Failed to serialize TOML: {}", e))?
690 }
691 }
692 _ => {
693 let output = MemoryDumpOutput {
694 memory_dump: MemoryDumpData {
695 mem_type: dump.mem_type.to_string(),
696 start: format!("0x{:04X}", dump.start_addr),
697 end: format!("0x{:04X}", dump.end_addr),
698 data: data_hex,
699 },
700 };
701 toml::to_string_pretty(&output)
702 .map_err(|e| format!("Failed to serialize TOML: {}", e))?
703 }
704 };
705
706 Ok(format!("{}\n", toml_str).into_bytes())
707 }
708
709 fn file_extension(&self) -> &'static str { "toml" }
710}
711
712impl OutputFormat {
717 pub fn formatter(&self) -> Box<dyn MemoryFormatter> {
721 match self {
722 OutputFormat::Hex => Box::new(HexFormatter),
723 OutputFormat::Json => Box::new(JsonFormatter),
724 OutputFormat::Toml => Box::new(TomlFormatter),
725 OutputFormat::Binary => Box::new(BinaryFormatter),
726 }
727 }
728
729 pub fn extension(&self) -> &'static str {
731 match self {
732 OutputFormat::Hex => "hex",
733 OutputFormat::Json => "json",
734 OutputFormat::Toml => "toml",
735 OutputFormat::Binary => "bin",
736 }
737 }
738}
739
740static OUTPUT_FILE_INITIALIZED: AtomicBool = AtomicBool::new(false);
748
749pub struct OutputWriter {
751 path: Option<PathBuf>,
753 format: OutputFormat,
755}
756
757impl OutputWriter {
758 pub fn new(path: Option<PathBuf>, format: OutputFormat) -> Self {
760 Self {
761 path,
762 format,
763 }
764 }
765
766 pub fn reset() { OUTPUT_FILE_INITIALIZED.store(false, Ordering::SeqCst); }
768
769 pub fn write(&self, dump: &MemoryDump) -> Result<(), String> {
771 let formatter = self.format.formatter();
772 let data = formatter.format(dump)?;
773 self.write_bytes(&data)
774 }
775
776 fn write_bytes(&self, data: &[u8]) -> Result<(), String> {
778 let mut writer = self.get_writer()?;
779 writer.write_all(data).map_err(|e| e.to_string())
780 }
781
782 fn get_writer(&self) -> Result<Box<dyn Write>, String> {
784 if let Some(ref path) = self.path {
785 let is_first_write = !OUTPUT_FILE_INITIALIZED.swap(true, Ordering::SeqCst);
786
787 let file = if is_first_write {
788 File::create(path)
789 } else {
790 OpenOptions::new().append(true).open(path)
791 }
792 .map_err(|e| format!("Failed to open output file: {}", e))?;
793
794 Ok(Box::new(file))
795 } else {
796 Ok(Box::new(std::io::stdout()))
797 }
798 }
799}
800
801#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn test_hex_formatter() {
811 let dump = MemoryDump::cpu(0x0000, vec![0x00, 0x01, 0x02, 0x03]);
812 let formatter = HexFormatter;
813 let output = formatter.format(&dump).unwrap();
814 let text = String::from_utf8(output).unwrap();
815 assert!(text.contains("0000: 00 01 02 03"));
816 }
817
818 #[test]
819 fn test_binary_formatter() {
820 let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
821 let dump = MemoryDump::cpu(0x1000, data.clone());
822 let formatter = BinaryFormatter;
823 let output = formatter.format(&dump).unwrap();
824 assert_eq!(output, data);
825 }
826
827 #[test]
828 fn test_json_formatter() {
829 let dump = MemoryDump::cpu(0x0000, vec![0xFF]);
830 let formatter = JsonFormatter;
831 let output = formatter.format(&dump).unwrap();
832 let text = String::from_utf8(output).unwrap();
833 assert!(text.contains("\"type\": \"cpu\""));
834 assert!(text.contains("0xFF"));
835 }
836
837 #[test]
838 fn test_toml_formatter() {
839 let dump = MemoryDump::cpu(0x0000, vec![0xFF]);
840 let formatter = TomlFormatter;
841 let output = formatter.format(&dump).unwrap();
842 let text = String::from_utf8(output).unwrap();
843 assert!(text.contains("type = \"cpu\""));
844 }
845
846 #[test]
847 fn test_output_format_formatter() {
848 assert_eq!(OutputFormat::Hex.extension(), "hex");
849 assert_eq!(OutputFormat::Json.extension(), "json");
850 assert_eq!(OutputFormat::Toml.extension(), "toml");
851 assert_eq!(OutputFormat::Binary.extension(), "bin");
852 }
853
854 #[test]
859 fn test_oam_sprite_from_bytes() {
860 let bytes = [0x20, 0x42, 0xE3, 0x80];
863 let sprite = OamSprite::from_bytes(5, &bytes);
864
865 assert_eq!(sprite.index, 5);
866 assert_eq!(sprite.y, 0x20);
867 assert_eq!(sprite.tile, 0x42);
868 assert_eq!(sprite.x, 0x80);
869 assert_eq!(sprite.palette, 3);
870 assert!(sprite.behind_background);
871 assert!(sprite.flip_h);
872 assert!(sprite.flip_v);
873 assert!(sprite.visible); }
875
876 #[test]
877 fn test_oam_sprite_visibility() {
878 let hidden = OamSprite::from_bytes(0, &[0xF0, 0x00, 0x00, 0x00]);
880 assert!(!hidden.visible);
881
882 let visible = OamSprite::from_bytes(0, &[0xEE, 0x00, 0x00, 0x00]);
884 assert!(visible.visible);
885 }
886
887 #[test]
888 fn test_interpreted_oam_from_raw() {
889 let mut data = vec![0xFFu8; 256]; data[0] = 0x10; data[1] = 0x01; data[2] = 0x00; data[3] = 0x20; data[4] = 0xFF; data[5] = 0x02; data[6] = 0x00; data[7] = 0x30; let interp = InterpretedOam::from_raw(&data);
904
905 assert_eq!(interp.sprite_count, 64);
906 assert_eq!(interp.visible_count, 1); assert_eq!(interp.sprites.len(), 64);
908 assert_eq!(interp.sprites[0].y, 0x10);
909 assert_eq!(interp.sprites[0].tile, 0x01);
910 assert!(interp.sprites[0].visible);
911 assert!(!interp.sprites[1].visible);
912 }
913
914 #[test]
915 fn test_oam_dump_json_has_interpretation() {
916 let mut data = vec![0u8; 256];
917 data[0] = 0x10; data[1] = 0x42; data[2] = 0x01; data[3] = 0x50; let dump = MemoryDump::oam(data);
923 let formatter = JsonFormatter;
924 let output = formatter.format(&dump).unwrap();
925 let text = String::from_utf8(output).unwrap();
926
927 assert!(text.contains("interpretation"));
929 assert!(text.contains("sprite_count"));
930 assert!(text.contains("visible_count"));
931 assert!(text.contains("sprites"));
932 }
933
934 #[test]
935 fn test_oam_dump_hex_has_interpretation() {
936 let mut data = vec![0u8; 256];
937 data[0] = 0x10;
938 data[1] = 0x42;
939 data[2] = 0x01;
940 data[3] = 0x50;
941
942 let dump = MemoryDump::oam(data);
943 let formatter = HexFormatter;
944 let output = formatter.format(&dump).unwrap();
945 let text = String::from_utf8(output).unwrap();
946
947 assert!(text.contains("=== OAM Interpretation ==="));
949 assert!(text.contains("Total sprites:"));
950 assert!(text.contains("Visible:"));
951 assert!(text.contains("=== Raw OAM Data ==="));
952 }
953
954 #[test]
959 fn test_interpreted_nametable_from_raw() {
960 let mut data = vec![0u8; 1024];
962 data[0] = 0x01; data[31] = 0x1F; data[32] = 0x20; data[0x3C0] = 0b11_10_01_00; let nt = InterpretedNametable::from_raw(0, &data);
970
971 assert_eq!(nt.index, 0);
972 assert_eq!(nt.base_address, "0x2000");
973 assert_eq!(nt.tiles.len(), 30);
974 assert_eq!(nt.tiles[0][0], 0x01);
975 assert_eq!(nt.tiles[0][31], 0x1F);
976 assert_eq!(nt.tiles[1][0], 0x20);
977 assert_eq!(nt.attributes.len(), 64);
978 assert_eq!(nt.attributes[0], 0b11_10_01_00);
979
980 assert_eq!(nt.tile_palettes[0][0], 0); assert_eq!(nt.tile_palettes[0][2], 1); assert_eq!(nt.tile_palettes[2][0], 2); assert_eq!(nt.tile_palettes[2][2], 3); }
986
987 #[test]
988 fn test_interpreted_nametables_from_raw() {
989 let data = vec![0u8; 4096];
991
992 let interp = InterpretedNametables::from_raw(&data);
993
994 assert_eq!(interp.total_size, 4096);
995 assert_eq!(interp.nametables.len(), 4);
996 assert_eq!(interp.nametables[0].base_address, "0x2000");
997 assert_eq!(interp.nametables[1].base_address, "0x2400");
998 assert_eq!(interp.nametables[2].base_address, "0x2800");
999 assert_eq!(interp.nametables[3].base_address, "0x2C00");
1000 }
1001
1002 #[test]
1003 fn test_nametables_dump_json_has_interpretation() {
1004 let data = vec![0u8; 4096];
1005 let dump = MemoryDump::nametables(data);
1006 let formatter = JsonFormatter;
1007 let output = formatter.format(&dump).unwrap();
1008 let text = String::from_utf8(output).unwrap();
1009
1010 assert!(text.contains("interpretation"));
1012 assert!(text.contains("total_size"));
1013 assert!(text.contains("nametables"));
1014 assert!(text.contains("base_address"));
1015 }
1016
1017 #[test]
1018 fn test_nametables_dump_hex_has_interpretation() {
1019 let data = vec![0u8; 4096];
1020 let dump = MemoryDump::nametables(data);
1021 let formatter = HexFormatter;
1022 let output = formatter.format(&dump).unwrap();
1023 let text = String::from_utf8(output).unwrap();
1024
1025 assert!(text.contains("=== Nametables Interpretation ==="));
1027 assert!(text.contains("Nametable 0"));
1028 assert!(text.contains("=== Raw Nametable Data ==="));
1029 }
1030}