neser 0.1.0

NESER - NES Emulator in Rust - is a NES emulator written in Rust. It aims to be a high-quality, hardware-accurate emulator that is also easy to use and extend. It supports a wide range of NES games and features, including various mappers, audio processing, and input handling. NESER is designed to be modular and extensible, allowing developers to easily add new features or support for additional hardware. It can be run using one of two frontends: a native desktop application using SDL2, or a web application using WebAssembly. The desktop application provides a high-performance, feature-rich experience with support for various input devices and display options, while the web application allows users to play NES games directly in their browsers without needing to install any software in a BYOR manner (Bring Your Own Roms).
Documentation
//! Mapper 330 – unknown bootleg board
//!
//! Specifications:
//! - NesDev wiki: unavailable due to network restriction (Cloudflare 403).
//! - Fallback: Mesen2 `MapperFactory.cpp` – `//330` (not implemented; no board name recorded).
//! - FCEUX: no implementation found.
//! - NES 2.0 database: one known ROM – 三国志 III꞉ 覇王の大陸 (Romance of the Three Kingdoms III
//!   bootleg port).  Header specifies 256 KB PRG-ROM, 256 KB CHR-ROM, 8 KB PRG-NVRAM
//!   (battery-backed), fixed horizontal mirroring.
//!
//! Known Limitations:
//! - No authoritative specification has been found for this board in any publicly
//!   accessible source. This implementation is a minimal stub that allows ROMs to
//!   instantiate without panicking.
//! - PRG and CHR banking behavior is unknown; both are treated as fixed (last 32 KB of
//!   PRG-ROM mapped at $8000–$FFFF; first 8 KB of CHR-ROM mapped at $0000–$1FFF).
//! - Mirroring is fixed horizontal as specified in the NES 2.0 database entry.
//! - IRQ behavior is unknown; no IRQ is generated.
//! - PRG-NVRAM (battery-backed save RAM at $6000–$7FFF) is supported via BaseMapper.

use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};

/// Mapper 330 – unknown bootleg board (minimal stub)
///
/// Hardware: unknown discrete logic board (single known game: Romance of the Three
/// Kingdoms III bootleg NES port)
///
/// This is a minimal stub implementation. The board's specification is not available
/// from any accessible source. The implementation maps the full PRG-ROM and CHR at
/// fixed addresses with no banking, which allows ROMs to instantiate without errors.
///
/// Power-on state: last 32 KB of PRG mapped from $8000–$FFFF; first 8 KB of CHR
/// from $0000–$1FFF; PRG-NVRAM at $6000–$7FFF.
pub struct Mapper330 {
    base: BaseMapper,
}

impl Mapper330 {
    pub fn new(ctx: super::mapper::MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            prg_bank_size_kb: 32,
            chr_bank_size_kb: 8,
            max_prg_ram_kb: 8,
            ..Default::default()
        };
        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(32 * 1024);
        base.configure_chr_banking(8 * 1024);
        let mut mapper = Self { base };
        mapper.update_banks();
        mapper
    }

    fn update_banks(&mut self) {
        let last_bank = self.base.prg_bank_count().saturating_sub(1) as i16;
        self.base.select_prg_page(0, last_bank);
        self.base.select_chr_page(0, 0);
    }
}

impl Mapper for Mapper330 {
    fn base(&self) -> &BaseMapper {
        &self.base
    }

    fn base_mut(&mut self) -> &mut BaseMapper {
        &mut self.base
    }

    fn read_prg(&self, addr: u16) -> u8 {
        match addr {
            0x6000..=0x7FFF => self.base.try_read_prg_ram(addr).unwrap_or(0),
            0x8000..=0xFFFF => self.base.read_prg_rom(addr),
            _ => 0,
        }
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        if (0x6000..=0x7FFF).contains(&addr) {
            self.base.try_write_prg_ram(addr, value);
        }
        // No known write-effect registers for ROM banking on this board.
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cartridge::NametableLayout;
    use crate::cartridge::mapper::{MapperContext, create_mapper};
    use crate::cartridge::test_helpers::banked_data;

    // Non-power-of-two bank counts to prevent false-pass modulo wrapping.
    const PRG_BANKS: usize = 5; // 5 × 32 KB = 160 KB
    const CHR_BANKS: usize = 7; // 7 × 8 KB = 56 KB

    fn make_mapper() -> Mapper330 {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        Mapper330::new(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        )
    }

    // --- Registration ---

    #[test]
    fn mapper_330_is_registered() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        let result = create_mapper(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        );
        assert!(
            result.is_ok(),
            "Mapper 330 must be registered in the factory"
        );
    }

    // --- Power-on PRG state: last bank fixed at $8000-$FFFF ---

    #[test]
    fn power_on_prg_8000_reads_last_bank_first_byte() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        // Last bank marker is PRG_BANKS - 1
        let expected = prg[(PRG_BANKS - 1) * 32 * 1024];
        let mapper = Mapper330::new(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        );
        assert_eq!(
            mapper.read_prg(0x8000),
            expected,
            "$8000 must map to last PRG bank at power-on"
        );
    }

    #[test]
    fn power_on_prg_ffff_reads_last_bank_last_byte() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        // Last byte of last bank
        let expected = prg[PRG_BANKS * 32 * 1024 - 1];
        let mapper = Mapper330::new(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        );
        assert_eq!(
            mapper.read_prg(0xFFFF),
            expected,
            "$FFFF must read last byte of last PRG bank"
        );
    }

    // --- Power-on CHR state: bank 0 at $0000-$1FFF ---

    #[test]
    fn power_on_chr_0000_reads_bank_0() {
        let mut mapper = make_mapper();
        assert_eq!(
            mapper.read_chr(0x0000),
            0,
            "$0000 must map to CHR bank 0 at power-on"
        );
    }

    #[test]
    fn power_on_chr_1fff_reads_bank_0_last_byte() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        let expected = chr[0x1FFF];
        let mut mapper = Mapper330::new(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        );
        assert_eq!(
            mapper.read_chr(0x1FFF),
            expected,
            "$1FFF must read last byte of CHR bank 0"
        );
    }

    // --- PRG-NVRAM at $6000-$7FFF ---

    #[test]
    fn prg_nvram_read_write_at_6000() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x6000, 0xAB);
        assert_eq!(
            mapper.read_prg(0x6000),
            0xAB,
            "$6000 PRG-NVRAM must be readable after write"
        );
    }

    #[test]
    fn prg_nvram_read_write_at_7fff() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x7FFF, 0xCD);
        assert_eq!(
            mapper.read_prg(0x7FFF),
            0xCD,
            "$7FFF PRG-NVRAM must be readable after write"
        );
    }

    // --- ROM banking is fixed (writes to $8000-$FFFF have no effect) ---

    #[test]
    fn writes_to_prg_rom_space_do_not_change_prg() {
        let mut mapper = make_mapper();
        let initial = mapper.read_prg(0x8000);
        mapper.write_prg(0x8000, 0xFF);
        mapper.write_prg(0xFFFF, 0xFF);
        assert_eq!(
            mapper.read_prg(0x8000),
            initial,
            "PRG must remain at last bank after writes to ROM space"
        );
    }

    // --- Mirroring: fixed horizontal ---

    #[test]
    fn mirroring_is_horizontal() {
        let mapper = make_mapper();
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "Mirroring must be fixed horizontal"
        );
    }

    #[test]
    fn mirroring_not_changed_by_writes() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0xFF);
        assert_eq!(
            mapper.get_mirroring(),
            NametableLayout::Horizontal,
            "Mirroring must not change after writes"
        );
    }

    // --- No IRQ ---

    #[test]
    fn irq_never_pending() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x8000, 0xFF);
        assert!(!mapper.irq_pending(), "Mapper 330 must never assert IRQ");
    }

    // --- Snapshot / restore (no register state) ---

    #[test]
    fn registers_snapshot_is_empty() {
        let mapper = make_mapper();
        assert!(
            mapper.registers_snapshot().is_empty(),
            "Mapper 330 has no registers; snapshot must be empty"
        );
    }

    #[test]
    fn restore_registers_with_empty_data_is_noop() {
        let mut mapper = make_mapper();
        let expected = mapper.read_prg(0x8000);
        mapper.restore_registers(&[]);
        assert_eq!(mapper.read_prg(0x8000), expected);
    }

    // --- Reset (no effect on fixed mapper) ---

    #[test]
    fn reset_leaves_prg_at_last_bank() {
        let prg = banked_data(32 * 1024, PRG_BANKS);
        let chr = banked_data(8 * 1024, CHR_BANKS);
        let expected = prg[(PRG_BANKS - 1) * 32 * 1024];
        let mut mapper = Mapper330::new(
            MapperContext::new_for_test(330, prg, chr, NametableLayout::Horizontal)
                .with_prg_ram_banks(1),
        );
        mapper.reset();
        assert_eq!(
            mapper.read_prg(0x8000),
            expected,
            "PRG must remain at last bank after reset"
        );
    }
}