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
@@ -11,9 +11,8 @@ module Badline
11
11
  # $D6 - zeropage_x - 6 cycles
12
12
  # $DE - absolute_x - 7 cycles
13
13
  def dec(addr, value)
14
- v = resolve(value)
15
- result = (v - 1) & 0xff
16
- write_modified(addr, v, result)
14
+ result = (value - 1) & 0xff
15
+ write_modified(addr, result)
17
16
  update_number_flags(result)
18
17
  end
19
18
 
@@ -22,7 +21,7 @@ module Badline
22
21
  # Opcodes:
23
22
  # $CA - implied - 2 cycles
24
23
  def dex(_addr, _value)
25
- cycle { @x = (@x - 1) & 0xff }
24
+ @x = (@x - 1) & 0xff
26
25
  update_number_flags(@x)
27
26
  end
28
27
 
@@ -31,7 +30,7 @@ module Badline
31
30
  # Opcodes:
32
31
  # $88 - implied - 2 cycles
33
32
  def dey(_addr, _value)
34
- cycle { @y = (@y - 1) & 0xff }
33
+ @y = (@y - 1) & 0xff
35
34
  update_number_flags(@y)
36
35
  end
37
36
 
@@ -43,9 +42,8 @@ module Badline
43
42
  # $F6 - zeropage_x - 6 cycles
44
43
  # $FE - absolute_x - 7 cycles
45
44
  def inc(addr, value)
46
- v = resolve(value)
47
- result = (v + 1) & 0xff
48
- write_modified(addr, v, result)
45
+ result = (value + 1) & 0xff
46
+ write_modified(addr, result)
49
47
  update_number_flags(result)
50
48
  end
51
49
 
@@ -54,7 +52,7 @@ module Badline
54
52
  # Opcodes:
55
53
  # $E8 - implied - 2 cycles
56
54
  def inx(_addr, _value)
57
- cycle { @x = (@x + 1) & 0xff }
55
+ @x = (@x + 1) & 0xff
58
56
  update_number_flags(@x)
59
57
  end
60
58
 
@@ -63,7 +61,7 @@ module Badline
63
61
  # Opcodes:
64
62
  # $C8 - implied - 2 cycles
65
63
  def iny(_addr, _value)
66
- cycle { @y = (@y + 1) & 0xff }
64
+ @y = (@y + 1) & 0xff
67
65
  update_number_flags(@y)
68
66
  end
69
67
  end
@@ -24,7 +24,7 @@ module Badline
24
24
  # Opcodes:
25
25
  # $68 - implied - 4 cycles
26
26
  def pla(_addr, _value)
27
- cycle { @a = stack_pull }
27
+ @a = stack_pull
28
28
  update_number_flags(@a)
29
29
  end
30
30
 
@@ -33,71 +33,19 @@ module Badline
33
33
  # Opcodes:
34
34
  # $28 - implied - 4 cycles
35
35
  def plp(_addr, _value)
36
- cycle { status.value = stack_pull & 0b11101111 }
37
- end
38
-
39
- # Jump to absolute address.
40
- #
41
- # Opcodes:
42
- # $4C - absolute - 3 cycles
43
- def jmp(addr, _value)
44
- @program_counter = addr
45
- end
46
-
47
- # Jump to subroutine. The operand high byte is fetched only after the
48
- # return address has been pushed, so a JSR executing inside the stack
49
- # jumps via the value its own push just wrote.
50
- #
51
- # Opcodes:
52
- # $20 - absolute - 6 cycles
53
- def jsr(addr, _value)
54
- return_addr = (program_counter - 1) & 0xffff
55
- cycle { @memory.peek(stack_address) } # internal cycle: dummy stack read
56
- write_byte(stack_address, high_byte(return_addr))
57
- write_byte(stack_address(-1), low_byte(return_addr))
58
- @stack_pointer = (@stack_pointer - 2) & 0xff
59
- @program_counter = uint16(addr, read_byte(return_addr))
60
- end
61
-
62
- # Return from interrupt.
63
- #
64
- # Opcodes:
65
- # $40 - implied - 6 cycles
66
- def rti(_addr, _value)
67
- cycle do
68
- @stack_pointer = (@stack_pointer + 1) & 0xff
69
- @status.value = memory[stack_address]
70
- @status.break = false
71
- end
72
- @program_counter = uint16(stack_pull, stack_pull)
73
- end
74
-
75
- # Return from subroutine.
76
- #
77
- # Opcodes:
78
- # $60 - implied - 6 cycles
79
- def rts(_addr, _value)
80
- cycle do
81
- @program_counter = (uint16(stack_pull, stack_pull) + 1) & 0xffff
82
- end
36
+ status.value = stack_pull & 0b11101111
83
37
  end
84
38
 
85
39
  private
86
40
 
87
41
  def stack_pull
88
- cycle { @stack_pointer = (@stack_pointer + 1) & 0xff }
89
- read_byte(stack_address)
42
+ @stack_pointer = (@stack_pointer + 1) & 0xff
43
+ @memory.peek(stack_address)
90
44
  end
91
45
 
92
46
  def stack_push(value)
93
47
  write_byte(stack_address, value)
94
- cycle { @stack_pointer = (@stack_pointer - 1) & 0xff }
95
- end
96
-
97
- def stack_push16(value)
98
- write_byte(stack_address, high_byte(value))
99
- write_byte(stack_address(-1), low_byte(value))
100
- cycle { @stack_pointer = (@stack_pointer - 2) & 0xff }
48
+ @stack_pointer = (@stack_pointer - 1) & 0xff
101
49
  end
102
50
  end
103
51
  end
@@ -16,7 +16,7 @@ module Badline
16
16
  # $B9 - absolute_y - 4+ cycles
17
17
  # $BD - absolute_x - 4+ cycles
18
18
  def lda(_addr, value)
19
- @a = resolve(value)
19
+ @a = value
20
20
  update_number_flags(@a)
21
21
  end
22
22
 
@@ -29,7 +29,7 @@ module Badline
29
29
  # $B6 - zeropage_y - 4 cycles
30
30
  # $BE - absolute_y - 4+ cycles
31
31
  def ldx(_addr, value)
32
- @x = resolve(value)
32
+ @x = value
33
33
  update_number_flags(@x)
34
34
  end
35
35
 
@@ -42,7 +42,7 @@ module Badline
42
42
  # $B4 - zeropage_x - 4 cycles
43
43
  # $BC - absolute_x - 4+ cycles
44
44
  def ldy(_addr, value)
45
- @y = resolve(value)
45
+ @y = value
46
46
  update_number_flags(@y)
47
47
  end
48
48
 
@@ -85,7 +85,7 @@ module Badline
85
85
  # Opcodes:
86
86
  # $AA - implied - 2 cycles
87
87
  def tax(_addr, _value)
88
- cycle { @x = a }
88
+ @x = a
89
89
  update_number_flags(@x)
90
90
  end
91
91
 
@@ -94,7 +94,7 @@ module Badline
94
94
  # Opcodes:
95
95
  # $A8 - implied - 2 cycles
96
96
  def tay(_addr, _value)
97
- cycle { @y = a }
97
+ @y = a
98
98
  update_number_flags(@y)
99
99
  end
100
100
 
@@ -103,7 +103,7 @@ module Badline
103
103
  # Opcodes:
104
104
  # $BA - implied - 2 cycles
105
105
  def tsx(_addr, _value)
106
- cycle { @x = stack_pointer }
106
+ @x = stack_pointer
107
107
  update_number_flags(@x)
108
108
  end
109
109
 
@@ -112,7 +112,7 @@ module Badline
112
112
  # Opcodes:
113
113
  # $8A - implied - 2 cycles
114
114
  def txa(_addr, _operand)
115
- cycle { @a = x }
115
+ @a = x
116
116
  update_number_flags(@a)
117
117
  end
118
118
 
@@ -121,7 +121,7 @@ module Badline
121
121
  # Opcodes:
122
122
  # $9A - implied - 2 cycles
123
123
  def txs(_addr, _operand)
124
- cycle { @stack_pointer = x }
124
+ @stack_pointer = x
125
125
  end
126
126
 
127
127
  # Transfer Y register to accumulator.
@@ -129,7 +129,7 @@ module Badline
129
129
  # Opcodes:
130
130
  # $98 - implied - 2 cycles
131
131
  def tya(_addr, _operand)
132
- cycle { @a = y }
132
+ @a = y
133
133
  update_number_flags(@a)
134
134
  end
135
135
  end
@@ -22,18 +22,8 @@ module Badline
22
22
  include InstructionSet::Stack
23
23
  include InstructionSet::Transfer
24
24
 
25
- # Forces a software interrupt (break).
26
- # Pushes PC+2 and status to stack, sets break flag, and jumps via IRQ vector.
27
- #
28
- # Opcodes:
29
- # $00 - implied - 7 cycles
30
- def brk(_addr, _value)
31
- status.break = true
32
- handle_interrupt(0xfffe, brk: true, pre_cycles: 1)
33
- status.break = false
34
- end
35
-
36
- # No operation. Does nothing but consume a clock cycle.
25
+ # No operation. The illegal forms with an operand still read it, in
26
+ # the steps before this is called.
37
27
  #
38
28
  # Opcodes:
39
29
  # $EA - implied - 2 cycles
@@ -42,35 +32,23 @@ module Badline
42
32
  # $0C - absolute - 4 cycles (illegal)
43
33
  # $1C, $3C, $5C, $7C, $DC, $FC - absolute_x - 4+ cycles (illegal)
44
34
  # $1A, $3A, $5A, $7A, $DA, $FA - implied - 2 cycles (illegal)
45
- def nop(_addr, _value)
46
- cycle
47
- end
35
+ def nop(_addr, _value); end
48
36
 
49
37
  private
50
38
 
51
- def resolve(value)
52
- if value == :lazy
53
- realize_value(@instruction, @operand, @address)
54
- elsif value.is_a?(Proc)
55
- value.call
56
- else
57
- value
58
- end
59
- end
60
-
61
39
  def update_number_flags(value)
62
40
  status.zero = value.zero?
63
- status.negative = value.anybits?(0x80)
41
+ status.negative = value & 0x80 != 0
64
42
  value
65
43
  end
66
44
 
67
- # Read-modify-write instructions put the unmodified value back on the bus before writing the result.
68
- def write_modified(addr, original, result)
45
+ # Stores a read-modify-write result. For memory, the write happens in
46
+ # the steps that follow (see Operations#op_rmw_read).
47
+ def write_modified(addr, result)
69
48
  if addr == :accumulator
70
- cycle { @a = result }
49
+ @a = result
71
50
  else
72
- write_byte(addr, original)
73
- write_byte(addr, result)
51
+ @rmw_result = result
74
52
  end
75
53
  end
76
54
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Interrupts
5
+ BREAK_MASK = 0x10
6
+
7
+ private
8
+
9
+ # Samples the IRQ line and the NMI latch once per cycle. An instruction
10
+ # boundary commits the sample taken on its second-to-last cycle.
11
+ def poll
12
+ if @skip_poll
13
+ @skip_poll = false
14
+ return
15
+ end
16
+ @irq_pending = @irq_sample
17
+ @nmi_pending = @nmi_sample
18
+ @irq_sample = @irq && !@status.interrupt?
19
+ @nmi_sample = @nmi
20
+ end
21
+
22
+ # A cycle the VIC stalls through BA keeps sampling the lines, OR-ed with
23
+ # the sample it already holds, but the boundary pipeline doesn't advance
24
+ # and a skipped poll stays owed to the next real cycle.
25
+ def sample_while_stalled
26
+ @irq_sample ||= @irq && !interrupt_disabled_after_stall?
27
+ @nmi_sample = true if @nmi
28
+ end
29
+
30
+ # A stalled CLI or SEI execute cycle masks with the I it is about to
31
+ # set. Every other step masks with the current I, PLP included: its
32
+ # pulled I only arrives with the stalled stack read.
33
+ def interrupt_disabled_after_stall?
34
+ return @status.interrupt? unless @plan.equal?(CPU::IMPLIED_PLAN)
35
+
36
+ case @operation
37
+ when :cli then false
38
+ when :sei then true
39
+ else @status.interrupt?
40
+ end
41
+ end
42
+
43
+ # Runs in the opcode fetch cycle in place of the fetch, with no bus
44
+ # access, and switches to the interrupt plan.
45
+ def start_interrupt
46
+ @interrupt = @boundary_nmi ? 0xfffa : 0xfffe
47
+ @brk = false
48
+ @plan = CPU::INTERRUPT_PLAN
49
+ @writes = CPU::INTERRUPT_WRITES
50
+ @index = 1
51
+ end
52
+
53
+ # BRK reads the byte after its opcode and ignores it, and selects the
54
+ # IRQ vector.
55
+ def op_brk_dummy
56
+ @memory.peek(@program_counter)
57
+ @interrupt = 0xfffe
58
+ @brk = true
59
+ end
60
+
61
+ def op_int_push_pch
62
+ @address = @brk ? (@program_counter + 1) & 0xffff : @program_counter
63
+ @memory.poke(stack_address, high_byte(@address))
64
+ @stack_pointer = (@stack_pointer - 1) & 0xff
65
+ end
66
+
67
+ def op_int_push_pcl
68
+ @memory.poke(stack_address, low_byte(@address))
69
+ @stack_pointer = (@stack_pointer - 1) & 0xff
70
+ end
71
+
72
+ def op_int_push_status
73
+ value = @brk ? @status.value | BREAK_MASK : @status.value & ~BREAK_MASK
74
+ @memory.poke(stack_address, value)
75
+ @stack_pointer = (@stack_pointer - 1) & 0xff
76
+ @status.interrupt = true
77
+ # An NMI that arrives before cycle 4 of a BRK or IRQ takes it over:
78
+ # the sequence finishes with the NMI vector.
79
+ @interrupt = 0xfffa if @interrupt == 0xfffe && @nmi_pending
80
+ @nmi = false if @interrupt == 0xfffa
81
+ end
82
+
83
+ def op_int_vector_low
84
+ @address = @memory.peek(@interrupt)
85
+ end
86
+
87
+ # Clears any interrupt that is pending, so the handler's first
88
+ # instruction always runs before the next interrupt. This applies to
89
+ # BRK too.
90
+ def op_int_vector_high
91
+ high = @memory.peek((@interrupt + 1) & 0xffff)
92
+ @program_counter = @address | (high << 8)
93
+ @irq_sample = @irq_pending = false
94
+ @nmi_sample = @nmi_pending = false
95
+ @interrupt = nil
96
+ if @brk
97
+ @status.break = false
98
+ end_instruction
99
+ else
100
+ end_sequence
101
+ end
102
+ end
103
+ end
104
+ end
@@ -4,22 +4,22 @@ module Badline
4
4
  class Joystick
5
5
  DIRECTIONS = { up: 0, down: 1, left: 2, right: 3, fire: 4 }.freeze
6
6
 
7
+ # Kept up to date as switches move, since the CIA samples the fire line
8
+ # every cycle to drive the light pen input.
9
+ attr_reader :port_bits
10
+
7
11
  def initialize
8
- @pressed = {}
12
+ @port_bits = 0xff
9
13
  end
10
14
 
11
15
  def press(direction)
12
- @pressed[direction] = true if DIRECTIONS.key?(direction)
16
+ bit = DIRECTIONS[direction]
17
+ @port_bits &= ~(1 << bit) if bit
13
18
  end
14
19
 
15
20
  def release(direction)
16
- @pressed.delete(direction)
17
- end
18
-
19
- def port_bits
20
- bits = 0xff
21
- DIRECTIONS.each { |dir, bit| bits &= ~(1 << bit) if @pressed[dir] }
22
- bits
21
+ bit = DIRECTIONS[direction]
22
+ @port_bits |= 1 << bit if bit
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # One open channel on the virtual drive, holding a file, a block buffer
6
+ # or the command channel's status message. It gives out one byte at a
7
+ # time and flags the last one, so the bus can raise EOI with it. A
8
+ # channel cut short by a read error never flags one, and holds the
9
+ # error for the drive to report once its bytes run out.
10
+ class Channel
11
+ attr_accessor :pointer
12
+ attr_reader :error
13
+
14
+ def initialize(bytes = [], error: nil)
15
+ replace(bytes)
16
+ @error = error
17
+ end
18
+
19
+ # A file's bytes, up to the first block its chain can't read. The
20
+ # drive reads a block ahead before sending the last byte of the one
21
+ # before, so a bad block holds that byte back too, and a bad first
22
+ # block or a missing file leaves nothing to send.
23
+ def self.for_file(storage, name, type)
24
+ data = storage.read_file(name, type:)
25
+ return new([], error: [Drive::FILE_NOT_FOUND]) unless data
26
+
27
+ failure = storage.respond_to?(:read_error) && storage.read_error(name, type:)
28
+ return new(data) unless failure
29
+
30
+ new(data[0, failure[:offset] - 1] || [], error: failure.values_at(:error, :track, :sector))
31
+ end
32
+
33
+ def replace(bytes)
34
+ @bytes = bytes
35
+ @pointer = 0
36
+ end
37
+
38
+ def exhausted?
39
+ @pointer >= @bytes.length
40
+ end
41
+
42
+ def read
43
+ return if exhausted?
44
+
45
+ byte = @bytes[@pointer]
46
+ @pointer += 1
47
+ [byte, exhausted? && !@error]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ class Drive
6
+ # The command channel's side of the drive: the last DOS error with the
7
+ # track and sector it happened at, or the bytes an M-R read. Once its
8
+ # last byte has been read, it holds OK again.
9
+ class Status
10
+ MESSAGES = [20, 21, 22, 23, 24, 27].to_h { |code| [code, "READ ERROR"] }.merge(
11
+ 0 => " OK",
12
+ 25 => "WRITE ERROR",
13
+ 26 => "WRITE PROTECT ON",
14
+ 28 => "WRITE ERROR",
15
+ 29 => "DISK ID MISMATCH",
16
+ 30 => "SYNTAX ERROR",
17
+ 62 => "FILE NOT FOUND",
18
+ 63 => "FILE EXISTS",
19
+ 66 => "ILLEGAL TRACK OR SECTOR",
20
+ 70 => "NO CHANNEL",
21
+ 73 => "CBM DOS V2.6 1541",
22
+ 74 => "DRIVE NOT READY"
23
+ ).freeze
24
+
25
+ def initialize
26
+ @channel = Channel.new
27
+ end
28
+
29
+ def report(code, track = 0, sector = 0)
30
+ message = format("%<code>02d,%<message>s,%<track>02d,%<sector>02d\r",
31
+ code:, message: MESSAGES.fetch(code),
32
+ track: track.to_i, sector: sector.to_i)
33
+ @channel.replace(message.bytes)
34
+ nil
35
+ end
36
+
37
+ def replace(bytes)
38
+ @channel.replace(bytes)
39
+ end
40
+
41
+ def read
42
+ result = @channel.read
43
+ report(0) if result&.last
44
+ result
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end