use crate::frontends::native::app_state::NativeAppState;
use crate::nes::input::{Button, PowerPadButton, SnesButton};
use crate::platform::audio::EmulatorAudio;
use crate::platform::emulator::Console;
use winit::keyboard::KeyCode;
#[derive(Debug, PartialEq, Eq)]
pub enum KeyOutcome {
Quit,
Continue,
CycleShader,
ToggleDebugger,
StepOver,
StepInto,
SwitchCartridge(String),
OpenCartridgeSwitch,
CloseCartridgeSwitch,
ToggleFps,
}
pub fn handle_key_pressed(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
) -> KeyOutcome {
match console {
Console::Nes(_) => {
if app_state.cart_switch.open {
return handle_cartridge_switch_key(key_code, app_state);
}
if app_state.modifiers.control_key() {
return handle_ctrl_hotkey(console, key_code, app_state);
}
handle_unmodified_key(console, key_code, app_state, audio)
}
Console::GameBoy(_) => handle_gameboy_key_pressed(console, key_code, app_state, audio),
Console::GameBoyAdvance(_) => handle_gba_key_pressed(console, key_code, app_state, audio),
}
}
pub fn handle_key_released(
console: &mut Console,
key_code: KeyCode,
gamepad_count: usize,
four_score: bool,
) {
match console {
Console::Nes(_) => {
let ports = keyboard_target_ports(gamepad_count, four_score);
handle_controller_key(console, key_code, false, ports);
}
Console::GameBoy(_) => {
if let Some(btn_id) = gameboy_key_to_button_id(key_code) {
console.set_button(0, btn_id, false);
}
}
Console::GameBoyAdvance(_) => {
if let Some(btn_id) = gba_key_to_button_id(key_code) {
console.set_button(0, btn_id, false);
}
}
}
}
pub fn keyboard_target_ports(gamepad_count: usize, four_score: bool) -> &'static [u8] {
if four_score {
match gamepad_count {
0 => &[1, 2],
1 => &[2, 3],
2 => &[3, 4],
3 => &[4],
_ => &[],
}
} else {
match gamepad_count {
0 => &[1, 2],
1 => &[2],
_ => &[],
}
}
}
fn gameboy_key_to_button_id(key_code: KeyCode) -> Option<u8> {
use Button::{A, B, Down, Left, Right, Select, Start, Up};
match key_code {
KeyCode::KeyT => Some(A as u8),
KeyCode::KeyR => Some(B as u8),
KeyCode::Digit4 => Some(Select as u8),
KeyCode::Digit5 => Some(Start as u8),
KeyCode::KeyW | KeyCode::ArrowUp => Some(Up as u8),
KeyCode::KeyS | KeyCode::ArrowDown => Some(Down as u8),
KeyCode::KeyA | KeyCode::ArrowLeft => Some(Left as u8),
KeyCode::KeyD | KeyCode::ArrowRight => Some(Right as u8),
_ => None,
}
}
fn gba_key_to_button_id(key_code: KeyCode) -> Option<u8> {
match key_code {
KeyCode::KeyQ => Some(8), KeyCode::KeyE => Some(9), _ => gameboy_key_to_button_id(key_code),
}
}
fn handle_common_hotkey(
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
) -> Option<KeyOutcome> {
match key_code {
KeyCode::Escape => {
app_state.mouse_grabbed = false;
app_state.mouse_released_by_escape = true;
Some(KeyOutcome::Continue)
}
KeyCode::Space => {
app_state.paused = !app_state.paused;
if let Some(audio) = audio {
if app_state.paused {
audio.pause();
} else {
audio.resume();
}
}
Some(KeyOutcome::Continue)
}
KeyCode::F1 => Some(KeyOutcome::ToggleFps),
KeyCode::F4 => Some(KeyOutcome::CycleShader),
KeyCode::F2 => {
adjust_volume(audio, 0.1);
Some(KeyOutcome::Continue)
}
KeyCode::F3 => {
adjust_volume(audio, -0.1);
Some(KeyOutcome::Continue)
}
_ => None,
}
}
fn handle_gameboy_key_pressed(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
) -> KeyOutcome {
handle_single_joypad_key_pressed(
console,
key_code,
app_state,
audio,
gameboy_key_to_button_id,
)
}
fn handle_gba_key_pressed(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
) -> KeyOutcome {
handle_single_joypad_key_pressed(console, key_code, app_state, audio, gba_key_to_button_id)
}
fn handle_single_joypad_key_pressed(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
key_to_button_id: fn(KeyCode) -> Option<u8>,
) -> KeyOutcome {
if app_state.modifiers.control_key() {
return match key_code {
KeyCode::KeyQ => KeyOutcome::Quit,
KeyCode::KeyR => {
console.reset(!app_state.modifiers.shift_key());
KeyOutcome::Continue
}
KeyCode::KeyF => {
app_state.fullscreen = !app_state.fullscreen;
KeyOutcome::Continue
}
_ => KeyOutcome::Continue,
};
}
if let Some(outcome) = handle_common_hotkey(key_code, app_state, audio) {
return outcome;
}
match key_code {
KeyCode::KeyH => app_state.help_overlay_visible = !app_state.help_overlay_visible,
KeyCode::F5 => return KeyOutcome::ToggleDebugger,
KeyCode::F6 => {
crate::nes::console::save_state_io::save_state_to_disk(console);
}
KeyCode::F7 => {
crate::nes::console::save_state_io::load_state_from_disk(console);
if let Some(audio) = audio {
audio.drain_buffer();
}
}
KeyCode::F10 => return KeyOutcome::StepOver,
KeyCode::F11 => return KeyOutcome::StepInto,
_ => {
if let Some(btn_id) = key_to_button_id(key_code) {
console.set_button(0, btn_id, true);
}
}
}
KeyOutcome::Continue
}
fn handle_ctrl_hotkey(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
) -> KeyOutcome {
match key_code {
KeyCode::KeyQ => KeyOutcome::Quit,
KeyCode::KeyR => {
console.reset(!app_state.modifiers.shift_key());
KeyOutcome::Continue
}
KeyCode::KeyO => KeyOutcome::OpenCartridgeSwitch,
KeyCode::KeyF => {
app_state.fullscreen = !app_state.fullscreen;
KeyOutcome::Continue
}
_ => KeyOutcome::Continue,
}
}
fn handle_unmodified_key(
console: &mut Console,
key_code: KeyCode,
app_state: &mut NativeAppState,
audio: Option<&dyn EmulatorAudio>,
) -> KeyOutcome {
if let Some(outcome) = handle_common_hotkey(key_code, app_state, audio) {
return outcome;
}
match key_code {
KeyCode::KeyH => app_state.help_overlay_visible = !app_state.help_overlay_visible,
KeyCode::F5 => return KeyOutcome::ToggleDebugger,
KeyCode::F6 => {
crate::nes::console::save_state_io::save_state_to_disk(console);
}
KeyCode::F7 => {
crate::nes::console::save_state_io::load_state_from_disk(console);
if let Some(audio) = audio {
audio.drain_buffer();
}
}
KeyCode::F10 => return KeyOutcome::StepOver,
KeyCode::F11 => return KeyOutcome::StepInto,
_ => {
let ports =
keyboard_target_ports(app_state.gamepad_count, app_state.four_score_enabled);
handle_controller_key(console, key_code, true, ports);
}
}
KeyOutcome::Continue
}
fn adjust_volume(audio: Option<&dyn EmulatorAudio>, delta: f32) {
if let Some(audio) = audio {
audio.set_volume(audio.get_volume() + delta);
}
}
fn handle_controller_key(console: &mut Console, key_code: KeyCode, pressed: bool, ports: &[u8]) {
let Console::Nes(nes) = console else {
return;
};
match key_code {
KeyCode::Digit1 => pp_p1(nes, PowerPadButton::One, pressed, ports),
KeyCode::Digit2 => pp_p1(nes, PowerPadButton::Two, pressed, ports),
KeyCode::Digit3 => pp_p1(nes, PowerPadButton::Three, pressed, ports),
KeyCode::KeyQ => pp_or_snes_p1(nes, PowerPadButton::Four, SnesButton::L, pressed, ports),
KeyCode::KeyW => pp_or_btn_or_snes_p1(
nes,
PowerPadButton::Five,
Button::Up,
SnesButton::Up,
pressed,
ports,
),
KeyCode::KeyE => pp_or_snes_p1(nes, PowerPadButton::Six, SnesButton::R, pressed, ports),
KeyCode::KeyA => pp_or_btn_or_snes_p1(
nes,
PowerPadButton::Seven,
Button::Left,
SnesButton::Left,
pressed,
ports,
),
KeyCode::KeyS => pp_or_btn_or_snes_p1(
nes,
PowerPadButton::Eight,
Button::Down,
SnesButton::Down,
pressed,
ports,
),
KeyCode::KeyD => pp_or_btn_or_snes_p1(
nes,
PowerPadButton::Nine,
Button::Right,
SnesButton::Right,
pressed,
ports,
),
KeyCode::KeyZ => pp_p1(nes, PowerPadButton::Ten, pressed, ports),
KeyCode::KeyX => pp_p1(nes, PowerPadButton::Eleven, pressed, ports),
KeyCode::KeyC => pp_p1(nes, PowerPadButton::Twelve, pressed, ports),
KeyCode::KeyT => btn_or_snes_p1(nes, Button::A, SnesButton::Y, pressed, ports),
KeyCode::KeyR => btn_or_snes_p1(nes, Button::B, SnesButton::X, pressed, ports),
KeyCode::KeyF => snes_p1(nes, SnesButton::B, pressed, ports),
KeyCode::KeyG => snes_p1(nes, SnesButton::A, pressed, ports),
KeyCode::Digit4 => btn_or_snes_p1(nes, Button::Select, SnesButton::Select, pressed, ports),
KeyCode::Digit5 => btn_or_snes_p1(nes, Button::Start, SnesButton::Start, pressed, ports),
KeyCode::Digit7 => pp_p2(nes, PowerPadButton::One, pressed, ports),
KeyCode::Digit8 => pp_p2(nes, PowerPadButton::Two, pressed, ports),
KeyCode::Digit9 => pp_or_btn_p2(nes, PowerPadButton::Three, Button::Select, pressed, ports),
KeyCode::Digit0 => btn_p2(nes, Button::Start, pressed, ports),
KeyCode::KeyU => pp_p2(nes, PowerPadButton::Four, pressed, ports),
KeyCode::KeyI => pp_or_btn_p2(nes, PowerPadButton::Five, Button::Up, pressed, ports),
KeyCode::KeyO => pp_or_btn_p2(nes, PowerPadButton::Six, Button::A, pressed, ports),
KeyCode::KeyJ => pp_or_btn_p2(nes, PowerPadButton::Seven, Button::Left, pressed, ports),
KeyCode::KeyK => pp_or_btn_p2(nes, PowerPadButton::Eight, Button::Down, pressed, ports),
KeyCode::KeyL => pp_or_btn_p2(nes, PowerPadButton::Nine, Button::Right, pressed, ports),
KeyCode::KeyM => pp_p2(nes, PowerPadButton::Ten, pressed, ports),
KeyCode::Comma => pp_p2(nes, PowerPadButton::Eleven, pressed, ports),
KeyCode::Period => pp_p2(nes, PowerPadButton::Twelve, pressed, ports),
KeyCode::KeyP => btn_p2(nes, Button::B, pressed, ports),
KeyCode::Digit6 => nes.set_vs_coin_insert(0, pressed),
KeyCode::Minus => nes.set_vs_service_button(pressed),
_ => {}
}
}
fn pp_p1(nes: &mut crate::nes::console::Nes, pp: PowerPadButton, pressed: bool, ports: &[u8]) {
if let Some(&port) = ports.first() {
nes.set_power_pad_button(port, pp, pressed);
}
}
fn snes_p1(nes: &mut crate::nes::console::Nes, snes: SnesButton, pressed: bool, ports: &[u8]) {
if let Some(&port) = ports.first() {
nes.set_snes_button(port, snes, pressed);
}
}
fn btn_or_snes_p1(
nes: &mut crate::nes::console::Nes,
btn: Button,
snes: SnesButton,
pressed: bool,
ports: &[u8],
) {
if let Some(&port) = ports.first()
&& !nes.set_snes_button(port, snes, pressed)
{
nes.set_button(port, btn, pressed);
}
}
fn pp_or_snes_p1(
nes: &mut crate::nes::console::Nes,
pp: PowerPadButton,
snes: SnesButton,
pressed: bool,
ports: &[u8],
) {
if let Some(&port) = ports.first()
&& !nes.set_power_pad_button(port, pp, pressed)
{
nes.set_snes_button(port, snes, pressed);
}
}
fn pp_or_btn_or_snes_p1(
nes: &mut crate::nes::console::Nes,
pp: PowerPadButton,
btn: Button,
snes: SnesButton,
pressed: bool,
ports: &[u8],
) {
if let Some(&port) = ports.first()
&& !nes.set_power_pad_button(port, pp, pressed)
&& !nes.set_snes_button(port, snes, pressed)
{
nes.set_button(port, btn, pressed);
}
}
fn btn_p2(nes: &mut crate::nes::console::Nes, btn: Button, pressed: bool, ports: &[u8]) {
if let Some(&port) = ports.get(1) {
nes.set_button(port, btn, pressed);
}
}
fn pp_p2(nes: &mut crate::nes::console::Nes, pp: PowerPadButton, pressed: bool, ports: &[u8]) {
if let Some(&port) = ports.get(1) {
nes.set_power_pad_button(port, pp, pressed);
}
}
fn pp_or_btn_p2(
nes: &mut crate::nes::console::Nes,
pp: PowerPadButton,
btn: Button,
pressed: bool,
ports: &[u8],
) {
if let Some(&port) = ports.get(1)
&& !nes.set_power_pad_button(port, pp, pressed)
{
nes.set_button(port, btn, pressed);
}
}
fn handle_cartridge_switch_key(key_code: KeyCode, app_state: &mut NativeAppState) -> KeyOutcome {
match key_code {
KeyCode::Escape => {
app_state.cart_switch.close();
KeyOutcome::CloseCartridgeSwitch
}
KeyCode::ArrowDown => {
app_state.cart_switch.move_selection_next();
KeyOutcome::Continue
}
KeyCode::ArrowUp => {
app_state.cart_switch.move_selection_prev();
KeyOutcome::Continue
}
KeyCode::Backspace => {
app_state.cart_switch.filter.pop();
app_state.cart_switch.refresh_filtered();
KeyOutcome::Continue
}
KeyCode::Enter | KeyCode::NumpadEnter => {
if let Some(path) = app_state.cart_switch.selected_entry().map(str::to_string) {
app_state.cart_switch.close();
KeyOutcome::SwitchCartridge(path)
} else {
KeyOutcome::Continue
}
}
_ => {
if let Some(ch) = key_code_to_filter_char(key_code, app_state.modifiers.shift_key()) {
app_state.cart_switch.filter.push(ch);
app_state.cart_switch.refresh_filtered();
}
KeyOutcome::Continue
}
}
}
fn key_code_to_filter_char(key_code: KeyCode, shift: bool) -> Option<char> {
match key_code {
KeyCode::KeyA => Some('a'),
KeyCode::KeyB => Some('b'),
KeyCode::KeyC => Some('c'),
KeyCode::KeyD => Some('d'),
KeyCode::KeyE => Some('e'),
KeyCode::KeyF => Some('f'),
KeyCode::KeyG => Some('g'),
KeyCode::KeyH => Some('h'),
KeyCode::KeyI => Some('i'),
KeyCode::KeyJ => Some('j'),
KeyCode::KeyK => Some('k'),
KeyCode::KeyL => Some('l'),
KeyCode::KeyM => Some('m'),
KeyCode::KeyN => Some('n'),
KeyCode::KeyO => Some('o'),
KeyCode::KeyP => Some('p'),
KeyCode::KeyQ => Some('q'),
KeyCode::KeyR => Some('r'),
KeyCode::KeyS => Some('s'),
KeyCode::KeyT => Some('t'),
KeyCode::KeyU => Some('u'),
KeyCode::KeyV => Some('v'),
KeyCode::KeyW => Some('w'),
KeyCode::KeyX => Some('x'),
KeyCode::KeyY => Some('y'),
KeyCode::KeyZ => Some('z'),
KeyCode::Digit0 => Some('0'),
KeyCode::Digit1 => Some('1'),
KeyCode::Digit2 => Some('2'),
KeyCode::Digit3 => Some('3'),
KeyCode::Digit4 => Some('4'),
KeyCode::Digit5 => Some('5'),
KeyCode::Digit6 => Some('6'),
KeyCode::Digit7 => Some('7'),
KeyCode::Digit8 => Some('8'),
KeyCode::Digit9 => Some('9'),
KeyCode::Space => Some(' '),
KeyCode::Minus => Some(if shift { '_' } else { '-' }),
KeyCode::Period => Some('.'),
KeyCode::Slash => Some('/'),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::console::{Config, Nes, NesConfig};
use crate::platform::app_context::AppContext;
use crate::platform::emulator::Console;
use winit::keyboard::ModifiersState;
fn make_nes() -> Nes {
Nes::new(AppContext::new_with_config(Config::default()))
}
fn make_nes_four_score() -> Nes {
Nes::new(AppContext::new_with_config(Config {
nes: NesConfig {
four_score_enabled: true,
..Default::default()
},
..Config::default()
}))
}
fn make_nes_with_cartridge() -> Nes {
let mut nes = make_nes();
let mut prg_rom = vec![0xEAu8; 0x8000]; prg_rom[0x7FFC] = 0x00;
prg_rom[0x7FFD] = 0x80;
prg_rom[0x7FFA] = 0x00;
prg_rom[0x7FFB] = 0x80;
prg_rom[0x7FFE] = 0x00;
prg_rom[0x7FFF] = 0x80;
let cartridge = crate::nes::cartridge::Cartridge::from_parts(
prg_rom,
vec![],
crate::nes::cartridge::NametableLayout::Horizontal,
);
nes.insert_cartridge(cartridge);
nes
}
fn make_nes_console() -> Console {
Console::Nes(Box::new(make_nes()))
}
fn make_nes_console_with_cart() -> Console {
Console::Nes(Box::new(make_nes_with_cartridge()))
}
fn make_nes_console_four_score() -> Console {
Console::Nes(Box::new(make_nes_four_score()))
}
fn make_gba_console() -> Console {
Console::new_gba(AppContext::new_with_config(Config::default()))
}
fn gba_keyinput(console: &Console) -> u16 {
let Console::GameBoyAdvance(gba) = console else {
panic!("expected GBA console");
};
gba.bus().keypad.read_keyinput()
}
fn make_state() -> NativeAppState {
NativeAppState::default()
}
fn with_ctrl(state: &mut NativeAppState) {
state.modifiers = ModifiersState::CONTROL;
}
fn with_ctrl_shift(state: &mut NativeAppState) {
state.modifiers = ModifiersState::CONTROL | ModifiersState::SHIFT;
}
#[allow(dead_code)]
fn buttons(nes: &Nes, port: u8) -> u8 {
nes.get_joypad_button_states(port)
}
const BIT_A: u8 = 1 << Button::A as u8;
const BIT_B: u8 = 1 << Button::B as u8;
const BIT_SELECT: u8 = 1 << Button::Select as u8;
const BIT_START: u8 = 1 << Button::Start as u8;
const BIT_UP: u8 = 1 << Button::Up as u8;
const BIT_DOWN: u8 = 1 << Button::Down as u8;
const BIT_LEFT: u8 = 1 << Button::Left as u8;
const BIT_RIGHT: u8 = 1 << Button::Right as u8;
const GBA_KEY_A: u16 = 1 << 0;
const GBA_KEY_B: u16 = 1 << 1;
const GBA_KEY_SELECT: u16 = 1 << 2;
const GBA_KEY_START: u16 = 1 << 3;
const GBA_KEY_RIGHT: u16 = 1 << 4;
const GBA_KEY_LEFT: u16 = 1 << 5;
const GBA_KEY_UP: u16 = 1 << 6;
const GBA_KEY_DOWN: u16 = 1 << 7;
const GBA_KEY_R: u16 = 1 << 8;
const GBA_KEY_L: u16 = 1 << 9;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
struct MockAudio {
volume: Arc<AtomicU32>,
}
struct TrackingMockAudio {
pause_called: Arc<AtomicBool>,
resume_called: Arc<AtomicBool>,
drain_buffer_called: Arc<AtomicBool>,
}
impl TrackingMockAudio {
fn new() -> (Self, Arc<AtomicBool>, Arc<AtomicBool>, Arc<AtomicBool>) {
let pause_called = Arc::new(AtomicBool::new(false));
let resume_called = Arc::new(AtomicBool::new(false));
let drain_buffer_called = Arc::new(AtomicBool::new(false));
let audio = Self {
pause_called: Arc::clone(&pause_called),
resume_called: Arc::clone(&resume_called),
drain_buffer_called: Arc::clone(&drain_buffer_called),
};
(audio, pause_called, resume_called, drain_buffer_called)
}
}
impl EmulatorAudio for TrackingMockAudio {
fn queue_sample(&mut self, _sample: f32) {}
fn resume(&self) {
self.resume_called.store(true, Ordering::Relaxed);
}
fn pause(&self) {
self.pause_called.store(true, Ordering::Relaxed);
}
fn drain_buffer(&self) {
self.drain_buffer_called.store(true, Ordering::Relaxed);
}
fn set_volume(&self, _volume: f32) {}
fn get_volume(&self) -> f32 {
0.0
}
fn prime_startup(&mut self, _samples: usize) {}
fn take_and_reset_stats(&self) -> (u64, u64, u64) {
(0, 0, 0)
}
fn actual_sample_rate(&self) -> i32 {
44100
}
}
impl MockAudio {
fn new_with_volume(vol: f32) -> Self {
Self {
volume: Arc::new(AtomicU32::new(f32::to_bits(vol))),
}
}
}
impl EmulatorAudio for MockAudio {
fn queue_sample(&mut self, _sample: f32) {}
fn resume(&self) {}
fn pause(&self) {}
fn set_volume(&self, volume: f32) {
self.volume
.store(f32::to_bits(volume.clamp(0.0, 1.0)), Ordering::Relaxed);
}
fn get_volume(&self) -> f32 {
f32::from_bits(self.volume.load(Ordering::Relaxed))
}
fn prime_startup(&mut self, _samples: usize) {}
fn take_and_reset_stats(&self) -> (u64, u64, u64) {
(0, 0, 0)
}
fn actual_sample_rate(&self) -> i32 {
44100
}
}
#[test]
fn test_ctrl_q_returns_quit() {
let mut console = make_nes_console();
let mut state = make_state();
with_ctrl(&mut state);
assert_eq!(
handle_key_pressed(&mut console, KeyCode::KeyQ, &mut state, None),
KeyOutcome::Quit
);
}
#[test]
fn test_escape_returns_continue_when_mouse_not_grabbed() {
let mut console = make_nes_console();
let mut state = make_state();
state.mouse_grabbed = false;
assert_eq!(
handle_key_pressed(&mut console, KeyCode::Escape, &mut state, None),
KeyOutcome::Continue
);
}
#[test]
fn test_escape_releases_mouse_when_grabbed() {
let mut console = make_nes_console();
let mut state = make_state();
state.mouse_grabbed = true;
handle_key_pressed(&mut console, KeyCode::Escape, &mut state, None);
assert!(!state.mouse_grabbed, "Escape should release mouse grab");
assert!(
state.mouse_released_by_escape,
"Escape should set mouse_released_by_escape"
);
}
#[test]
fn test_space_toggles_pause_on() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::Space, &mut state, None);
assert!(state.paused, "Space should pause when unpaused");
}
#[test]
fn test_space_toggles_pause_off() {
let mut console = make_nes_console();
let mut state = make_state();
state.paused = true;
handle_key_pressed(&mut console, KeyCode::Space, &mut state, None);
assert!(!state.paused, "Space should unpause when paused");
}
#[test]
fn test_space_calls_audio_pause_when_pausing() {
let mut console = make_nes_console();
let mut state = make_state();
state.paused = false;
let (audio, pause_called, _resume_called, _drain_buffer_called) = TrackingMockAudio::new();
handle_key_pressed(
&mut console,
KeyCode::Space,
&mut state,
Some(&audio as &dyn EmulatorAudio),
);
assert!(
pause_called.load(Ordering::Relaxed),
"Space (pause) should call audio.pause() to prevent ring buffer drain"
);
}
#[test]
fn test_space_calls_audio_resume_when_resuming() {
let mut console = make_nes_console();
let mut state = make_state();
state.paused = true;
let (audio, _pause_called, resume_called, _drain_buffer_called) = TrackingMockAudio::new();
handle_key_pressed(
&mut console,
KeyCode::Space,
&mut state,
Some(&audio as &dyn EmulatorAudio),
);
assert!(
resume_called.load(Ordering::Relaxed),
"Space (resume) should call audio.resume() to restore audio after pause"
);
}
#[test]
fn test_h_toggles_help_overlay_on() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyH, &mut state, None);
assert!(state.help_overlay_visible);
}
#[test]
fn test_h_toggles_help_overlay_off() {
let mut console = make_nes_console();
let mut state = make_state();
state.help_overlay_visible = true;
handle_key_pressed(&mut console, KeyCode::KeyH, &mut state, None);
assert!(!state.help_overlay_visible);
}
#[test]
fn test_ctrl_f_toggles_fullscreen_on() {
let mut console = make_nes_console();
let mut state = make_state();
with_ctrl(&mut state);
handle_key_pressed(&mut console, KeyCode::KeyF, &mut state, None);
assert!(state.fullscreen);
}
#[test]
fn test_ctrl_f_toggles_fullscreen_off() {
let mut console = make_nes_console();
let mut state = make_state();
state.fullscreen = true;
with_ctrl(&mut state);
handle_key_pressed(&mut console, KeyCode::KeyF, &mut state, None);
assert!(!state.fullscreen);
}
#[test]
fn test_alt_f_does_not_toggle_fullscreen() {
let mut console = make_nes_console();
let mut state = make_state();
state.modifiers = ModifiersState::ALT;
handle_key_pressed(&mut console, KeyCode::KeyF, &mut state, None);
assert!(!state.fullscreen, "Alt+F should not toggle fullscreen");
}
#[test]
fn test_f4_returns_cycle_shader() {
let mut console = make_nes_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F4, &mut state, None),
KeyOutcome::CycleShader
);
}
#[test]
fn test_f2_increases_volume() {
let mut console = make_nes_console();
let mut state = make_state();
let audio = MockAudio::new_with_volume(0.5);
handle_key_pressed(&mut console, KeyCode::F2, &mut state, Some(&audio));
let vol = audio.get_volume();
assert!(
(vol - 0.6).abs() < 1e-5,
"F2 should raise volume by 0.1 (got {vol})"
);
}
#[test]
fn test_f3_decreases_volume() {
let mut console = make_nes_console();
let mut state = make_state();
let audio = MockAudio::new_with_volume(0.5);
handle_key_pressed(&mut console, KeyCode::F3, &mut state, Some(&audio));
let vol = audio.get_volume();
assert!(
(vol - 0.4).abs() < 1e-5,
"F3 should lower volume by 0.1 (got {vol})"
);
}
#[test]
fn test_f5_returns_toggle_debugger() {
let mut console = make_nes_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F5, &mut state, None),
KeyOutcome::ToggleDebugger
);
}
#[test]
fn test_f10_returns_step_over() {
let mut console = make_nes_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F10, &mut state, None),
KeyOutcome::StepOver
);
}
#[test]
fn test_f11_returns_step_into() {
let mut console = make_nes_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F11, &mut state, None),
KeyOutcome::StepInto
);
}
#[test]
fn test_ctrl_r_soft_resets() {
let mut console = make_nes_console_with_cart();
let mut state = make_state();
with_ctrl(&mut state);
assert_eq!(
handle_key_pressed(&mut console, KeyCode::KeyR, &mut state, None),
KeyOutcome::Continue
);
}
#[test]
fn test_ctrl_shift_r_hard_resets() {
let mut console = make_nes_console_with_cart();
let mut state = make_state();
with_ctrl_shift(&mut state);
assert_eq!(
handle_key_pressed(&mut console, KeyCode::KeyR, &mut state, None),
KeyOutcome::Continue
);
}
#[test]
fn test_p1_w_sets_up() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should set Up on P1"
);
}
#[test]
fn test_p1_a_sets_left() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyA, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_LEFT,
0,
"A should set Left on P1"
);
}
#[test]
fn test_p1_s_sets_down() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyS, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_DOWN,
0,
"S should set Down on P1"
);
}
#[test]
fn test_p1_d_sets_right() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyD, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_RIGHT,
0,
"D should set Right on P1"
);
}
#[test]
fn test_p1_t_sets_a() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyT, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_A,
0,
"T should set A on P1"
);
}
#[test]
fn test_p1_r_sets_b() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyR, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_B,
0,
"R should set B on P1"
);
}
#[test]
fn test_p1_num4_sets_select() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::Digit4, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_SELECT,
0,
"4 should set Select on P1"
);
}
#[test]
fn test_p1_num5_sets_start() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::Digit5, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_START,
0,
"5 should set Start on P1"
);
}
#[test]
fn test_p1_w_released_clears_up() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(console.get_joypad_button_states(1) & BIT_UP, 0);
handle_key_released(&mut console, KeyCode::KeyW, 0, false);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"Releasing W should clear Up"
);
}
#[test]
fn test_p1_t_released_clears_a() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyT, &mut state, None);
handle_key_released(&mut console, KeyCode::KeyT, 0, false);
assert_eq!(
console.get_joypad_button_states(1) & BIT_A,
0,
"Releasing T should clear A"
);
}
#[test]
fn test_p2_i_sets_up() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyI, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"I should set Up on P2"
);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"I should not affect P1"
);
}
#[test]
fn test_p2_j_sets_left() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyJ, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_LEFT,
0,
"J should set Left on P2"
);
}
#[test]
fn test_p2_k_sets_down() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyK, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_DOWN,
0,
"K should set Down on P2"
);
}
#[test]
fn test_p2_l_sets_right() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyL, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_RIGHT,
0,
"L should set Right on P2"
);
}
#[test]
fn test_p2_o_sets_a() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyO, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_A,
0,
"O should set A on P2"
);
}
#[test]
fn test_p2_p_sets_b() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyP, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_B,
0,
"P should set B on P2"
);
}
#[test]
fn test_p2_num9_sets_select() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::Digit9, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_SELECT,
0,
"9 should set Select on P2"
);
}
#[test]
fn test_p2_num0_sets_start() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::Digit0, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_START,
0,
"0 should set Start on P2"
);
}
#[test]
fn test_w_targets_port1_only_when_no_gamepad() {
let mut console = make_nes_console();
let mut state = make_state(); handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should set Up on P1"
);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"W should NOT set Up on P2 (port 2 has dedicated IJKL keys)"
);
}
#[test]
fn test_s_targets_port1_only_when_no_gamepad() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyS, &mut state, None);
assert_ne!(
console.get_joypad_button_states(1) & BIT_DOWN,
0,
"S should set Down on P1"
);
assert_eq!(
console.get_joypad_button_states(2) & BIT_DOWN,
0,
"S should NOT set Down on P2"
);
}
#[test]
fn test_p2_i_released_clears_up() {
let mut console = make_nes_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyI, &mut state, None);
handle_key_released(&mut console, KeyCode::KeyI, 0, false);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"Releasing I should clear Up on P2"
);
}
fn make_cart_switch_state(entries: &[&str]) -> NativeAppState {
let mut state = make_state();
state.cart_switch.open = true;
state.cart_switch.entries = entries.iter().map(|s| s.to_string()).collect();
state
}
#[test]
fn test_cart_switch_typing_adds_to_filter() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["alpha.nes", "beta.nes"]);
handle_key_pressed(&mut console, KeyCode::KeyA, &mut state, None);
assert_eq!(state.cart_switch.filter, "a");
handle_key_pressed(&mut console, KeyCode::KeyB, &mut state, None);
assert_eq!(state.cart_switch.filter, "ab");
}
#[test]
fn test_cart_switch_backspace_removes_char() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes"]);
state.cart_switch.filter = "abc".to_string();
let outcome = handle_key_pressed(&mut console, KeyCode::Backspace, &mut state, None);
assert_eq!(state.cart_switch.filter, "ab");
assert_eq!(outcome, KeyOutcome::Continue);
}
#[test]
fn test_cart_switch_backspace_on_empty_filter() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes"]);
let outcome = handle_key_pressed(&mut console, KeyCode::Backspace, &mut state, None);
assert!(state.cart_switch.filter.is_empty());
assert_eq!(outcome, KeyOutcome::Continue);
}
#[test]
fn test_cart_switch_enter_returns_switch_cartridge() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["game.nes", "other.nes"]);
state.cart_switch.selection = 0;
let outcome = handle_key_pressed(&mut console, KeyCode::Enter, &mut state, None);
assert_eq!(outcome, KeyOutcome::SwitchCartridge("game.nes".to_string()));
assert!(!state.cart_switch.open, "dialog should close after Enter");
}
#[test]
fn test_cart_switch_enter_with_no_entries_returns_continue() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&[]);
let outcome = handle_key_pressed(&mut console, KeyCode::Enter, &mut state, None);
assert_eq!(outcome, KeyOutcome::Continue);
}
#[test]
fn test_cart_switch_escape_closes_dialog() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes"]);
handle_key_pressed(&mut console, KeyCode::Escape, &mut state, None);
assert!(!state.cart_switch.open);
}
#[test]
fn test_cart_switch_arrow_down_wraps() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes", "b.nes"]);
state.cart_switch.selection = 1;
handle_key_pressed(&mut console, KeyCode::ArrowDown, &mut state, None);
assert_eq!(state.cart_switch.selection, 0, "should wrap to first");
}
#[test]
fn test_cart_switch_arrow_up_wraps() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes", "b.nes"]);
state.cart_switch.selection = 0;
handle_key_pressed(&mut console, KeyCode::ArrowUp, &mut state, None);
assert_eq!(state.cart_switch.selection, 1, "should wrap to last");
}
#[test]
fn test_cart_switch_typing_filters_entries() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["alpha.nes", "beta.nes", "gamma.nes"]);
handle_key_pressed(&mut console, KeyCode::KeyB, &mut state, None);
assert_eq!(state.cart_switch.visible_count(), 1);
assert_eq!(state.cart_switch.selected_entry(), Some("beta.nes"));
}
#[test]
fn test_cart_switch_keys_dont_trigger_controller() {
let mut console = make_nes_console_with_cart();
let mut state = make_cart_switch_state(&["a.nes"]);
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should not control NES when dialog is open"
);
}
#[test]
fn test_cart_switch_escape_returns_close_outcome() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a.nes"]);
let outcome = handle_key_pressed(&mut console, KeyCode::Escape, &mut state, None);
assert_eq!(
outcome,
KeyOutcome::CloseCartridgeSwitch,
"Escape should return CloseCartridgeSwitch so event loop can restore pause"
);
}
#[test]
fn test_cart_switch_shift_minus_types_underscore() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a_b.nes"]);
state.modifiers = ModifiersState::SHIFT;
handle_key_pressed(&mut console, KeyCode::Minus, &mut state, None);
assert_eq!(
state.cart_switch.filter, "_",
"Shift+Minus should type underscore"
);
}
#[test]
fn test_cart_switch_minus_without_shift_types_dash() {
let mut console = make_nes_console();
let mut state = make_cart_switch_state(&["a-b.nes"]);
handle_key_pressed(&mut console, KeyCode::Minus, &mut state, None);
assert_eq!(
state.cart_switch.filter, "-",
"Minus without Shift should type dash"
);
}
#[test]
fn test_f7_drains_audio_buffer_after_state_load() {
let mut console = make_nes_console();
let mut state = make_state();
let (audio, _pause_called, _resume_called, drain_buffer_called) = TrackingMockAudio::new();
handle_key_pressed(
&mut console,
KeyCode::F7,
&mut state,
Some(&audio as &dyn EmulatorAudio),
);
assert!(
drain_buffer_called.load(Ordering::Relaxed),
"F7 (load state) must call audio.drain_buffer() to discard stale samples"
);
}
#[test]
fn test_wasd_routes_to_port2_only_when_one_gamepad() {
let mut console = make_nes_console();
let mut state = NativeAppState {
gamepad_count: 1,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"W should set port 2 Up when one gamepad is connected"
);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should NOT set port 1 Up when one gamepad is connected"
);
}
#[test]
fn test_wasd_disabled_when_two_gamepads() {
let mut console = make_nes_console();
let mut state = NativeAppState {
gamepad_count: 2,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should NOT set port 1 Up when two gamepads are connected"
);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"W should NOT set port 2 Up when two gamepads are connected"
);
}
#[test]
fn test_ijkl_disabled_when_two_gamepads() {
let mut console = make_nes_console();
let mut state = NativeAppState {
gamepad_count: 2,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyI, &mut state, None);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"I (P2 Up) should be disabled when two gamepads are connected"
);
}
#[test]
fn test_ijkl_disabled_when_one_gamepad() {
let mut console = make_nes_console();
let mut state = NativeAppState {
gamepad_count: 1,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyI, &mut state, None);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"I (P2 Up) should be disabled when one gamepad is connected; use WASD instead"
);
}
#[test]
fn test_help_overlay_port2_shows_wasd_not_ijkl_when_one_gamepad() {
let state = crate::frontends::native::app_state::NativeAppState {
help_overlay_visible: true,
gamepad_count: 1,
..Default::default()
};
let nes = make_nes_console();
let text = state
.overlay_text(&nes, None)
.expect("help overlay must be present");
assert!(
text.contains("W/A/S/D"),
"help overlay must list W/A/S/D for port 2 with 1 gamepad; got:\n{text}"
);
assert!(
!text.contains("I/J/K/L"),
"help overlay must NOT list I/J/K/L when 1 gamepad connected; got:\n{text}"
);
}
#[test]
fn test_keyboard_target_ports_four_score_0_gamepads() {
assert_eq!(keyboard_target_ports(0, true), &[1, 2]);
}
#[test]
fn test_keyboard_target_ports_four_score_1_gamepad() {
assert_eq!(keyboard_target_ports(1, true), &[2, 3]);
}
#[test]
fn test_keyboard_target_ports_four_score_2_gamepads() {
assert_eq!(keyboard_target_ports(2, true), &[3, 4]);
}
#[test]
fn test_keyboard_target_ports_four_score_3_gamepads() {
assert_eq!(keyboard_target_ports(3, true), &[4]);
}
#[test]
fn test_keyboard_target_ports_four_score_4_gamepads() {
assert_eq!(keyboard_target_ports(4, true), &[] as &[u8]);
}
#[test]
fn test_keyboard_target_ports_no_four_score_unchanged() {
assert_eq!(keyboard_target_ports(0, false), &[1, 2]);
assert_eq!(keyboard_target_ports(1, false), &[2]);
assert_eq!(keyboard_target_ports(2, false), &[] as &[u8]);
assert_eq!(keyboard_target_ports(3, false), &[] as &[u8]);
}
#[test]
fn test_wasd_routes_to_port3_with_four_score_and_2_gamepads() {
let mut console = make_nes_console_four_score();
let mut state = NativeAppState {
gamepad_count: 2,
four_score_enabled: true,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(
console.get_joypad_button_states(3) & BIT_UP,
0,
"W should set Up on port 3 with four-score and 2 gamepads"
);
assert_eq!(
console.get_joypad_button_states(1) & BIT_UP,
0,
"W should NOT affect port 1 (owned by gamepad)"
);
assert_eq!(
console.get_joypad_button_states(2) & BIT_UP,
0,
"W should NOT affect port 2 (owned by gamepad)"
);
}
#[test]
fn test_ijkl_routes_to_port4_with_four_score_and_2_gamepads() {
let mut console = make_nes_console_four_score();
let mut state = NativeAppState {
gamepad_count: 2,
four_score_enabled: true,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyI, &mut state, None);
assert_ne!(
console.get_joypad_button_states(4) & BIT_UP,
0,
"I should set Up on port 4 with four-score and 2 gamepads"
);
}
#[test]
fn test_p2_start_routes_to_port4_with_four_score_and_2_gamepads() {
let mut console = make_nes_console_four_score();
let mut state = NativeAppState {
gamepad_count: 2,
four_score_enabled: true,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::Digit0, &mut state, None);
assert_ne!(
console.get_joypad_button_states(4) & BIT_START,
0,
"0 should set Start on port 4 with four-score and 2 gamepads"
);
}
#[test]
fn test_key_release_works_on_port3_with_four_score() {
let mut console = make_nes_console_four_score();
let mut state = NativeAppState {
gamepad_count: 2,
four_score_enabled: true,
..NativeAppState::default()
};
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(console.get_joypad_button_states(3) & BIT_UP, 0);
handle_key_released(&mut console, KeyCode::KeyW, 2, true);
assert_eq!(
console.get_joypad_button_states(3) & BIT_UP,
0,
"Releasing W should clear Up on port 3"
);
}
fn minimal_gb_rom() -> Vec<u8> {
let mut rom = vec![0u8; 0x8000];
rom[0x0147] = 0x00; rom[0x0148] = 0x00; rom[0x0149] = 0x00; let chk = rom[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1));
rom[0x014D] = chk;
rom
}
fn make_gameboy_console() -> Console {
let mut console = Console::new_gameboy(AppContext::new_with_config(Config::default()));
console
.load_rom(&minimal_gb_rom(), "test.gb")
.expect("minimal GB ROM should load");
console
}
#[test]
fn gameboy_d_key_sets_right_button() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyD, &mut state, None);
assert_ne!(
console.get_joypad_button_states(0) & BIT_RIGHT,
0,
"D key should set GB Right button"
);
}
#[test]
fn gameboy_w_key_sets_up_button() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyW, &mut state, None);
assert_ne!(
console.get_joypad_button_states(0) & BIT_UP,
0,
"W key should set GB Up button"
);
}
#[test]
fn gameboy_t_key_sets_a_button() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyT, &mut state, None);
assert_ne!(
console.get_joypad_button_states(0) & BIT_A,
0,
"T key should set GB A button"
);
}
#[test]
fn gameboy_f6_save_state_returns_continue() {
let mut console = make_gameboy_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F6, &mut state, None),
KeyOutcome::Continue,
"F6 should return Continue in GB mode"
);
}
#[test]
fn gameboy_f7_load_state_returns_continue() {
let mut console = make_gameboy_console();
let mut state = make_state();
assert_eq!(
handle_key_pressed(&mut console, KeyCode::F7, &mut state, None),
KeyOutcome::Continue,
"F7 should return Continue in GB mode"
);
}
#[test]
fn gameboy_ctrl_r_soft_resets() {
let mut console = make_gameboy_console();
let mut state = make_state();
with_ctrl(&mut state);
assert_eq!(
handle_key_pressed(&mut console, KeyCode::KeyR, &mut state, None),
KeyOutcome::Continue,
"Ctrl+R should return Continue in GB mode"
);
}
#[test]
fn gameboy_ctrl_shift_r_hard_resets() {
let mut console = make_gameboy_console();
let mut state = make_state();
with_ctrl_shift(&mut state);
assert_eq!(
handle_key_pressed(&mut console, KeyCode::KeyR, &mut state, None),
KeyOutcome::Continue,
"Ctrl+Shift+R should return Continue in GB mode"
);
}
#[test]
fn gameboy_h_key_toggles_help_overlay_on() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyH, &mut state, None);
assert!(
state.help_overlay_visible,
"H key should toggle help overlay on in GB mode"
);
}
#[test]
fn gameboy_h_key_toggles_help_overlay_off() {
let mut console = make_gameboy_console();
let mut state = make_state();
state.help_overlay_visible = true;
handle_key_pressed(&mut console, KeyCode::KeyH, &mut state, None);
assert!(
!state.help_overlay_visible,
"H key should toggle help overlay off in GB mode"
);
}
#[test]
fn gameboy_d_key_released_clears_right_button() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyD, &mut state, None);
assert_ne!(
console.get_joypad_button_states(0) & BIT_RIGHT,
0,
"D key must set GB Right button before testing release"
);
handle_key_released(&mut console, KeyCode::KeyD, 0, false);
assert_eq!(
console.get_joypad_button_states(0) & BIT_RIGHT,
0,
"Releasing D should clear GB Right button"
);
}
#[test]
fn gba_keyboard_maps_all_ten_buttons() {
let cases = [
(KeyCode::KeyT, GBA_KEY_A, "T should press GBA A"),
(KeyCode::KeyR, GBA_KEY_B, "R should press GBA B"),
(KeyCode::Digit4, GBA_KEY_SELECT, "4 should press GBA Select"),
(KeyCode::Digit5, GBA_KEY_START, "5 should press GBA Start"),
(KeyCode::KeyW, GBA_KEY_UP, "W should press GBA Up"),
(KeyCode::KeyS, GBA_KEY_DOWN, "S should press GBA Down"),
(KeyCode::KeyA, GBA_KEY_LEFT, "A should press GBA Left"),
(KeyCode::KeyD, GBA_KEY_RIGHT, "D should press GBA Right"),
(KeyCode::KeyQ, GBA_KEY_L, "Q should press GBA L"),
(KeyCode::KeyE, GBA_KEY_R, "E should press GBA R"),
(KeyCode::ArrowUp, GBA_KEY_UP, "ArrowUp should press GBA Up"),
(
KeyCode::ArrowDown,
GBA_KEY_DOWN,
"ArrowDown should press GBA Down",
),
(
KeyCode::ArrowLeft,
GBA_KEY_LEFT,
"ArrowLeft should press GBA Left",
),
(
KeyCode::ArrowRight,
GBA_KEY_RIGHT,
"ArrowRight should press GBA Right",
),
];
for (key, mask, message) in cases {
let mut console = make_gba_console();
let mut state = make_state();
handle_key_pressed(&mut console, key, &mut state, None);
assert_eq!(gba_keyinput(&console) & mask, 0, "{message}");
}
}
#[test]
fn gba_keyboard_releases_l_and_r_shoulders() {
let mut console = make_gba_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyQ, &mut state, None);
handle_key_pressed(&mut console, KeyCode::KeyE, &mut state, None);
assert_eq!(
gba_keyinput(&console) & GBA_KEY_L,
0,
"Q should press GBA L"
);
assert_eq!(
gba_keyinput(&console) & GBA_KEY_R,
0,
"E should press GBA R"
);
handle_key_released(&mut console, KeyCode::KeyQ, 0, false);
handle_key_released(&mut console, KeyCode::KeyE, 0, false);
assert_ne!(
gba_keyinput(&console) & GBA_KEY_L,
0,
"releasing Q should clear GBA L"
);
assert_ne!(
gba_keyinput(&console) & GBA_KEY_R,
0,
"releasing E should clear GBA R"
);
}
#[test]
fn gameboy_q_and_e_do_not_map_to_buttons() {
let mut console = make_gameboy_console();
let mut state = make_state();
handle_key_pressed(&mut console, KeyCode::KeyQ, &mut state, None);
handle_key_pressed(&mut console, KeyCode::KeyE, &mut state, None);
assert_eq!(
console.get_joypad_button_states(0),
0,
"Game Boy keyboard mapping should remain eight-button only"
);
}
}