use super::ControllerInput;
use crate::app_context::AppContext;
use crate::input::Button;
use crate::ppu::Ppu;
use serde::{Deserialize, Serialize};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ZapperState {
pub x: u8,
pub y: u8,
pub trigger: bool,
pub light: bool,
}
const LIGHT_DETECTION_THRESHOLD: f32 = 85.0;
const MAX_SCANLINES_BEHIND: i32 = 2;
pub struct Zapper {
x: u8,
y: u8,
trigger: bool,
light: Cell<bool>,
ppu: Rc<RefCell<Ppu>>,
app_context: Rc<RefCell<AppContext>>,
}
impl Zapper {
pub fn new(ppu: Rc<RefCell<Ppu>>, app_context: Rc<RefCell<AppContext>>) -> Self {
Self {
x: 0,
y: 0,
trigger: false,
light: Cell::new(false),
ppu,
app_context,
}
}
pub fn capture_state(&self) -> ZapperState {
ZapperState {
x: self.x,
y: self.y,
trigger: self.trigger,
light: self.light.get(),
}
}
pub fn restore_state(&mut self, state: &ZapperState) {
self.x = state.x;
self.y = state.y;
self.trigger = state.trigger;
self.light.set(state.light);
}
}
impl crate::input::Controller for Zapper {
fn write_strobe(&mut self, _value: u8) {}
fn read(&mut self, _is_dummy_read: bool) -> u8 {
let detection_size = self.app_context.borrow().config().zapper_detection_size;
let ppu = self.ppu.borrow();
let scanline = ppu.timing().scanline();
let pixel = ppu.timing().pixel();
let light_now = self.detect_light(scanline, pixel, ppu.screen_buffer(), detection_size);
self.light.set(light_now);
let trigger_bit = (self.trigger as u8) << 4;
let light_bit = if light_now { 0 } else { 1 << 3 };
trigger_bit | light_bit
}
fn capture_state(&self) -> crate::input::ControllerState {
crate::input::ControllerState::Zapper(self.capture_state())
}
fn restore_state(&mut self, state: &crate::input::ControllerState) {
if let crate::input::ControllerState::Zapper(zapper_state) = state {
self.restore_state(zapper_state);
}
}
fn set_button(&mut self, _button: Button, _pressed: bool) -> bool {
false
}
fn set_mouse_x_position(&mut self, position: u8) -> bool {
self.x = position;
true
}
fn set_mouse_y_position(&mut self, position: u8) -> bool {
self.y = position;
true
}
fn set_mouse_left_button(&mut self, pressed: bool) -> bool {
self.trigger = pressed;
true
}
fn input_type(&self) -> ControllerInput {
crate::input::controller_input_type(crate::input::ControllerType::Zapper)
}
}
impl Zapper {
fn detect_light(
&self,
current_scanline: u16,
current_pixel: u16,
screen_buffer: &crate::ppu::ScreenBuffer,
detection_size: u8,
) -> bool {
let zapper_x = self.x as i32;
let zapper_y = self.y as i32;
let beam_position = (current_scanline as i32) * 341 + (current_pixel as i32);
let zapper_position = zapper_y * 341 + zapper_x;
if zapper_position > beam_position {
return false;
}
let scanlines_behind = (beam_position - zapper_position) / 341;
if scanlines_behind > MAX_SCANLINES_BEHIND {
return false;
}
let size_i32 = detection_size as i32;
for dy in -size_i32..=size_i32 {
for dx in -size_i32..=size_i32 {
let sample_x = zapper_x + dx;
let sample_y = zapper_y + dy;
if !(0..256).contains(&sample_x) || !(0..240).contains(&sample_y) {
continue;
}
let luminance = screen_buffer.get_luminance(sample_x as u32, sample_y as u32);
if luminance >= LIGHT_DETECTION_THRESHOLD {
return true;
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::Zapper;
use crate::console::TimingMode;
use crate::input::Controller;
use crate::ppu::Ppu;
use std::cell::RefCell;
use std::rc::Rc;
fn test_app_context_with_size(size: u8) -> Rc<RefCell<crate::app_context::AppContext>> {
let config = crate::console::Config {
zapper_detection_size: size,
..Default::default()
};
Rc::new(RefCell::new(
crate::app_context::AppContext::new_with_config(config),
))
}
fn create_zapper_with_ppu(size: u8) -> (Zapper, Rc<RefCell<Ppu>>) {
let ppu = Rc::new(RefCell::new(Ppu::new_for_testing(TimingMode::Ntsc)));
let app_context = test_app_context_with_size(size);
let zapper = Zapper::new(ppu.clone(), app_context);
(zapper, ppu)
}
fn advance_ppu_to(ppu: &Rc<RefCell<Ppu>>, scanline: u16, pixel: u16) {
let current_scanline = ppu.borrow().timing().scanline();
let current_pixel = ppu.borrow().timing().pixel();
let current_cycles = (current_scanline as u64) * 341 + (current_pixel as u64);
let target_cycles = (scanline as u64) * 341 + (pixel as u64);
let delta = target_cycles.saturating_sub(current_cycles);
if delta > 0 {
ppu.borrow_mut().run_ppu_cycles(delta);
}
}
#[test]
fn test_zapper_trigger_and_light_bits() {
let (mut zapper, _ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_left_button(true);
let value = zapper.read(false);
assert_eq!((value >> 3) & 0x01, 1);
assert_eq!((value >> 4) & 0x01, 1);
zapper.set_mouse_left_button(false);
let value = zapper.read(false);
assert_eq!((value >> 3) & 0x01, 1);
assert_eq!((value >> 4) & 0x01, 0);
}
#[test]
fn test_zapper_light_bit_clears_on_light() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(0);
zapper.set_mouse_y_position(0);
advance_ppu_to(&ppu, 1, 0);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(0, 0, 255, 255, 255);
let value = zapper.read(false);
assert_eq!((value >> 3) & 0x01, 0);
}
#[test]
fn test_zapper_capture_restore_roundtrip() {
let (mut zapper, _ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(0x22);
zapper.set_mouse_y_position(0x77);
zapper.set_mouse_left_button(true);
let state = zapper.capture_state();
let (mut restored, _ppu) = create_zapper_with_ppu(0);
restored.restore_state(&state);
let restored_state = restored.capture_state();
assert_eq!(restored_state.x, 0x22);
assert_eq!(restored_state.y, 0x77);
assert!(restored_state.trigger);
}
#[test]
fn test_zapper_detects_light_on_bright_pixel() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(100);
advance_ppu_to(&ppu, 101, 100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
let value = zapper.read(false);
assert_eq!(
(value >> 3) & 0x01,
0,
"Light bit should be 0 when light is detected"
);
assert!(zapper.capture_state().light);
}
#[test]
fn test_zapper_no_light_on_dark_pixel() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(50);
zapper.set_mouse_y_position(50);
advance_ppu_to(&ppu, 51, 50);
let value = zapper.read(false);
assert_eq!(
(value >> 3) & 0x01,
1,
"Light bit should be 1 when no light is detected"
);
assert!(!zapper.capture_state().light);
}
#[test]
fn test_zapper_light_threshold() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(30);
zapper.set_mouse_y_position(30);
advance_ppu_to(&ppu, 31, 30);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(30, 30, 84, 84, 84);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Light should not be detected below threshold"
);
advance_ppu_to(&ppu, 31, 30);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(30, 30, 85, 85, 85);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Light should be detected at threshold"
);
advance_ppu_to(&ppu, 31, 30);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(30, 30, 200, 200, 200);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Light should be detected above threshold"
);
}
#[test]
fn test_zapper_light_detection_with_different_colors() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(60);
zapper.set_mouse_y_position(60);
advance_ppu_to(&ppu, 61, 60);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(60, 60, 0, 255, 0);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Bright green should be detected"
);
advance_ppu_to(&ppu, 61, 60);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(60, 60, 255, 0, 0);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Pure red alone is below threshold"
);
advance_ppu_to(&ppu, 61, 60);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(60, 60, 0, 0, 255);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Pure blue alone is below threshold"
);
}
#[test]
fn test_zapper_no_light_ahead_of_beam() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
advance_ppu_to(&ppu, 99, 200);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Cannot detect light ahead of beam"
);
}
#[test]
fn test_zapper_no_light_too_far_behind_beam() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(10);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 10, 255, 255, 255);
advance_ppu_to(&ppu, 200, 100);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Cannot detect light too far behind beam"
);
}
#[test]
fn test_zapper_light_persistence_is_short_hardware_like_window() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
advance_ppu_to(&ppu, 100, 100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Light should be detected on target line"
);
advance_ppu_to(&ppu, 102, 100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Light should still be detected within a short persistence window"
);
advance_ppu_to(&ppu, 103, 100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Light should no longer be detected after the short persistence window"
);
}
#[test]
fn test_zapper_with_radius_detects_nearby_bright_pixel() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(100);
advance_ppu_to(&ppu, 101, 0);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(101, 100, 255, 255, 255);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Radius 0 should not detect neighboring pixel"
);
let (mut zapper, ppu) = create_zapper_with_ppu(1);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(100);
advance_ppu_to(&ppu, 101, 0);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(101, 100, 255, 255, 255);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Radius 1 should detect pixel at distance 1"
);
}
#[test]
fn test_zapper_y_boundary_240() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(240);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 100, 255, 255, 255);
advance_ppu_to(&ppu, 241, 100);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Should not detect light when Y >= 240"
);
}
#[test]
fn test_zapper_y_boundary_255() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(255);
advance_ppu_to(&ppu, 260, 100);
zapper.read(false);
assert!(
!zapper.capture_state().light,
"Should not detect light when Y = 255"
);
}
#[test]
fn test_zapper_y_boundary_239() {
let (mut zapper, ppu) = create_zapper_with_ppu(0);
zapper.set_mouse_x_position(100);
zapper.set_mouse_y_position(239);
advance_ppu_to(&ppu, 240, 100);
ppu.borrow_mut()
.screen_buffer_mut()
.set_pixel(100, 239, 255, 255, 255);
zapper.read(false);
assert!(
zapper.capture_state().light,
"Should detect light when Y = 239 (within bounds)"
);
}
}