badline 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +88 -23
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +131 -15
  51. data/lib/badline/cia/interrupt_register.rb +88 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +109 -74
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +68 -8
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +171 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +108 -167
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +33 -0
  85. data/lib/badline/kernal_trap/drive.rb +239 -0
  86. data/lib/badline/kernal_trap/file.rb +4 -24
  87. data/lib/badline/kernal_trap/load.rb +2 -2
  88. data/lib/badline/kernal_trap/routine.rb +34 -0
  89. data/lib/badline/kernal_trap/save.rb +1 -1
  90. data/lib/badline/kernal_trap/serial.rb +139 -0
  91. data/lib/badline/kernal_trap.rb +4 -0
  92. data/lib/badline/keyboard.rb +40 -16
  93. data/lib/badline/media.rb +48 -10
  94. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  95. data/lib/badline/roms/eapi/README +41 -0
  96. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  97. data/lib/badline/sid/decimator.rb +46 -0
  98. data/lib/badline/sid/envelope.rb +232 -0
  99. data/lib/badline/sid/filter.rb +213 -0
  100. data/lib/badline/sid/voice.rb +61 -0
  101. data/lib/badline/sid/waveform/combined.rb +126 -0
  102. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  103. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  104. data/lib/badline/sid/waveform.rb +289 -0
  105. data/lib/badline/sid.rb +274 -7
  106. data/lib/badline/status.rb +32 -27
  107. data/lib/badline/storage/crt_file.rb +18 -2
  108. data/lib/badline/storage/disk_image.rb +52 -5
  109. data/lib/badline/storage/host_directory.rb +18 -4
  110. data/lib/badline/storage/sid_file.rb +274 -0
  111. data/lib/badline/storage/song_lengths.rb +65 -0
  112. data/lib/badline/storage/t64.rb +67 -0
  113. data/lib/badline/storage/tap.rb +68 -0
  114. data/lib/badline/storage.rb +19 -0
  115. data/lib/badline/time_of_day.rb +105 -52
  116. data/lib/badline/version.rb +1 -1
  117. data/lib/badline/vic/bank.rb +14 -6
  118. data/lib/badline/vic/border_mask.rb +76 -0
  119. data/lib/badline/vic/collisions.rb +104 -0
  120. data/lib/badline/vic/color_patches.rb +33 -0
  121. data/lib/badline/vic/display_state.rb +92 -29
  122. data/lib/badline/vic/graphics_mode.rb +78 -53
  123. data/lib/badline/vic/graphics_shifter.rb +140 -0
  124. data/lib/badline/vic/register_log.rb +63 -0
  125. data/lib/badline/vic/registers.rb +3 -0
  126. data/lib/badline/vic/sequencer.rb +163 -137
  127. data/lib/badline/vic/sequencer_output.rb +116 -0
  128. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  129. data/lib/badline/vic/sprite/shifter.rb +129 -0
  130. data/lib/badline/vic/sprite.rb +275 -65
  131. data/lib/badline/vic/sprites.rb +220 -58
  132. data/lib/badline/vic.rb +373 -49
  133. data/lib/badline.rb +5 -0
  134. data/vendor/.gitkeep +0 -0
  135. metadata +103 -4
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
5
+ # motor runs with a key pressed, and reports the keys through the cassette
6
+ # sense line on the $01 port.
7
+ class Datasette
8
+ attr_reader :tape
9
+ attr_writer :motor
10
+
11
+ def initialize(tape = nil)
12
+ @tape = tape
13
+ @playing = false
14
+ @motor = false
15
+ @countdown = 0
16
+ @flag_handler = nil
17
+ @sense_handler = nil
18
+ end
19
+
20
+ # Falling edges on the tape read line.
21
+ def on_flag(&handler)
22
+ @flag_handler = handler
23
+ end
24
+
25
+ # The cassette sense line, which the $01 port reads back.
26
+ def on_sense_change(&handler)
27
+ @sense_handler = handler
28
+ end
29
+
30
+ def insert(tape)
31
+ @tape = tape
32
+ rewind
33
+ end
34
+
35
+ def eject
36
+ stop!
37
+ @tape = nil
38
+ end
39
+
40
+ def rewind
41
+ @tape&.rewind
42
+ @countdown = 0
43
+ end
44
+
45
+ def play! = press(true)
46
+ def stop! = press(false)
47
+
48
+ def playing? = @playing
49
+
50
+ # Sense reads low while a key is down on the deck.
51
+ def sense_low? = @playing
52
+
53
+ def motor? = @motor
54
+
55
+ def running? = @playing && @motor && !@tape.nil?
56
+
57
+ def cycle!
58
+ return unless running?
59
+
60
+ @countdown = @tape.next_pulse.to_i if @countdown.zero?
61
+ return if @countdown.zero?
62
+
63
+ @countdown -= 1
64
+ @flag_handler&.call if @countdown.zero?
65
+ end
66
+
67
+ def inspect
68
+ "#<#{self.class.name} tape=#{@tape ? @tape.class.name : 'none'} " \
69
+ "playing=#{@playing} motor=#{@motor}>"
70
+ end
71
+
72
+ private
73
+
74
+ def press(down)
75
+ return if down == @playing
76
+
77
+ @playing = down
78
+ @sense_handler&.call
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # The VICE debug cartridge register, mapped at $D7FF on the C64. It is a
5
+ # VICE facility rather than real hardware, enabled there with -debugcart:
6
+ # writing a byte ends the run with that value as the exit code. The VICE
7
+ # test suites write $00 for success and $ff for failure.
8
+ #
9
+ # Wraps the SID's top mirror page, so every other access falls through.
10
+ class DebugRegister
11
+ ADDRESS = 0xd7ff
12
+
13
+ def initialize(sid, &handler)
14
+ @sid = sid
15
+ @handler = handler
16
+ end
17
+
18
+ def peek(addr)
19
+ @sid.peek(addr)
20
+ end
21
+
22
+ def poke(addr, value)
23
+ return @handler.call(value) if addr == ADDRESS
24
+
25
+ @sid.poke(addr, value)
26
+ end
27
+ end
28
+ end
@@ -6,20 +6,42 @@ module Badline
6
6
  PAL_CLOCK_HZ = 985_248
7
7
  TITLE = "Badline"
8
8
  TOGGLE_SYM = SDL2::Key::TAB
9
+ REVERSE_MOD = SDL2::Key::Mod::SHIFT
9
10
 
10
- SHARED_KEYS = %i[up left cursor_h cursor_v space].freeze
11
+ SHARED_KEYS = %i[up left cursor_h cursor_v space w a s d lshift].freeze
11
12
 
12
- def initialize(media_path: nil, autostart: true, debug: false)
13
- @computer = Computer.new(debug:)
14
- puts Media.attach(@computer, media_path, autostart:) if media_path
13
+ # Tab steps through the input modes and shift-Tab back; the title bar
14
+ # names the live one. The pot devices appear once per control port, since
15
+ # games disagree on which one they read.
16
+ MODES = {
17
+ keyboard: nil, joystick: "JOY",
18
+ mouse1: "MOUSE 1", mouse2: "MOUSE 2",
19
+ paddles1: "PADDLE 1", paddles2: "PADDLE 2"
20
+ }.freeze
15
21
 
16
- @joystick_mode = false
22
+ # Both pot devices take their input from the host mouse. Motion turns the
23
+ # paddle knobs or steps the 1351's counters, and the host buttons go to
24
+ # whichever lines the device puts them on.
25
+ POT_DEVICES = {
26
+ mouse1: [Input::Mouse1351, 1], mouse2: [Input::Mouse1351, 2],
27
+ paddles1: [Input::Paddles, 1], paddles2: [Input::Paddles, 2]
28
+ }.freeze
29
+ MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
30
+
31
+ def initialize(media_path: nil, autostart: true, song: nil, sid_model: nil, debug: false)
32
+ @computer = Computer.new(debug:, sid_model: sid_model || Media.sid_model(media_path))
33
+ puts Media.attach(@computer, media_path, autostart:, song:) if media_path
34
+
35
+ @mode = :keyboard
36
+ @pot_device = nil
17
37
  @panes = [ScreenPane.new(@computer)]
18
38
  @window = Window.new(
19
39
  title: TITLE,
20
40
  width: canvas_width, height: canvas_height,
21
41
  vsync: ENV["NOVSYNC"].nil?
22
42
  )
43
+ @gamepads = Gamepads.new(@computer)
44
+ @gamepads.names.each { |name| puts "Gamepad: #{name}" }
23
45
 
24
46
  rate = @window.refresh_rate
25
47
  @cycles_per_frame = PAL_CLOCK_HZ / rate
@@ -30,10 +52,12 @@ module Badline
30
52
  @running = true
31
53
  while @running
32
54
  handle_events
55
+ @gamepads.poll
33
56
  @cycles_per_frame.times { @computer.cycle! }
34
57
  @window.draw(@panes)
35
58
  end
36
59
  ensure
60
+ @gamepads.close
37
61
  puts @computer.cpu.inspect
38
62
  end
39
63
 
@@ -48,15 +72,22 @@ module Badline
48
72
  handle_key_down(event)
49
73
  when SDL2::Event::KeyUp
50
74
  handle_key_up(event)
75
+ when SDL2::Event::MouseMotion
76
+ @pot_device&.move(event.xrel, event.yrel)
77
+ when SDL2::Event::MouseButton
78
+ handle_mouse_button(event)
79
+ when SDL2::Event::ControllerDevice
80
+ @gamepads.rescan
51
81
  end
52
82
  end
53
83
  end
54
84
 
55
85
  def handle_key_down(event)
56
- if event.sym == TOGGLE_SYM
57
- toggle_joystick_mode
58
- elsif @joystick_mode && (dir = JoyMap.parse(event))
59
- @computer.joystick2.press(dir)
86
+ return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
87
+
88
+ port, dir = JoyMap.parse(event) if @mode == :joystick
89
+ if port
90
+ joystick(port).press(dir)
60
91
  else
61
92
  @computer.keyboard.press(KeyMap.parse(event))
62
93
  end
@@ -65,21 +96,51 @@ module Badline
65
96
  def handle_key_up(event)
66
97
  return if event.sym == TOGGLE_SYM
67
98
 
68
- if @joystick_mode && (dir = JoyMap.parse(event))
69
- @computer.joystick2.release(dir)
99
+ port, dir = JoyMap.parse(event) if @mode == :joystick
100
+ if port
101
+ joystick(port).release(dir)
70
102
  else
71
103
  @computer.keyboard.release(KeyMap.parse(event))
72
104
  end
73
105
  end
74
106
 
75
- def toggle_joystick_mode
76
- @joystick_mode = !@joystick_mode
77
- if @joystick_mode
78
- SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
79
- else
80
- Joystick::DIRECTIONS.each_key { |dir| @computer.joystick2.release(dir) }
107
+ def handle_mouse_button(event)
108
+ button = MOUSE_BUTTONS[event.button]
109
+ return unless button && @pot_device
110
+
111
+ case event
112
+ when SDL2::Event::MouseButtonDown then @pot_device.press(button)
113
+ when SDL2::Event::MouseButtonUp then @pot_device.release(button)
114
+ end
115
+ end
116
+
117
+ def joystick(port)
118
+ port == 1 ? @computer.joystick1 : @computer.joystick2
119
+ end
120
+
121
+ def cycle_mode(step)
122
+ modes = MODES.keys
123
+ @mode = modes[(modes.index(@mode) + step) % modes.size]
124
+ release_inputs
125
+ attach_pot_device
126
+ @window.title = [TITLE, MODES[@mode] && "[#{MODES[@mode]}]"].compact.join(" ")
127
+ end
128
+
129
+ def attach_pot_device
130
+ device_class, port = POT_DEVICES[@mode]
131
+ @pot_device = device_class&.new
132
+ ports = @computer.control_ports
133
+ ports.device1 = port == 1 ? @pot_device : nil
134
+ ports.device2 = port == 2 ? @pot_device : nil
135
+ SDL2::Mouse.relative_mode = !@pot_device.nil?
136
+ end
137
+
138
+ def release_inputs
139
+ SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
140
+ Joystick::DIRECTIONS.each_key do |dir|
141
+ @computer.joystick1.release(dir)
142
+ @computer.joystick2.release(dir)
81
143
  end
82
- @window.title = @joystick_mode ? "#{TITLE} [JOY]" : TITLE
83
144
  end
84
145
 
85
146
  def canvas_width
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module GUI
5
+ # Host game controllers wired onto the control ports.
6
+ #
7
+ # The first controller found drives joystick 2, the port most games read;
8
+ # a second one drives joystick 1. D-pad and left stick both steer, every
9
+ # face and shoulder button fires.
10
+ #
11
+ # SDL announces arrivals by device index but reports button presses by
12
+ # instance id, and the binding gives no way to line the two up, so this
13
+ # class polls the controllers every frame. It only releases directions it
14
+ # pressed itself, so the keyboard mapping keeps working with a pad
15
+ # connected.
16
+ class Gamepads
17
+ DEADZONE = 8_000
18
+ PORTS = [2, 1].freeze
19
+
20
+ BUTTONS = {
21
+ up: [SDL2::GameController::Button::DPAD_UP],
22
+ down: [SDL2::GameController::Button::DPAD_DOWN],
23
+ left: [SDL2::GameController::Button::DPAD_LEFT],
24
+ right: [SDL2::GameController::Button::DPAD_RIGHT],
25
+ fire: [SDL2::GameController::Button::A, SDL2::GameController::Button::B,
26
+ SDL2::GameController::Button::X, SDL2::GameController::Button::Y,
27
+ SDL2::GameController::Button::LEFTSHOULDER,
28
+ SDL2::GameController::Button::RIGHTSHOULDER]
29
+ }.freeze
30
+
31
+ AXES = {
32
+ up: [SDL2::GameController::Axis::LEFTY, -1],
33
+ down: [SDL2::GameController::Axis::LEFTY, 1],
34
+ left: [SDL2::GameController::Axis::LEFTX, -1],
35
+ right: [SDL2::GameController::Axis::LEFTX, 1]
36
+ }.freeze
37
+
38
+ def initialize(computer)
39
+ SDL2.init(SDL2::INIT_GAMECONTROLLER)
40
+ @computer = computer
41
+ @controllers = []
42
+ @pressed = Array.new(PORTS.size) { [] }
43
+ rescan
44
+ end
45
+
46
+ def names = @controllers.map(&:name)
47
+
48
+ def rescan
49
+ close
50
+ @controllers = (0...SDL2::Joystick.num_connected_joysticks)
51
+ .select { |index| SDL2::Joystick.game_controller?(index) }
52
+ .first(PORTS.size)
53
+ .map { |index| SDL2::GameController.open(index) }
54
+ end
55
+
56
+ def poll
57
+ @controllers.each_with_index { |controller, index| apply(controller, index) }
58
+ end
59
+
60
+ def close
61
+ @controllers.each { |controller| controller.destroy unless controller.destroy? }
62
+ @controllers = []
63
+ @pressed.size.times { |index| release(index) }
64
+ end
65
+
66
+ private
67
+
68
+ def apply(controller, index)
69
+ active = Joystick::DIRECTIONS.keys.select { |dir| active?(controller, dir) }
70
+ (@pressed[index] - active).each { |dir| joystick(index).release(dir) }
71
+ active.each { |dir| joystick(index).press(dir) }
72
+ @pressed[index] = active
73
+ end
74
+
75
+ def release(index)
76
+ @pressed[index].each { |dir| joystick(index).release(dir) }
77
+ @pressed[index] = []
78
+ end
79
+
80
+ def active?(controller, direction)
81
+ BUTTONS.fetch(direction).any? { |button| controller.button_pressed?(button) } ||
82
+ tilted?(controller, direction)
83
+ end
84
+
85
+ def tilted?(controller, direction)
86
+ axis, sign = AXES[direction]
87
+ return false unless axis
88
+
89
+ controller.axis(axis) * sign > DEADZONE
90
+ end
91
+
92
+ def joystick(index)
93
+ PORTS[index] == 1 ? @computer.joystick1 : @computer.joystick2
94
+ end
95
+ end
96
+ end
97
+ end
@@ -2,17 +2,25 @@
2
2
 
3
3
  module Badline
4
4
  module GUI
5
+ # Maps host keys onto the two control ports while joystick mode is on.
6
+ #
7
+ # The arrow cluster plus space drives joystick 2, WASD plus left shift
8
+ # joystick 1, so both ports can be played at once.
5
9
  class JoyMap
6
- MAP = {
7
- "Up" => :up,
8
- "Down" => :down,
9
- "Left" => :left,
10
- "Right" => :right,
11
- "Space" => :fire
10
+ PORTS = {
11
+ 2 => { "Up" => :up, "Down" => :down, "Left" => :left, "Right" => :right, "Space" => :fire }.freeze,
12
+ 1 => { "W" => :up, "S" => :down, "A" => :left, "D" => :right, "Left Shift" => :fire }.freeze
12
13
  }.freeze
13
14
 
14
15
  def self.parse(event)
15
- MAP[SDL2::Key.name_of(event.sym)]
16
+ name = SDL2::Key.name_of(event.sym)
17
+
18
+ PORTS.each do |port, map|
19
+ direction = map[name]
20
+ return [port, direction] if direction
21
+ end
22
+
23
+ nil
16
24
  end
17
25
  end
18
26
  end
data/lib/badline/gui.rb CHANGED
@@ -5,6 +5,7 @@ require "sdl2"
5
5
  require "badline/gui/palette"
6
6
  require "badline/gui/key_map"
7
7
  require "badline/gui/joy_map"
8
+ require "badline/gui/gamepads"
8
9
  require "badline/gui/pane"
9
10
  require "badline/gui/screen_pane"
10
11
  require "badline/gui/window"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Input
5
+ # Commodore 1351 mouse in proportional mode.
6
+ #
7
+ # Each axis drives a 6-bit counter that the mouse presents on POTX/POTY
8
+ # shifted up one bit. A driver recovers movement by masking off bit 7,
9
+ # subtracting the previous reading and halving the difference. The counters
10
+ # wrap, so a single reading says nothing on its own, and the driver has to
11
+ # sample before the mouse travels 32 counts.
12
+ #
13
+ # #move takes host deltas with y counting down the screen; the counter
14
+ # counts the other way. The left button sits on the joystick fire line, the
15
+ # right button on the up line.
16
+ class Mouse1351
17
+ BUTTONS = { left: :fire, right: :up }.freeze
18
+
19
+ def initialize
20
+ @lines = Joystick.new
21
+ @x = 0
22
+ @y = 0
23
+ end
24
+
25
+ def press(button) = @lines.press(BUTTONS[button])
26
+
27
+ def release(button) = @lines.release(BUTTONS[button])
28
+
29
+ def port_bits = @lines.port_bits
30
+
31
+ def move(delta_x, delta_y)
32
+ @x = (@x + delta_x) & 0x3f
33
+ @y = (@y - delta_y) & 0x3f
34
+ end
35
+
36
+ def pot_x = @x << 1
37
+
38
+ def pot_y = @y << 1
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Input
5
+ # A pair of paddles on one control port.
6
+ #
7
+ # Paddle A sits on POTX with its button on the joystick left line, paddle B
8
+ # on POTY with its button on the right line. Positions are the 0-255 counts
9
+ # SID reads back, and the knobs stop at either end.
10
+ #
11
+ # Buttons are named after the host mouse buttons, as on the 1351: the left
12
+ # one fires paddle A, the right one paddle B.
13
+ class Paddles
14
+ BUTTONS = { left: :left, right: :right }.freeze
15
+
16
+ attr_reader :pot_x, :pot_y
17
+
18
+ def initialize
19
+ @lines = Joystick.new
20
+ @pot_x = 0x80
21
+ @pot_y = 0x80
22
+ end
23
+
24
+ def press(button) = @lines.press(BUTTONS[button])
25
+
26
+ def release(button) = @lines.release(BUTTONS[button])
27
+
28
+ def port_bits = @lines.port_bits
29
+
30
+ def move(turn_a, turn_b)
31
+ @pot_x = (@pot_x + turn_a).clamp(0, 0xff)
32
+ @pot_y = (@pot_y + turn_b).clamp(0, 0xff)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/input/mouse1351"
4
+ require "badline/input/paddles"
@@ -15,19 +15,18 @@ module Badline
15
15
  # $79 - absolute_y - 4+ cycles
16
16
  # $7D - absolute_x - 4+ cycles
17
17
  def adc(_addr, value)
18
- v = resolve(value)
19
- result = a + v + status.carry
18
+ result = a + value + status.carry
20
19
  status.zero = result.nobits?(0xff)
21
20
 
22
21
  if status.decimal?
23
- result = (a & 0x0f) + (v & 0x0f) + status.carry
22
+ result = (a & 0x0f) + (value & 0x0f) + status.carry
24
23
  result += 0x06 if result > 0x09
25
24
  c = result > 0x0f ? 1 : 0
26
- result = (a & 0xf0) + (v & 0xf0) + (c << 4) + (result & 0x0f)
27
- update_calculation_flags(v, result)
25
+ result = (a & 0xf0) + (value & 0xf0) + (c << 4) + (result & 0x0f)
26
+ update_calculation_flags(value, result)
28
27
  result += 0x60 if result > 0x9f
29
28
  else
30
- update_calculation_flags(v, result)
29
+ update_calculation_flags(value, result)
31
30
  end
32
31
 
33
32
  status.carry = result > 0xff
@@ -46,9 +45,8 @@ module Badline
46
45
  # $D9 - absolute_y - 4+ cycles
47
46
  # $DD - absolute_x - 4+ cycles
48
47
  def cmp(_addr, value)
49
- v = resolve(value)
50
- status.carry = (@a >= v)
51
- update_number_flags(@a - v)
48
+ status.carry = (@a >= value)
49
+ update_number_flags(@a - value)
52
50
  end
53
51
 
54
52
  # Compare memory with X register.
@@ -58,9 +56,8 @@ module Badline
58
56
  # $E4 - zeropage - 3 cycles
59
57
  # $EC - absolute - 4 cycles
60
58
  def cpx(_addr, value)
61
- v = resolve(value)
62
- status.carry = @x >= v
63
- update_number_flags(@x - v)
59
+ status.carry = @x >= value
60
+ update_number_flags(@x - value)
64
61
  end
65
62
 
66
63
  # Compare memory with Y register.
@@ -70,9 +67,8 @@ module Badline
70
67
  # $C4 - zeropage - 3 cycles
71
68
  # $CC - absolute - 4 cycles
72
69
  def cpy(_addr, value)
73
- v = resolve(value)
74
- status.carry = @y >= v
75
- update_number_flags(@y - v)
70
+ status.carry = @y >= value
71
+ update_number_flags(@y - value)
76
72
  end
77
73
 
78
74
  # Subtract memory from accumulator with borrow.
@@ -87,20 +83,19 @@ module Badline
87
83
  # $F9 - absolute_y - 4+ cycles
88
84
  # $FD - absolute_x - 4+ cycles
89
85
  def sbc(_addr, value)
90
- v = resolve(value)
91
- v_inv = ~v & 0xff
86
+ inverted = ~value & 0xff
92
87
  carry = status.carry? ? 0 : -1
93
88
 
94
- result = a + v_inv + status.carry
89
+ result = a + inverted + status.carry
95
90
  status.zero = result.nobits?(0xff)
96
91
  status.carry = result > 0xff
97
- update_calculation_flags(v_inv, result)
92
+ update_calculation_flags(inverted, result)
98
93
 
99
94
  if status.decimal?
100
- al = (a & 0x0f) - (v & 0x0f) + carry
95
+ al = (a & 0x0f) - (value & 0x0f) + carry
101
96
  al = ((al - 0x06) & 0x0F) - 0x10 if al.negative?
102
97
 
103
- result = (a & 0xf0) - (v & 0xf0) + al
98
+ result = (a & 0xf0) - (value & 0xf0) + al
104
99
  result -= 0x60 if result.negative?
105
100
  end
106
101
 
@@ -15,7 +15,7 @@ module Badline
15
15
  # $39 - absolute_y - 4+ cycles
16
16
  # $3D - absolute_x - 4+ cycles
17
17
  def and(_addr, value)
18
- @a &= resolve(value)
18
+ @a &= value
19
19
  update_number_flags(@a)
20
20
  end
21
21
 
@@ -28,10 +28,9 @@ module Badline
28
28
  # $16 - zeropage_x - 6 cycles
29
29
  # $1E - absolute_x - 7 cycles
30
30
  def asl(addr, value)
31
- v = resolve(value)
32
- result = (v << 1) & 0xff
33
- status.carry = v[7]
34
- write_modified(addr, v, result)
31
+ result = (value << 1) & 0xff
32
+ status.carry = value[7]
33
+ write_modified(addr, result)
35
34
  update_number_flags(result)
36
35
  end
37
36
 
@@ -41,10 +40,9 @@ module Badline
41
40
  # $24 - zeropage - 3 cycles
42
41
  # $2C - absolute - 4 cycles
43
42
  def bit(_addr, value)
44
- v = resolve(value)
45
43
  status.value = ((status.value & 0b00111111) +
46
- (v & 0b11000000)).to_i
47
- status.zero = (a & v).nobits?(0xff)
44
+ (value & 0b11000000)).to_i
45
+ status.zero = (a & value).nobits?(0xff)
48
46
  end
49
47
 
50
48
  # Performs bitwise XOR on accumulator with memory.
@@ -59,7 +57,7 @@ module Badline
59
57
  # $59 - absolute_y - 4+ cycles
60
58
  # $5D - absolute_x - 4+ cycles
61
59
  def eor(_addr, value)
62
- @a = (@a ^ resolve(value)) & 0xff
60
+ @a = (@a ^ value) & 0xff
63
61
  update_number_flags(@a)
64
62
  end
65
63
 
@@ -72,10 +70,9 @@ module Badline
72
70
  # $56 - zeropage_x - 6 cycles
73
71
  # $5E - absolute_x - 7 cycles
74
72
  def lsr(addr, value)
75
- v = resolve(value)
76
- result = (v >> 1) & 0xff
77
- status.carry = v[0]
78
- write_modified(addr, v, result)
73
+ result = (value >> 1) & 0xff
74
+ status.carry = value[0]
75
+ write_modified(addr, result)
79
76
  update_number_flags(result)
80
77
  end
81
78
 
@@ -91,7 +88,7 @@ module Badline
91
88
  # $19 - absolute_y - 4+ cycles
92
89
  # $1D - absolute_x - 4+ cycles
93
90
  def ora(_addr, value)
94
- @a |= resolve(value)
91
+ @a |= value
95
92
  update_number_flags(@a)
96
93
  end
97
94
 
@@ -104,10 +101,9 @@ module Badline
104
101
  # $36 - zeropage_x - 6 cycles
105
102
  # $3E - absolute_x - 7 cycles
106
103
  def rol(addr, value)
107
- v = resolve(value)
108
- result = ((v << 1) + status.carry) & 0xff
109
- status.carry = v[7]
110
- write_modified(addr, v, result)
104
+ result = ((value << 1) + status.carry) & 0xff
105
+ status.carry = value[7]
106
+ write_modified(addr, result)
111
107
  update_number_flags(result)
112
108
  end
113
109
 
@@ -120,10 +116,9 @@ module Badline
120
116
  # $76 - zeropage_x - 6 cycles
121
117
  # $7E - absolute_x - 7 cycles
122
118
  def ror(addr, value)
123
- v = resolve(value)
124
- result = ((v >> 1) + (status.carry? ? 0x80 : 0)) & 0xff
125
- status.carry = v[0]
126
- write_modified(addr, v, result)
119
+ result = ((value >> 1) + (status.carry? ? 0x80 : 0)) & 0xff
120
+ status.carry = value[0]
121
+ write_modified(addr, result)
127
122
  update_number_flags(result)
128
123
  end
129
124
  end