badline 0.1.0 → 0.2.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -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 +96 -25
  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 +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -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 +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  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 +169 -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 +109 -168
  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 +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
data/lib/badline/cpu.rb CHANGED
@@ -1,35 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/cpu/microcode"
4
+ require "badline/cpu/addressing"
5
+ require "badline/cpu/operations"
6
+ require "badline/cpu/stack_operations"
7
+
3
8
  module Badline
4
- class CPU < Cycleable
5
- STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1,
6
- :overflow, :negative].freeze
9
+ class CPU
10
+ STATUS_FLAGS = [:carry, :zero, :interrupt, :decimal, :break, 1, :overflow, :negative].freeze
7
11
 
8
- class InvalidOpcodeError < StandardError; end
9
12
  include IntegerHelper
10
13
  include InstructionSet
14
+ include Interrupts
11
15
  include Traps
16
+ include Addressing
17
+ include Operations
18
+ include StackOperations
19
+
20
+ attr_reader :memory, :instructions, :boundary_crossed, :cycles, :pending_write
21
+ attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y, :nmi, :irq
12
22
 
13
- attr_reader :memory, :instructions, :boundary_crossed
14
- attr_accessor :program_counter, :stack_pointer, :status, :a, :x, :y,
15
- :nmi, :irq
23
+ alias pending_write? pending_write
16
24
 
17
- def initialize(memory = nil, debug: false)
25
+ # +ane_constant+ is ANE's magic constant, which varies from chip to
26
+ # chip. The default is the C64 6510's.
27
+ def initialize(memory = nil, debug: false, ane_constant: 0xef)
18
28
  @debug = debug
29
+ @ane_constant = ane_constant
19
30
  @memory = memory || Memory.new
20
- @status = Status.new(STATUS_FLAGS, value: 0b00100000)
31
+ @status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
21
32
  reset_registers
22
33
 
23
34
  @nmi = @irq = false
35
+ @irq_sample = @irq_pending = false
36
+ @nmi_sample = @nmi_pending = false
37
+ @skip_poll = false
38
+ @boundary_crossed = false
39
+ @interrupt = nil
40
+ @brk = false
41
+ @pending_write = false
42
+ @stalled_at = nil
24
43
 
44
+ @cycles = 0
25
45
  @instructions = 0
26
46
  @traps = nil
27
- super()
47
+ end_sequence
28
48
  end
29
49
 
30
50
  def reset!
31
51
  status.interrupt = true
32
52
  reset_registers
53
+ @nmi = false
54
+ @irq_sample = @irq_pending = false
55
+ @nmi_sample = @nmi_pending = false
56
+ @skip_poll = false
57
+ @pending_write = false
58
+ end_sequence
33
59
  end
34
60
 
35
61
  def p
@@ -40,9 +66,37 @@ module Badline
40
66
  status.value = new_value
41
67
  end
42
68
 
69
+ def cycle!
70
+ poll # the interrupt lines
71
+
72
+ # Run the next step
73
+ index = @index
74
+ @index = index + 1
75
+ send(@plan[index])
76
+ @cycles += 1
77
+
78
+ # Record if the next step writes to memory
79
+ @pending_write = @writes[@index]
80
+ nil
81
+ end
82
+
83
+ # Called instead of #cycle! on a cycle the VIC holds the CPU through
84
+ # BA. Records which cycle the CPU was stalled before, and keeps
85
+ # sampling the interrupt lines.
86
+ def stall!
87
+ @stalled_at = @cycles
88
+ sample_while_stalled
89
+ end
90
+
91
+ def jammed?
92
+ @plan.equal?(JAMMED_PLAN)
93
+ end
94
+
95
+ # Runs to the end of the instruction, or until the CPU jams.
43
96
  def step!
44
- @loop.resume unless @instruction || @interrupt
45
- cycle! while @instruction || @interrupt
97
+ cycle!
98
+ cycle! until @plan.equal?(FETCH_PLAN) || jammed?
99
+ nil
46
100
  end
47
101
 
48
102
  def inspect
@@ -53,66 +107,58 @@ module Badline
53
107
 
54
108
  private
55
109
 
56
- def extra_cycle(instruction)
57
- return cycle unless instruction.boundary_cycle?
58
-
59
- boundary_crossed && cycle
60
- end
61
-
62
- def handle_interrupt(vector, brk: false, pre_cycles: 2)
63
- pre_cycles.times { cycle }
64
-
65
- pc = program_counter
66
- pc = (pc + 1) & 0xffff if brk
110
+ # The first cycle of every instruction. Starts an interrupt if one was
111
+ # pending when the previous instruction ended. Otherwise reads the
112
+ # opcode and switches to its plan.
113
+ def op_fetch
114
+ return start_interrupt if @boundary_nmi || @boundary_irq
67
115
 
68
- write_byte(stack_address, high_byte(pc))
69
- @stack_pointer = (@stack_pointer - 1) & 0xff
70
- write_byte(stack_address, low_byte(pc))
71
- @stack_pointer = (@stack_pointer - 1) & 0xff
72
- write_byte(stack_address,
73
- status.clone.tap { |s| s.break = brk }.value)
74
- @stack_pointer = (@stack_pointer - 1) & 0xff
75
- status.interrupt = true
76
- @program_counter = read_word(vector)
116
+ run_traps
117
+ opcode = @memory.peek(@program_counter)
118
+ log(opcode) if @debug
119
+ micro = MICROCODE[opcode]
120
+ @operation = micro.operation
121
+ @plan = micro.plan
122
+ @writes = micro.writes
123
+ @optional_dummy = micro.optional_dummy
124
+ @index = 1
125
+ @boundary_crossed = false
126
+ @program_counter = (@program_counter + 1) & 0xffff
77
127
  end
78
128
 
79
- def handle_interrupts
80
- @interrupt = if nmi
81
- 0xfffa
82
- elsif irq && !status.interrupt?
83
- 0xfffe
84
- end
85
-
86
- handle_interrupt(@interrupt) if @interrupt
87
- @interrupt = nil
88
- @nmi = @irq = false
129
+ def end_instruction
130
+ @instructions += 1
131
+ end_sequence
89
132
  end
90
133
 
91
- def read_byte(addr)
92
- cycle { @memory.peek(addr) }
134
+ # Makes the next cycle an opcode fetch, and saves the pending interrupt
135
+ # flags for op_fetch. The next cycle polls before op_fetch runs, which
136
+ # updates the live flags.
137
+ def end_sequence
138
+ @plan = FETCH_PLAN
139
+ @writes = FETCH_WRITES
140
+ @index = 0
141
+ @boundary_irq = @irq_pending
142
+ @boundary_nmi = @nmi_pending
93
143
  end
94
144
 
95
- def read_word(addr)
96
- uint16(read_byte(addr),
97
- read_byte((addr + 1) & 0xffff))
145
+ # True when the CPU was stalled right before this cycle.
146
+ def stalled_before_this_cycle?
147
+ @stalled_at == @cycles
98
148
  end
99
149
 
100
- def read_zeropage_word(addr)
101
- uint16(read_byte(addr & 0xff),
102
- read_byte((addr + 1) & 0xff))
150
+ # True when the CPU was stalled right before the cycle that preceded
151
+ # this one.
152
+ def stalled_before_previous_cycle?
153
+ @stalled_at == @cycles - 1
103
154
  end
104
155
 
105
- def read_instruction
106
- Instruction.find(@memory.peek(@program_counter))
156
+ def write_byte(addr, value)
157
+ @memory.poke(addr, value)
107
158
  end
108
159
 
109
- def read_operand(instruction)
110
- return nil unless instruction.operand?
111
- # JSR fetches its operand high byte late (see Stack#jsr).
112
- return read_byte(program_counter) if instruction.name == :jsr
113
- return read_word(program_counter) if instruction.operand_length == 2
114
-
115
- read_byte(program_counter)
160
+ def stack_address(offset = 0)
161
+ 0x0100 | ((@stack_pointer + offset) & 0xff)
116
162
  end
117
163
 
118
164
  def reset_registers
@@ -123,117 +169,12 @@ module Badline
123
169
  @a = @x = @y = 0x0
124
170
  end
125
171
 
126
- def read_address(instruction, operand)
127
- case instruction.addressing_mode
128
- when :implied, :immediate
129
- nil
130
- when :accumulator
131
- :accumulator
132
- when :relative
133
- (@program_counter + signed_int8(operand) + 1) & 0xffff
134
- when :zeropage, :absolute
135
- operand
136
- when :zeropage_x
137
- cycle { (operand + @x) & 0xff }
138
- when :zeropage_y
139
- cycle { (operand + @y) & 0xff }
140
- when :absolute_x
141
- # Do an extra cycle if page boundary is crossed
142
- @boundary_crossed = high_byte(operand + @x) != high_byte(operand)
143
- extra_cycle(instruction)
144
- (operand + @x) & 0xffff
145
- when :absolute_y
146
- @boundary_crossed = high_byte(operand + @y) != high_byte(operand)
147
- # Do an extra cycle if page boundary is crossed
148
- extra_cycle(instruction)
149
- (operand + @y) & 0xffff
150
- when :indirect
151
- # This is only used for JMP. There's no carry associated, so an
152
- # indirect jump to $30FF will wrap around on the same page and read
153
- # from [0x30ff, 0x3000].
154
- uint16(
155
- read_byte(operand),
156
- read_byte(uint16(
157
- (low_byte(operand) + 1) & 0xff, # Wrap around low byte
158
- high_byte(operand)
159
- ))
160
- )
161
- when :indirect_x
162
- cycle
163
- read_zeropage_word(operand + @x)
164
- when :indirect_y
165
- value = read_zeropage_word(operand)
166
- @boundary_crossed = ((value & 0xff) + y) > 0xff
167
- # Do an extra cycle if page boundary is crossed
168
- extra_cycle(instruction)
169
- (value + y) & 0xffff
170
- end
171
- end
172
-
173
- def realize_value(instruction, operand, address)
174
- case instruction.addressing_mode
175
- when :implied
176
- raise "Implied value can't be realized"
177
- when :accumulator
178
- @a
179
- when :immediate
180
- operand
181
- else
182
- read_byte(address)
183
- end
184
- end
185
-
186
- def stack_address(offset = 0)
187
- uint16((stack_pointer + offset) & 0xff, 0x01)
188
- end
189
-
190
- def log(instruction, operand, address)
191
- return unless @debug
192
-
193
- pc = (@program_counter - 1) - instruction.operand_length
172
+ def log(opcode)
173
+ instruction = Instruction.find(opcode)
194
174
  puts(
195
- "#{@cycles}: " \
196
- "PC: #{pc.to_s(16)} - " \
197
- "#{@instruction.name.upcase} #{@instruction.addressing_mode} " \
198
- "Operand: #{operand.inspect} Address: #{address.inspect}"
175
+ "#{@cycles}: PC: #{format16(@program_counter)} - " \
176
+ "#{instruction.name.upcase} #{instruction.addressing_mode}"
199
177
  )
200
178
  end
201
-
202
- def main_loop
203
- if nmi || (irq && !status.interrupt?)
204
- handle_interrupts
205
- else
206
- run_traps
207
- @boundary_crossed = false
208
- @instruction = read_instruction
209
- raise InvalidOpcodeError unless @instruction
210
-
211
- @program_counter = (@program_counter + 1) & 0xffff
212
- @cycles += 1
213
-
214
- @operand = operand = read_operand(@instruction)
215
- @address = address = read_address(@instruction, operand)
216
-
217
- @program_counter = (@program_counter + @instruction.operand_length) &
218
- 0xffff
219
-
220
- log(@instruction, operand, address)
221
-
222
- # Run the instruction; :lazy realizes the value through #resolve
223
- send(@instruction.name, address, :lazy)
224
-
225
- @instructions += 1
226
- @instruction = nil
227
- end
228
- Fiber.yield
229
- end
230
-
231
- def write_byte(addr, value)
232
- if addr == :accumulator
233
- @a = value
234
- else
235
- cycle(write: true) { @memory.poke(addr, value) }
236
- end
237
- end
238
179
  end
239
180
  end
@@ -22,12 +22,15 @@ module Badline
22
22
  def cycle(write: false)
23
23
  @pending_write = write
24
24
  Fiber.yield
25
+ poll
25
26
  result = yield if block_given?
26
27
  @cycles += 1
27
28
  @pending_write = false
28
29
  result
29
30
  end
30
31
 
32
+ def poll; end
33
+
31
34
  def main_loop
32
35
  Fiber.yield
33
36
  end
@@ -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