1use alloc::string::{String, ToString};
44use alloc::vec::Vec;
45
46use rustynes_mappers::{ConsoleType, Mirroring, VsPpuType};
47use sha2::{Digest, Sha256};
48
49use crate::Cpu2A03Revision;
50use crate::Region;
51use crate::genie::GenieCode;
52use crate::nes::{ConsoleModel, Nes, PowerOnRam};
53use crate::save_state::{BinReader, BinWriter};
54use rustynes_ppu::{PaletteInit, PpuRevision};
55
56pub type OptionsDecodeError = &'static str;
62
63const MAX_GENIE_CODES: usize = u8::MAX as usize;
68
69pub const EMULATION_EPOCH: u32 = 3;
100
101#[derive(Clone, Debug, Eq, PartialEq, Hash)]
118#[non_exhaustive]
119#[allow(clippy::struct_excessive_bools)]
123pub struct HardwareOptions {
124 pub console_model: ConsoleModel,
126 pub ppu_revision: PpuRevision,
128 pub cpu_2a03_revision: Cpu2A03Revision,
130 pub oam_decay: bool,
132 pub power_on_ram: PowerOnRam,
134 pub power_up_palette: PaletteInit,
136 pub extra_scanlines: u16,
138 pub cpu_overclock: u8,
144 pub sprite_limit_disabled: bool,
147 pub mmc3_revision: Option<rustynes_mappers::Mmc3Revision>,
150 pub four_score: bool,
153 pub zapper_temporal_light: bool,
156 pub vs_dip: u8,
159 pub vs_ppu_type: Option<VsPpuType>,
162 pub mirroring_override: Option<Mirroring>,
165 pub genie_codes: Vec<String>,
168}
169
170impl Default for HardwareOptions {
171 fn default() -> Self {
172 Self {
173 console_model: ConsoleModel::default(),
174 ppu_revision: PpuRevision::default(),
175 cpu_2a03_revision: Cpu2A03Revision::default(),
176 oam_decay: false,
177 power_on_ram: PowerOnRam::default(),
178 power_up_palette: PaletteInit::default(),
179 extra_scanlines: 0,
180 cpu_overclock: 1,
181 sprite_limit_disabled: false,
182 mmc3_revision: None,
183 four_score: false,
184 zapper_temporal_light: true,
187 vs_dip: 0,
188 vs_ppu_type: None,
189 mirroring_override: None,
190 genie_codes: Vec::new(),
191 }
192 }
193}
194
195impl HardwareOptions {
196 #[must_use]
198 pub fn capture(nes: &Nes) -> Self {
199 Self {
200 console_model: nes.console_model(),
201 ppu_revision: nes.ppu_revision(),
202 cpu_2a03_revision: nes.cpu_2a03_revision(),
203 oam_decay: nes.oam_decay_enabled(),
204 power_on_ram: nes.power_on_ram(),
205 power_up_palette: nes.power_up_palette(),
206 extra_scanlines: nes.extra_scanlines(),
207 cpu_overclock: nes.cpu_overclock(),
208 sprite_limit_disabled: nes.sprite_limit_disabled(),
209 mmc3_revision: nes.mmc3_revision_override(),
210 four_score: nes.four_score(),
211 zapper_temporal_light: nes.zapper_temporal_light(),
212 vs_dip: nes.vs_dip(),
213 vs_ppu_type: Some(nes.vs_ppu_type()),
214 mirroring_override: nes.mirroring_override(),
215 genie_codes: nes.genie_codes().map(|g| g.code().to_string()).collect(),
216 }
217 }
218
219 pub fn apply(&self, nes: &mut Nes) -> Result<(), String> {
233 nes.set_power_on_ram(self.power_on_ram);
234 nes.set_power_up_palette(self.power_up_palette);
235 self.apply_live(nes)
236 }
237
238 pub fn apply_live(&self, nes: &mut Nes) -> Result<(), String> {
250 if nes.console_model() != self.console_model {
251 nes.set_console_model(self.console_model);
252 }
253 if nes.ppu_revision() != self.ppu_revision {
254 nes.set_ppu_revision(self.ppu_revision);
255 }
256 if nes.cpu_2a03_revision() != self.cpu_2a03_revision {
257 nes.set_cpu_2a03_revision(self.cpu_2a03_revision);
258 }
259 if nes.oam_decay_enabled() != self.oam_decay {
260 nes.set_oam_decay(self.oam_decay);
261 }
262 if nes.extra_scanlines() != self.extra_scanlines {
263 nes.set_extra_scanlines(self.extra_scanlines);
264 }
265 if nes.cpu_overclock() != self.cpu_overclock {
266 nes.set_cpu_overclock(self.cpu_overclock);
267 }
268 if nes.sprite_limit_disabled() != self.sprite_limit_disabled {
269 nes.set_sprite_limit_disabled(self.sprite_limit_disabled);
270 }
271 if nes.mmc3_revision_override() != self.mmc3_revision {
272 nes.set_mmc3_revision_override(self.mmc3_revision);
273 }
274 if nes.four_score() != self.four_score {
275 nes.set_four_score(self.four_score);
276 }
277 if nes.zapper_temporal_light() != self.zapper_temporal_light {
278 nes.set_zapper_temporal_light(self.zapper_temporal_light);
279 }
280 if nes.vs_dip() != self.vs_dip {
281 nes.set_vs_dip(self.vs_dip);
282 }
283 if let Some(t) = self.vs_ppu_type
284 && nes.vs_ppu_type() != t
285 {
286 nes.set_vs_ppu_type(t);
287 }
288 if nes.mirroring_override() != self.mirroring_override {
289 nes.set_mirroring_override(self.mirroring_override);
290 }
291 if !nes
294 .genie_codes()
295 .map(GenieCode::code)
296 .eq(self.genie_codes.iter().map(String::as_str))
297 {
298 nes.clear_genie_codes();
299 for code in &self.genie_codes {
300 nes.add_genie_code(code).map_err(|_| code.clone())?;
301 }
302 }
303 Ok(())
304 }
305
306 pub fn restore_after_playback(&self, nes: &mut Nes) -> Result<(), String> {
322 if nes.power_on_ram() != self.power_on_ram
323 || nes.power_up_palette() != self.power_up_palette
324 {
325 let snap = nes.snapshot();
326 nes.set_power_on_ram(self.power_on_ram);
327 nes.set_power_up_palette(self.power_up_palette);
328 let restored = nes.restore_quiet(&snap);
332 debug_assert!(restored.is_ok(), "own snapshot must restore");
333 }
334 self.apply_live(nes)
335 }
336
337 pub fn write_to(&self, w: &mut BinWriter) {
351 w.u8(match self.console_model {
352 ConsoleModel::Nes => 0,
353 ConsoleModel::Famicom => 1,
354 });
355 w.u8(match self.ppu_revision {
356 PpuRevision::Rp2c02H => 0,
357 PpuRevision::Rp2c02G => 1,
358 });
359 w.u8(match self.cpu_2a03_revision {
360 Cpu2A03Revision::Rp2A03G => 0,
361 Cpu2A03Revision::Rp2A03H => 1,
362 });
363 w.u8(u8::from(self.oam_decay));
364 let (kind, value) = match self.power_on_ram {
365 PowerOnRam::Zeroed => (0u8, 0u64),
366 PowerOnRam::Seeded(seed) => (1, seed),
367 PowerOnRam::Filled(byte) => (2, u64::from(byte)),
368 };
369 w.u8(kind);
370 w.u64(value);
371 w.u8(match self.power_up_palette {
372 PaletteInit::Zeroed => 0,
373 PaletteInit::Blargg => 1,
374 });
375 w.u16(self.extra_scanlines);
376 w.u8(self.cpu_overclock);
377 w.u8(u8::from(self.sprite_limit_disabled));
378 w.u8(match self.mmc3_revision {
379 None => 0,
380 Some(rustynes_mappers::Mmc3Revision::Sharp) => 1,
381 Some(rustynes_mappers::Mmc3Revision::Nec) => 2,
382 });
383 w.u8(u8::from(self.four_score));
384 w.u8(u8::from(self.zapper_temporal_light));
385 w.u8(self.vs_dip);
386 w.u8(self.vs_ppu_type.map_or(0xFF, vs_ppu_to_byte));
387 w.u8(self
388 .mirroring_override
389 .map_or(0, |m| mirroring_to_byte(m) + 1));
390 let count = self.genie_codes.len().min(MAX_GENIE_CODES);
391 w.u8(u8::try_from(count).unwrap_or(u8::MAX));
392 for code in self.genie_codes.iter().take(count) {
393 let bytes = code.as_bytes();
394 w.u8(u8::try_from(bytes.len()).unwrap_or(u8::MAX));
395 w.bytes(&bytes[..bytes.len().min(usize::from(u8::MAX))]);
396 }
397 }
398
399 pub fn read_from(r: &mut BinReader<'_>) -> Result<Self, OptionsDecodeError> {
408 let console_model = match byte(r)? {
409 0 => ConsoleModel::Nes,
410 1 => ConsoleModel::Famicom,
411 _ => return Err("unknown console-model byte"),
412 };
413 let ppu_revision = match byte(r)? {
414 0 => PpuRevision::Rp2c02H,
415 1 => PpuRevision::Rp2c02G,
416 _ => return Err("unknown PPU-revision byte"),
417 };
418 let cpu_2a03_revision = match byte(r)? {
419 0 => Cpu2A03Revision::Rp2A03G,
420 1 => Cpu2A03Revision::Rp2A03H,
421 _ => return Err("unknown 2A03-revision byte"),
422 };
423 let oam_decay = flag(r, "OAM-decay flag is not 0 or 1")?;
424 let kind = byte(r)?;
425 let value = r.u64().map_err(|_| "truncated")?;
426 let power_on_ram = match kind {
427 0 if value == 0 => PowerOnRam::Zeroed,
428 1 => PowerOnRam::Seeded(value),
429 2 => {
430 PowerOnRam::Filled(u8::try_from(value).map_err(|_| "power-on fill is not a byte")?)
431 }
432 _ => return Err("unknown power-on RAM kind"),
433 };
434 let power_up_palette = match byte(r)? {
435 0 => PaletteInit::Zeroed,
436 1 => PaletteInit::Blargg,
437 _ => return Err("unknown power-up palette byte"),
438 };
439 let extra_scanlines = r.u16().map_err(|_| "truncated")?;
440 if extra_scanlines > crate::nes::MAX_EXTRA_SCANLINES {
444 return Err("extra-scanline overclock is above the core's maximum");
445 }
446 let cpu_overclock = byte(r)?;
449 if cpu_overclock == 0 || cpu_overclock > crate::nes::MAX_CPU_OVERCLOCK {
450 return Err("CPU overclock is outside 1..=MAX_CPU_OVERCLOCK");
451 }
452 let sprite_limit_disabled = flag(r, "sprite-limit flag is not 0 or 1")?;
453 let mmc3_revision = match byte(r)? {
454 0 => None,
455 1 => Some(rustynes_mappers::Mmc3Revision::Sharp),
456 2 => Some(rustynes_mappers::Mmc3Revision::Nec),
457 _ => return Err("unknown MMC3 revision byte"),
458 };
459 let four_score = flag(r, "Four Score flag is not 0 or 1")?;
460 let zapper_temporal_light = flag(r, "Zapper light flag is not 0 or 1")?;
461 let vs_dip = byte(r)?;
462 let vs_ppu_type = match byte(r)? {
463 0xFF => None,
464 b => Some(vs_ppu_from_byte(b).ok_or("unknown Vs. PPU type byte")?),
465 };
466 let mirroring_override = match byte(r)? {
467 0 => None,
468 b => Some(mirroring_from_byte(b - 1).ok_or("unknown mirroring byte")?),
469 };
470 let count = usize::from(byte(r)?);
471 let mut by_addr = alloc::collections::BTreeMap::new();
477 for _ in 0..count {
478 let len = usize::from(byte(r)?);
479 let raw = r.take(len).map_err(|_| "truncated")?;
480 let text = core::str::from_utf8(raw).map_err(|_| "Game Genie code is not text")?;
481 let code = GenieCode::new(text).map_err(|_| "Game Genie code does not decode")?;
482 by_addr.insert(code.addr(), code.code().to_string());
483 }
484 let genie_codes: Vec<String> = by_addr.into_values().collect();
485 Ok(Self {
486 console_model,
487 ppu_revision,
488 cpu_2a03_revision,
489 oam_decay,
490 power_on_ram,
491 power_up_palette,
492 extra_scanlines,
493 cpu_overclock,
494 sprite_limit_disabled,
495 mmc3_revision,
496 four_score,
497 zapper_temporal_light,
498 vs_dip,
499 vs_ppu_type,
500 mirroring_override,
501 genie_codes,
502 })
503 }
504
505 #[must_use]
507 pub fn to_bytes(&self) -> Vec<u8> {
508 let mut w = BinWriter::with_capacity(32);
509 self.write_to(&mut w);
510 w.into_vec()
511 }
512
513 #[must_use]
517 pub fn differences(&self, other: &Self) -> Vec<&'static str> {
518 let mut out = Vec::new();
519 let mut check = |differs: bool, name: &'static str| {
520 if differs {
521 out.push(name);
522 }
523 };
524 check(self.console_model != other.console_model, "console model");
525 check(self.ppu_revision != other.ppu_revision, "PPU revision");
526 check(
527 self.cpu_2a03_revision != other.cpu_2a03_revision,
528 "2A03 revision",
529 );
530 check(self.oam_decay != other.oam_decay, "OAM decay");
531 check(self.power_on_ram != other.power_on_ram, "power-on RAM");
532 check(
533 self.power_up_palette != other.power_up_palette,
534 "power-up palette",
535 );
536 check(
537 self.extra_scanlines != other.extra_scanlines,
538 "overclock scanlines",
539 );
540 check(self.cpu_overclock != other.cpu_overclock, "CPU overclock");
541 check(
542 self.sprite_limit_disabled != other.sprite_limit_disabled,
543 "sprite limit",
544 );
545 check(self.mmc3_revision != other.mmc3_revision, "MMC3 revision");
546 check(self.four_score != other.four_score, "Four Score");
547 check(
548 self.zapper_temporal_light != other.zapper_temporal_light,
549 "Zapper light model",
550 );
551 check(self.vs_dip != other.vs_dip, "Vs. DIP switches");
552 check(self.vs_ppu_type != other.vs_ppu_type, "Vs. PPU type");
553 check(
554 self.mirroring_override != other.mirroring_override,
555 "mirroring override",
556 );
557 check(self.genie_codes != other.genie_codes, "Game Genie codes");
558 out
559 }
560}
561
562#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
572#[non_exhaustive]
573pub struct BoardDescription {
574 pub mapper_id: u16,
576 pub submapper: u8,
578 pub mirroring: Mirroring,
580 pub console_type: ConsoleType,
582 pub vs_dual_system: bool,
584 pub prg_ram_size: u32,
586 pub chr_ram_size: u32,
588 pub has_battery: bool,
590 pub has_trainer: bool,
592 pub prg_rom_len: u32,
597 pub chr_rom_len: u32,
599 pub nametable_wiring_bits: u8,
602}
603
604impl BoardDescription {
605 #[must_use]
607 #[allow(clippy::cast_possible_truncation)] pub fn capture(nes: &Nes) -> Self {
609 let c = nes.cartridge();
610 Self {
611 mapper_id: c.mapper_id,
612 submapper: c.submapper,
613 mirroring: c.mirroring,
614 console_type: c.console_type,
615 vs_dual_system: c.vs_dual_system,
616 prg_ram_size: c.prg_ram_size,
617 chr_ram_size: c.chr_ram_size,
618 has_battery: c.has_battery,
619 has_trainer: c.has_trainer,
620 prg_rom_len: c.prg_rom.len() as u32,
621 chr_rom_len: c.chr_rom.len() as u32,
622 nametable_wiring_bits: c.nametable_wiring_bits,
623 }
624 }
625
626 #[must_use]
629 pub fn first_difference(&self, other: &Self) -> Option<&'static str> {
630 [
631 (self.mapper_id != other.mapper_id, "mapper"),
632 (self.submapper != other.submapper, "submapper"),
633 (self.mirroring != other.mirroring, "mirroring"),
634 (self.console_type != other.console_type, "console type"),
635 (
636 self.vs_dual_system != other.vs_dual_system,
637 "Vs. DualSystem",
638 ),
639 (self.prg_ram_size != other.prg_ram_size, "PRG-RAM size"),
640 (self.chr_ram_size != other.chr_ram_size, "CHR-RAM size"),
641 (self.has_battery != other.has_battery, "battery"),
642 (self.has_trainer != other.has_trainer, "trainer"),
643 (self.prg_rom_len != other.prg_rom_len, "PRG-ROM size"),
644 (self.chr_rom_len != other.chr_rom_len, "CHR-ROM size"),
645 (
646 self.nametable_wiring_bits != other.nametable_wiring_bits,
647 "header nametable wiring",
648 ),
649 ]
650 .into_iter()
651 .find_map(|(differs, name)| differs.then_some(name))
652 }
653
654 pub fn write_to(&self, w: &mut BinWriter) {
659 w.u16(self.mapper_id);
660 w.u8(self.submapper);
661 w.u8(mirroring_to_byte(self.mirroring));
662 w.u8(match self.console_type {
663 ConsoleType::Nes => 0,
664 ConsoleType::VsSystem => 1,
665 ConsoleType::Playchoice10 => 2,
666 ConsoleType::Extended => 3,
667 });
668 w.u8(u8::from(self.vs_dual_system));
669 w.u32(self.prg_ram_size);
670 w.u32(self.chr_ram_size);
671 w.u8(u8::from(self.has_battery));
672 w.u8(u8::from(self.has_trainer));
673 w.u32(self.prg_rom_len);
674 w.u32(self.chr_rom_len);
675 w.u8(self.nametable_wiring_bits);
676 }
677
678 pub fn read_from(r: &mut BinReader<'_>) -> Result<Self, OptionsDecodeError> {
685 let mapper_id = r.u16().map_err(|_| "truncated")?;
686 let submapper = byte(r)?;
687 let mirroring = mirroring_from_byte(byte(r)?).ok_or("unknown board mirroring byte")?;
688 let console_type = match byte(r)? {
689 0 => ConsoleType::Nes,
690 1 => ConsoleType::VsSystem,
691 2 => ConsoleType::Playchoice10,
692 3 => ConsoleType::Extended,
693 _ => return Err("unknown console-type byte"),
694 };
695 let vs_dual_system = flag(r, "DualSystem flag is not 0 or 1")?;
696 let prg_ram_size = r.u32().map_err(|_| "truncated")?;
697 let chr_ram_size = r.u32().map_err(|_| "truncated")?;
698 let has_battery = flag(r, "battery flag is not 0 or 1")?;
699 let has_trainer = flag(r, "trainer flag is not 0 or 1")?;
700 let prg_rom_len = r.u32().map_err(|_| "truncated")?;
701 let chr_rom_len = r.u32().map_err(|_| "truncated")?;
702 let nametable_wiring_bits = byte(r)?;
703 if nametable_wiring_bits & !0x09 != 0 {
704 return Err("header nametable wiring byte has bits other than 0 and 3");
705 }
706 Ok(Self {
707 mapper_id,
708 submapper,
709 mirroring,
710 console_type,
711 vs_dual_system,
712 prg_ram_size,
713 chr_ram_size,
714 has_battery,
715 has_trainer,
716 prg_rom_len,
717 chr_rom_len,
718 nametable_wiring_bits,
719 })
720 }
721}
722
723#[must_use]
734pub fn config_digest(nes: &Nes) -> [u8; 32] {
735 let mut w = BinWriter::with_capacity(64);
736 w.u8(match nes.region() {
737 Region::Ntsc => 0,
738 Region::Pal => 1,
739 Region::Dendy => 2,
740 });
741 BoardDescription::capture(nes).write_to(&mut w);
742 HardwareOptions::capture(nes).write_to(&mut w);
743 let mut out = [0u8; 32];
744 out.copy_from_slice(&Sha256::digest(w.into_vec()));
745 out
746}
747
748fn byte(r: &mut BinReader<'_>) -> Result<u8, OptionsDecodeError> {
749 r.u8().map_err(|_| "truncated")
750}
751
752fn flag(r: &mut BinReader<'_>, what: OptionsDecodeError) -> Result<bool, OptionsDecodeError> {
753 match byte(r)? {
754 0 => Ok(false),
755 1 => Ok(true),
756 _ => Err(what),
757 }
758}
759
760const fn mirroring_to_byte(m: Mirroring) -> u8 {
761 match m {
762 Mirroring::Horizontal => 0,
763 Mirroring::Vertical => 1,
764 Mirroring::SingleScreenA => 2,
765 Mirroring::SingleScreenB => 3,
766 Mirroring::FourScreen => 4,
767 Mirroring::MapperControlled => 5,
768 }
769}
770
771const fn mirroring_from_byte(b: u8) -> Option<Mirroring> {
772 Some(match b {
773 0 => Mirroring::Horizontal,
774 1 => Mirroring::Vertical,
775 2 => Mirroring::SingleScreenA,
776 3 => Mirroring::SingleScreenB,
777 4 => Mirroring::FourScreen,
778 5 => Mirroring::MapperControlled,
779 _ => return None,
780 })
781}
782
783const fn vs_ppu_to_byte(t: VsPpuType) -> u8 {
784 match t {
785 VsPpuType::None => 0,
786 VsPpuType::Rp2C03 => 1,
787 VsPpuType::Rp2C04_0001 => 2,
788 VsPpuType::Rp2C04_0002 => 3,
789 VsPpuType::Rp2C04_0003 => 4,
790 VsPpuType::Rp2C04_0004 => 5,
791 VsPpuType::Rc2C05_01 => 6,
792 VsPpuType::Rc2C05_02 => 7,
793 VsPpuType::Rc2C05_03 => 8,
794 VsPpuType::Rc2C05_04 => 9,
795 }
796}
797
798const fn vs_ppu_from_byte(b: u8) -> Option<VsPpuType> {
799 Some(match b {
800 0 => VsPpuType::None,
801 1 => VsPpuType::Rp2C03,
802 2 => VsPpuType::Rp2C04_0001,
803 3 => VsPpuType::Rp2C04_0002,
804 4 => VsPpuType::Rp2C04_0003,
805 5 => VsPpuType::Rp2C04_0004,
806 6 => VsPpuType::Rc2C05_01,
807 7 => VsPpuType::Rc2C05_02,
808 8 => VsPpuType::Rc2C05_03,
809 9 => VsPpuType::Rc2C05_04,
810 _ => return None,
811 })
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 fn synth_nrom() -> Vec<u8> {
820 let mut bytes = alloc::vec![b'N', b'E', b'S', 0x1A, 1, 1, 0, 0];
821 bytes.extend_from_slice(&[0u8; 8]);
822 let mut prg = alloc::vec![0u8; 16 * 1024];
823 prg[..3].copy_from_slice(&[0x4C, 0x00, 0xC0]);
824 let len = prg.len();
825 prg[len - 6..].copy_from_slice(&[0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0]);
826 bytes.extend_from_slice(&prg);
827 bytes.extend_from_slice(&[0u8; 8 * 1024]);
828 bytes
829 }
830
831 fn non_default() -> HardwareOptions {
832 HardwareOptions {
833 console_model: ConsoleModel::Famicom,
834 ppu_revision: PpuRevision::Rp2c02G,
835 cpu_2a03_revision: Cpu2A03Revision::Rp2A03H,
836 oam_decay: true,
837 power_on_ram: PowerOnRam::Seeded(0x0123_4567_89AB_CDEF),
838 power_up_palette: PaletteInit::Blargg,
839 extra_scanlines: 40,
840 cpu_overclock: 3,
841 sprite_limit_disabled: true,
842 mmc3_revision: Some(rustynes_mappers::Mmc3Revision::Nec),
843 four_score: true,
844 zapper_temporal_light: false,
845 vs_dip: 0xA5,
846 vs_ppu_type: Some(VsPpuType::Rc2C05_03),
847 mirroring_override: Some(Mirroring::SingleScreenB),
848 genie_codes: alloc::vec!["AAEAULPA".to_string(), "SXIOPO".to_string()],
850 }
851 }
852
853 #[test]
857 fn genie_codes_decode_to_the_canonical_order_without_duplicates() {
858 let canonical = non_default();
859 for list in [
860 alloc::vec!["SXIOPO", "AAEAULPA"],
861 alloc::vec!["AAEAULPA", "SXIOPO", "SXIOPO"],
862 alloc::vec!["sxiopo", "AAEAULPA"],
863 ] {
864 let opts = HardwareOptions {
865 genie_codes: list.iter().map(|c| (*c).to_string()).collect(),
866 ..non_default()
867 };
868 let decoded = HardwareOptions::read_from(&mut BinReader::new(&opts.to_bytes()));
869 assert_eq!(decoded, Ok(canonical.clone()), "list {list:?}");
870 }
871 }
872
873 #[test]
876 fn an_overclock_above_the_maximum_is_refused() {
877 for (lines, ok) in [
878 (crate::nes::MAX_EXTRA_SCANLINES, true),
879 (crate::nes::MAX_EXTRA_SCANLINES + 1, false),
880 (u16::MAX, false),
881 ] {
882 let opts = HardwareOptions {
883 extra_scanlines: lines,
884 ..HardwareOptions::default()
885 };
886 let decoded = HardwareOptions::read_from(&mut BinReader::new(&opts.to_bytes()));
887 assert_eq!(decoded.is_ok(), ok, "extra_scanlines {lines}");
888 }
889 }
890
891 #[test]
892 fn options_round_trip_through_their_encoding() {
893 for opts in [HardwareOptions::default(), non_default()] {
894 let bytes = opts.to_bytes();
895 let mut r = BinReader::new(&bytes);
896 assert_eq!(HardwareOptions::read_from(&mut r), Ok(opts));
897 assert_eq!(r.remaining(), 0, "the decoder consumes exactly the record");
898 }
899 let filled = HardwareOptions {
900 power_on_ram: PowerOnRam::Filled(0xFF),
901 ..HardwareOptions::default()
902 };
903 let bytes = filled.to_bytes();
904 assert_eq!(
905 HardwareOptions::read_from(&mut BinReader::new(&bytes)),
906 Ok(filled)
907 );
908 }
909
910 #[test]
911 fn every_truncation_is_an_error_not_a_default() {
912 let bytes = non_default().to_bytes();
913 for len in 0..bytes.len() {
914 assert!(
915 HardwareOptions::read_from(&mut BinReader::new(&bytes[..len])).is_err(),
916 "a record cut at {len} bytes must not decode"
917 );
918 }
919 }
920
921 #[test]
922 fn an_unknown_byte_is_refused() {
923 let mut bytes = HardwareOptions::default().to_bytes();
924 bytes[0] = 7; assert_eq!(
926 HardwareOptions::read_from(&mut BinReader::new(&bytes)),
927 Err("unknown console-model byte")
928 );
929 let mut bytes = HardwareOptions::default().to_bytes();
930 bytes[3] = 2; assert!(HardwareOptions::read_from(&mut BinReader::new(&bytes)).is_err());
932 }
933
934 #[test]
935 fn differences_name_each_differing_option() {
936 let a = HardwareOptions::default();
937 assert_eq!(a.differences(&a), [] as [&str; 0]);
938 let b = HardwareOptions {
939 oam_decay: true,
940 console_model: ConsoleModel::Famicom,
941 ..HardwareOptions::default()
942 };
943 assert_eq!(a.differences(&b), ["console model", "OAM decay"]);
944 }
945
946 #[test]
947 fn a_fresh_machine_captures_the_stock_options_and_its_own_vs_type() {
948 let rom = synth_nrom();
949 let nes = Nes::from_rom(&rom).unwrap();
950 let captured = HardwareOptions::capture(&nes);
951 assert_eq!(
952 captured,
953 HardwareOptions {
954 vs_ppu_type: Some(VsPpuType::None),
955 ..HardwareOptions::default()
956 }
957 );
958 }
959
960 #[test]
961 fn board_round_trips_and_names_the_differing_field() {
962 let rom = synth_nrom();
963 let nes = Nes::from_rom(&rom).unwrap();
964 let board = BoardDescription::capture(&nes);
965 let mut w = BinWriter::new();
966 board.write_to(&mut w);
967 let bytes = w.into_vec();
968 assert_eq!(
969 BoardDescription::read_from(&mut BinReader::new(&bytes)),
970 Ok(board)
971 );
972 let other = BoardDescription {
973 submapper: board.submapper + 1,
974 ..board
975 };
976 assert_eq!(board.first_difference(&other), Some("submapper"));
977 assert_eq!(board.first_difference(&board), None);
978 }
979
980 #[test]
981 fn the_config_digest_follows_every_option() {
982 let rom = synth_nrom();
983 let mut nes = Nes::from_rom(&rom).unwrap();
984 let base = config_digest(&nes);
985 assert_eq!(base, config_digest(&Nes::from_rom(&rom).unwrap()));
986 nes.set_oam_decay(true);
987 assert_ne!(config_digest(&nes), base);
988 }
989
990 #[test]
994 fn headers_that_build_different_machines_differ_in_the_description() {
995 let body: Vec<u8> = (0u32..64 * 1024).map(|i| (i % 251) as u8).collect();
998 let image = |prg16: u8, chr8: u8, flags6: u8, mapper: u8| {
999 let mut v = alloc::vec![
1000 b'N',
1001 b'E',
1002 b'S',
1003 0x1A,
1004 prg16,
1005 chr8,
1006 flags6 | (mapper << 4),
1007 mapper & 0xF0
1008 ];
1009 v.extend_from_slice(&[0u8; 8]);
1010 v.extend_from_slice(&body);
1011 v
1012 };
1013 let a = Nes::from_rom(&image(2, 4, 0, 3)).unwrap();
1014 let b = Nes::from_rom(&image(1, 6, 0, 3)).unwrap();
1015 assert_eq!(a.rom_sha256(), b.rom_sha256(), "one identity");
1016 assert_eq!(
1017 BoardDescription::capture(&a).first_difference(&BoardDescription::capture(&b)),
1018 Some("PRG-ROM size")
1019 );
1020 assert_ne!(config_digest(&a), config_digest(&b));
1021
1022 let c = Nes::from_rom(&image(2, 0, 0x08, 218)).unwrap();
1025 let d = Nes::from_rom(&image(2, 0, 0x09, 218)).unwrap();
1026 assert_eq!(
1027 BoardDescription::capture(&c).first_difference(&BoardDescription::capture(&d)),
1028 Some("header nametable wiring")
1029 );
1030 }
1031
1032 #[test]
1033 fn a_wiring_byte_with_other_bits_is_refused() {
1034 let board = BoardDescription::capture(&Nes::from_rom(&synth_nrom()).unwrap());
1035 let mut w = BinWriter::with_capacity(32);
1036 board.write_to(&mut w);
1037 let mut bytes = w.into_vec();
1038 let last = bytes.len() - 1;
1039 bytes[last] = 0x02;
1040 assert!(BoardDescription::read_from(&mut BinReader::new(&bytes)).is_err());
1041 }
1042}