1use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum PortDevice {
24 #[default]
27 Gamepad,
28 Mouse,
30 SuperScope,
35 Multitap,
39}
40
41impl PortDevice {
42 const fn to_tag(self) -> u8 {
43 match self {
44 Self::Gamepad => 0,
45 Self::Mouse => 1,
46 Self::SuperScope => 2,
47 Self::Multitap => 3,
48 }
49 }
50
51 const fn from_tag(tag: u8) -> Self {
52 match tag {
53 1 => Self::Mouse,
54 2 => Self::SuperScope,
55 3 => Self::Multitap,
56 _ => Self::Gamepad,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, Default)]
63pub(crate) struct MouseState {
64 pending_dx: i32,
69 pending_dy: i32,
70 pending_left: bool,
71 pending_right: bool,
72 speed: u8,
76 shift: u32,
78 latched: bool,
81}
82
83impl MouseState {
84 pub(crate) const fn set_input(&mut self, dx: i32, dy: i32, left: bool, right: bool) {
85 self.pending_dx = dx;
86 self.pending_dy = dy;
87 self.pending_left = left;
88 self.pending_right = right;
89 }
90
91 #[allow(clippy::similar_names)] pub(crate) fn latch(&mut self, strobe: bool) {
95 if self.latched == strobe {
96 return;
97 }
98 self.latched = strobe;
99 let neg_x = u32::from(self.pending_dx < 0);
100 let neg_y = u32::from(self.pending_dy < 0);
101 let multiplier: f64 = match self.speed {
102 1 => 1.5,
103 2 => 2.0,
104 _ => 1.0,
105 };
106 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
107 let cx = (f64::from(self.pending_dx.unsigned_abs()) * multiplier).min(127.0) as u32;
108 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
109 let cy = (f64::from(self.pending_dy.unsigned_abs()) * multiplier).min(127.0) as u32;
110 self.shift = (u32::from(self.pending_right) << 23)
111 | (u32::from(self.pending_left) << 22)
112 | (u32::from((self.speed >> 1) & 1) << 21)
113 | (u32::from(self.speed & 1) << 20)
114 | (1 << 16) | (neg_y << 15)
116 | ((cy & 0x7F) << 8)
117 | (neg_x << 7)
118 | (cx & 0x7F);
119 }
120
121 pub(crate) const fn clock(&mut self) -> u8 {
123 if self.latched {
124 self.speed = (self.speed + 1) % 3;
125 return 0;
126 }
127 let bit = ((self.shift >> 31) & 1) as u8;
128 self.shift = (self.shift << 1) | 1;
129 bit
130 }
131
132 fn save_state(self, s: &mut SaveWriter) {
133 s.write_u8(self.speed);
134 s.write_u32(self.shift);
135 s.write_bool(self.latched);
136 }
137
138 fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
139 Ok(Self {
140 speed: s.read_u8()?,
141 shift: s.read_u32()?,
142 latched: s.read_bool()?,
143 ..Self::default()
144 })
145 }
146}
147
148pub mod scope {
151 pub const TRIGGER: u8 = 1 << 0;
153 pub const CURSOR: u8 = 1 << 1;
155 pub const TURBO: u8 = 1 << 2;
157 pub const PAUSE: u8 = 1 << 3;
159}
160
161#[allow(clippy::struct_excessive_bools)]
166#[derive(Debug, Clone, Copy, Default)]
167pub(crate) struct SuperScopeState {
168 x: i32,
172 y: i32,
173 buttons: u8,
175
176 latched: bool,
177 shift: u16,
178 pending_sample: bool,
185
186 turbo_edge: bool,
187 turbo_prev: bool,
188 trigger_lock: bool,
189 pause_edge: bool,
190 pause_lock: bool,
191}
192
193impl SuperScopeState {
194 pub(crate) fn set_input(&mut self, x: i32, y: i32, buttons: u8) {
195 self.x = x.clamp(-16, 256 + 16);
196 self.y = y.clamp(-16, 240 + 16);
197 self.buttons = buttons;
198 }
199
200 fn offscreen(&self, visible_height: u16) -> bool {
201 self.x < 0 || self.y < 0 || self.x >= 256 || self.y >= i32::from(visible_height)
202 }
203
204 pub(crate) const fn latch(&mut self, strobe: bool) {
206 if self.latched == strobe {
207 return;
208 }
209 self.latched = strobe;
210 self.pending_sample = true;
211 }
212
213 fn sample(&mut self, visible_height: u16) {
216 self.pending_sample = false;
217
218 let turbo = self.buttons & scope::TURBO != 0;
219 if turbo && !self.turbo_prev {
220 self.turbo_edge = !self.turbo_edge;
221 }
222 self.turbo_prev = turbo;
223
224 let trigger = self.buttons & scope::TRIGGER != 0;
225 let mut trigger_value = false;
226 if trigger && (self.turbo_edge || !self.trigger_lock) {
227 trigger_value = true;
228 self.trigger_lock = true;
229 } else if !trigger {
230 self.trigger_lock = false;
231 }
232
233 let pause = self.buttons & scope::PAUSE != 0;
234 self.pause_edge = false;
235 if pause && !self.pause_lock {
236 self.pause_edge = true;
237 self.pause_lock = true;
238 } else if !pause {
239 self.pause_lock = false;
240 }
241
242 let cursor = self.buttons & scope::CURSOR != 0;
243 let offscreen = self.offscreen(visible_height);
244 self.shift = (u16::from(trigger_value && !offscreen) << 15)
245 | (u16::from(cursor) << 14)
246 | (u16::from(self.turbo_edge) << 13)
247 | (u16::from(self.pause_edge) << 12)
248 | (u16::from(offscreen) << 9)
249 | 0x00FF; }
251
252 pub(crate) fn clock(&mut self, visible_height: u16) -> u8 {
253 if self.pending_sample {
254 self.sample(visible_height);
255 }
256 let bit = ((self.shift >> 15) & 1) as u8;
257 self.shift = (self.shift << 1) | 1;
258 bit
259 }
260
261 pub(crate) fn beam_target(&self, visible_height: u16) -> Option<(u16, u16)> {
266 if self.offscreen(visible_height) {
267 return None;
268 }
269 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
273 Some((self.y as u16, (self.x + 24) as u16))
274 }
275
276 fn save_state(self, s: &mut SaveWriter) {
277 s.write_bool(self.latched);
278 s.write_u16(self.shift);
279 s.write_bool(self.turbo_edge);
280 s.write_bool(self.turbo_prev);
281 s.write_bool(self.trigger_lock);
282 s.write_bool(self.pause_edge);
283 s.write_bool(self.pause_lock);
284 }
285
286 fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
287 Ok(Self {
288 latched: s.read_bool()?,
289 shift: s.read_u16()?,
290 turbo_edge: s.read_bool()?,
291 turbo_prev: s.read_bool()?,
292 trigger_lock: s.read_bool()?,
293 pause_edge: s.read_bool()?,
294 pause_lock: s.read_bool()?,
295 ..Self::default()
296 })
297 }
298}
299
300#[derive(Debug, Clone, Copy, Default)]
304pub(crate) struct MultitapState {
305 pads: [u16; 4],
308 latched: bool,
309}
310
311impl MultitapState {
312 pub(crate) fn set_pad(&mut self, index: usize, buttons: u16) {
315 if let Some(pad) = self.pads.get_mut(index) {
316 *pad = buttons;
317 }
318 }
319
320 pub(crate) fn pad(&self, index: usize) -> u16 {
321 self.pads.get(index).copied().unwrap_or(0)
322 }
323
324 pub(crate) const fn latch(&mut self, strobe: bool) {
326 self.latched = strobe;
327 }
328
329 pub(crate) const fn clock(&mut self, iobit: bool) -> (u8, u8) {
335 if self.latched {
336 return (0, 1);
337 }
338 let (a, b) = if iobit { (0, 1) } else { (2, 3) };
339 let bit_a = ((self.pads[a] >> 15) & 1) as u8;
340 self.pads[a] = (self.pads[a] << 1) | 1;
341 let bit_b = ((self.pads[b] >> 15) & 1) as u8;
342 self.pads[b] = (self.pads[b] << 1) | 1;
343 (bit_a, bit_b)
344 }
345
346 fn save_state(self, s: &mut SaveWriter) {
347 for pad in self.pads {
348 s.write_u16(pad);
349 }
350 s.write_bool(self.latched);
351 }
352
353 fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
354 let mut pads = [0u16; 4];
355 for pad in &mut pads {
356 *pad = s.read_u16()?;
357 }
358 Ok(Self {
359 pads,
360 latched: s.read_bool()?,
361 })
362 }
363}
364
365#[derive(Debug, Clone, Copy, Default)]
369pub(crate) struct PortState {
370 pub(crate) device: PortDevice,
371 pub(crate) mouse: MouseState,
372 pub(crate) super_scope: SuperScopeState,
373 pub(crate) multitap: MultitapState,
374}
375
376impl PortState {
377 pub(crate) fn latch(&mut self, strobe: bool) {
378 match self.device {
379 PortDevice::Gamepad => {}
380 PortDevice::Mouse => self.mouse.latch(strobe),
381 PortDevice::SuperScope => self.super_scope.latch(strobe),
382 PortDevice::Multitap => self.multitap.latch(strobe),
383 }
384 }
385
386 pub(crate) fn clock(&mut self, iobit: bool, visible_height: u16) -> (u8, u8) {
390 match self.device {
391 PortDevice::Gamepad => (0, 0),
392 PortDevice::Mouse => (self.mouse.clock(), 0),
393 PortDevice::SuperScope => (self.super_scope.clock(visible_height), 0),
394 PortDevice::Multitap => self.multitap.clock(iobit),
395 }
396 }
397
398 pub(crate) fn save_state(self, s: &mut SaveWriter) {
399 s.write_u8(self.device.to_tag());
400 self.mouse.save_state(s);
401 self.super_scope.save_state(s);
402 self.multitap.save_state(s);
403 }
404
405 pub(crate) fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
406 let device = PortDevice::from_tag(s.read_u8()?);
407 let mouse = MouseState::load_state(s)?;
408 let super_scope = SuperScopeState::load_state(s)?;
409 let multitap = MultitapState::load_state(s)?;
410 Ok(Self {
411 device,
412 mouse,
413 super_scope,
414 multitap,
415 })
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use alloc::vec::Vec;
422
423 use super::{MouseState, MultitapState, PortDevice, SuperScopeState, scope};
424
425 fn shift_bits<T>(mut clock: impl FnMut(&mut T) -> u8, state: &mut T, n: usize) -> Vec<u8> {
427 (0..n).map(|_| clock(state)).collect()
428 }
429
430 #[test]
431 fn port_device_tag_round_trips() {
432 for device in [
433 PortDevice::Gamepad,
434 PortDevice::Mouse,
435 PortDevice::SuperScope,
436 PortDevice::Multitap,
437 ] {
438 assert_eq!(PortDevice::from_tag(device.to_tag()), device);
439 }
440 }
441
442 #[test]
443 fn mouse_signature_and_button_bits() {
444 let mut mouse = MouseState::default();
445 mouse.set_input(0, 0, true, true);
447 mouse.latch(true); mouse.latch(false); let bits = shift_bits(MouseState::clock, &mut mouse, 32);
450 assert!(bits[0..8].iter().all(|&b| b == 0));
452 assert_eq!(bits[8], 1, "right button bit");
454 assert_eq!(bits[9], 1, "left button bit");
455 assert_eq!(bits[10], 0);
457 assert_eq!(bits[11], 0);
458 assert_eq!(&bits[12..16], &[0, 0, 0, 1], "mouse device signature");
460 assert_eq!(bits[16], 0, "dy direction");
462 assert!(bits[17..24].iter().all(|&b| b == 0), "cy magnitude");
463 assert_eq!(bits[24], 0, "dx direction");
464 assert!(bits[25..32].iter().all(|&b| b == 0), "cx magnitude");
465 }
466
467 #[test]
468 fn mouse_direction_and_magnitude() {
469 let mut mouse = MouseState::default();
470 mouse.set_input(-5, 10, false, false);
471 mouse.latch(true);
472 mouse.latch(false);
473 let bits = shift_bits(MouseState::clock, &mut mouse, 32);
474 assert_eq!(
475 bits[16], 0,
476 "moving down (positive dy) clears the dy-direction bit"
477 );
478 let cy = bits[17..24]
479 .iter()
480 .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
481 assert_eq!(cy, 10, "cy magnitude, no speed multiplier at default speed");
482 assert_eq!(
483 bits[24], 1,
484 "moving left (negative dx) sets the dx-direction bit"
485 );
486 let cx = bits[25..32]
487 .iter()
488 .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
489 assert_eq!(cx, 5, "cx magnitude");
490 }
491
492 #[test]
493 fn mouse_magnitude_clamps_to_127() {
494 let mut mouse = MouseState::default();
495 mouse.set_input(500, 0, false, false);
496 mouse.latch(true);
497 mouse.latch(false);
498 let bits = shift_bits(MouseState::clock, &mut mouse, 32);
499 let cx = bits[25..32]
500 .iter()
501 .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
502 assert_eq!(
503 cx, 127,
504 "a huge host delta clamps to the 7-bit magnitude's max"
505 );
506 }
507
508 #[test]
509 fn mouse_speed_cycles_while_strobe_held_high() {
510 let mut mouse = MouseState::default();
511 mouse.latch(true);
512 assert_eq!(mouse.clock(), 0);
515 assert_eq!(mouse.speed, 1);
516 assert_eq!(mouse.clock(), 0);
517 assert_eq!(mouse.speed, 2);
518 assert_eq!(mouse.clock(), 0);
519 assert_eq!(mouse.speed, 0);
520 }
521
522 #[test]
523 fn mouse_free_runs_high_past_32_bits() {
524 let mut mouse = MouseState::default();
525 mouse.set_input(0, 0, false, false);
526 mouse.latch(true);
527 mouse.latch(false);
528 let bits = shift_bits(MouseState::clock, &mut mouse, 40);
529 assert!(
530 bits[32..].iter().all(|&b| b == 1),
531 "past bit 32, reads free-run high"
532 );
533 }
534
535 #[test]
536 fn superscope_trigger_cursor_and_offscreen_bits() {
537 let mut scope = SuperScopeState::default();
538 scope.set_input(100, 100, scope::TRIGGER | scope::CURSOR);
539 scope.latch(true);
540 scope.latch(false);
541 let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 8);
542 assert_eq!(
543 bits[0], 1,
544 "trigger fires (aimed on-screen, not turbo-locked yet)"
545 );
546 assert_eq!(bits[1], 1, "cursor is level-sensitive, reads live");
547 assert_eq!(
548 bits[2], 0,
549 "turbo edge hasn't toggled (turbo switch not held)"
550 );
551 assert_eq!(bits[3], 0, "pause hasn't been pressed");
552 assert_eq!(bits[6], 0, "aimed on-screen");
553 }
554
555 #[test]
556 fn superscope_offscreen_suppresses_trigger_and_sets_offscreen_bit() {
557 let mut scope = SuperScopeState::default();
558 scope.set_input(-16, -16, scope::TRIGGER);
559 scope.latch(true);
560 scope.latch(false);
561 let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 8);
562 assert_eq!(bits[0], 0, "trigger is suppressed while aimed off-screen");
563 assert_eq!(bits[6], 1, "offscreen bit set");
564 assert!(
565 scope.beam_target(224).is_none(),
566 "no beam target while offscreen"
567 );
568 }
569
570 #[test]
571 fn superscope_beam_target_matches_ares_formula() {
572 let mut scope = SuperScopeState::default();
573 scope.set_input(50, 30, 0);
574 assert_eq!(scope.beam_target(224), Some((30, 74)));
576 }
577
578 #[test]
579 fn superscope_turbo_makes_trigger_level_sensitive() {
580 let mut scope = SuperScopeState::default();
581 scope.set_input(10, 10, scope::TURBO);
583 scope.latch(true);
584 scope.latch(false);
585 scope.clock(224);
586 assert!(scope.turbo_edge, "turbo toggled on");
587
588 scope.set_input(10, 10, scope::TURBO | scope::TRIGGER);
592 scope.latch(true);
593 scope.latch(false);
594 let first = scope.clock(224);
595 scope.latch(true);
596 scope.latch(false);
597 let second = scope.clock(224);
598 assert_eq!(first, 1, "trigger fires with turbo active");
599 assert_eq!(
600 second, 1,
601 "trigger keeps firing every latch while held, under turbo"
602 );
603 }
604
605 #[test]
606 fn superscope_free_runs_high_past_8_bits() {
607 let mut scope = SuperScopeState::default();
608 scope.set_input(10, 10, 0);
609 scope.latch(true);
610 scope.latch(false);
611 let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 12);
612 assert!(
613 bits[8..].iter().all(|&b| b == 1),
614 "past bit 8, reads free-run high"
615 );
616 }
617
618 #[test]
619 fn multitap_device_detection_signature_while_latched() {
620 let mut tap = MultitapState::default();
621 tap.latch(true);
622 assert_eq!(
623 tap.clock(true),
624 (0, 1),
625 "multitap ID: data1=0, data2=1, while latched"
626 );
627 assert_eq!(
628 tap.clock(false),
629 (0, 1),
630 "signature holds regardless of iobit"
631 );
632 }
633
634 #[test]
635 fn multitap_iobit_selects_the_addressed_pair() {
636 let mut tap = MultitapState::default();
637 tap.set_pad(0, 0x8000); tap.set_pad(1, 0x0000); tap.set_pad(2, 0x0000); tap.set_pad(3, 0x8000); tap.latch(false);
642
643 assert_eq!(tap.clock(true), (1, 0));
645 assert_eq!(tap.clock(false), (0, 1));
648 }
649
650 #[test]
651 fn multitap_pad_get_set_round_trips() {
652 let mut tap = MultitapState::default();
653 tap.set_pad(2, 0x1234);
654 assert_eq!(tap.pad(2), 0x1234);
655 assert_eq!(tap.pad(0), 0, "untouched sub-pads stay zero");
656 }
657}