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