badline 0.17.0 → 0.18.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -153,6 +153,19 @@ module Badline
153
153
  6 => :cbm, 8 => :ce, 10 => :norm_graph, 21 => :off_rvs, 19 => :pi, 39 => :kp00, 66 => :f9, 67 => :f10
154
154
  }.freeze
155
155
 
156
+ # The KIM-1's keys, by the names KIM1::Panel::KEYS gives them, with
157
+ # ST, RS and the SST switch: the hex keys on the digits, the keypad's
158
+ # and A-F, AD on comma, DA on period, + on space, = and the keypad's
159
+ # +, GO on G, Return and Enter, PC on P, ST on S and Esc, RS on R and
160
+ # SST on T.
161
+ KIM1_KEYS = {
162
+ 4 => "A", 5 => "B", 6 => "C", 7 => "D", 8 => "E", 9 => "F", 30 => "1", 31 => "2", 32 => "3", 33 => "4",
163
+ 34 => "5", 35 => "6", 36 => "7", 37 => "8", 38 => "9", 39 => "0", 89 => "1", 90 => "2", 91 => "3",
164
+ 92 => "4", 93 => "5", 94 => "6", 95 => "7", 96 => "8", 97 => "9", 98 => "0", 54 => "AD", 55 => "DA",
165
+ 44 => "+", 46 => "+", 87 => "+", 10 => "GO", 40 => "GO", 88 => "GO", 19 => "PC", 22 => "ST", 41 => "ST",
166
+ 21 => "RS", 23 => "SST"
167
+ }.freeze
168
+
156
169
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
157
170
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
158
171
 
@@ -249,6 +262,29 @@ module Badline
249
262
  end
250
263
  end
251
264
 
265
+ # What the KIM-1's teletype sends for the keys that type no
266
+ # character: RETURN, RUBOUT on Backspace and Delete, and LINE FEED on
267
+ # Down.
268
+ KIM1_TELETYPE_KEYS = { 40 => "\r", 88 => "\r", 42 => "\x7f", 76 => "\x7f", 81 => "\n" }.freeze
269
+
270
+ # What the KIM-1's teletype sends for the scancode, with the host's
271
+ # SHIFT down if `shift`: a key of KIM1_TELETYPE_KEYS, or the
272
+ # character the key types on a US layout, or an empty string.
273
+ def self.kim1_teletype(scancode, shift)
274
+ sent = KIM1_TELETYPE_KEYS[scancode]
275
+ return sent unless sent.nil?
276
+ return " " if scancode == SPACE
277
+ return character(scancode, shift) if scancode.between?(4, 29)
278
+
279
+ characters = US_CHARACTERS[scancode]
280
+ return "" if characters.nil?
281
+
282
+ characters[shift ? 1 : 0]
283
+ end
284
+
285
+ # The KIM-1 key for the scancode, or an empty string.
286
+ def self.kim1_key(scancode) = KIM1_KEYS[scancode] || ""
287
+
252
288
  def self.arrows(scancode) = ARROWS[scancode] || :none
253
289
 
254
290
  def self.wasd(scancode) = WASD[scancode] || :none
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The KIM-1's panel as the window shows it, in place of a video chip's
6
+ # picture: the six LED digits, the keypad laid out as the board's
7
+ # (User Manual, figure 3.6), the SST switch, and the host keys that
8
+ # press each. It answers what a Screen reads of a video chip: a palette
9
+ # index a pixel, the lines changed, and the palette.
10
+ #
11
+ # Each frame #follow takes the cycles each segment was lit
12
+ # (KIM1::Panel#lit_cycles). A segment lit FULL_DUTY of the time, as
13
+ # the monitor lights each digit, shows at full brightness, and one lit
14
+ # less dimmer, through a square root, as the eye sees it. The level
15
+ # follows over SMOOTHING_CYCLES, the eye's persistence, so the frames'
16
+ # beat against the program's scan doesn't flicker.
17
+ class KIM1Panel
18
+ WIDTH = 384
19
+ HEIGHT = 272
20
+
21
+ BOARD = 0
22
+ LED = 1
23
+ LED_LEVELS = 16
24
+ WINDOW = 17
25
+ KEY = 18
26
+ KEY_HELD = 19
27
+ KEY_TEXT = 20
28
+ HINT = 21
29
+ TITLE = 22
30
+ SWITCH_ON = 23
31
+
32
+ # The board's green, the LED segments from unlit to full, the black
33
+ # behind them, the keys, and the text.
34
+ PALETTE = ([0x123c22] + Array.new(LED_LEVELS) do |level|
35
+ red = 0x2c + (((0xff - 0x2c) * level) / (LED_LEVELS - 1))
36
+ rest = 0x08 + ((0x28 * level) / (LED_LEVELS - 1))
37
+ (red << 16) | (rest << 8) | rest
38
+ end + [0x000000, 0xe8e0c8, 0x8c8470, 0x101010, 0xa8c8b0, 0xffffff, 0xffd040] +
39
+ Array.new(8, 0)).freeze
40
+
41
+ FULL_DUTY = 0.15
42
+ SMOOTHING_CYCLES = 40_000.0
43
+
44
+ # Where the digits go: the left edge of each, and their top.
45
+ DIGIT_LEFT = [56, 100, 144, 188, 252, 296].freeze
46
+ DIGIT_TOP = 30
47
+ THICKNESS = 4
48
+ LENGTH = 20
49
+
50
+ # Each segment's place in a digit, a to g: left, top, width, height.
51
+ SEGMENTS = [
52
+ [THICKNESS, 0, LENGTH, THICKNESS],
53
+ [THICKNESS + LENGTH, THICKNESS, THICKNESS, LENGTH],
54
+ [THICKNESS + LENGTH, (2 * THICKNESS) + LENGTH, THICKNESS, LENGTH],
55
+ [THICKNESS, (2 * THICKNESS) + (2 * LENGTH), LENGTH, THICKNESS],
56
+ [0, (2 * THICKNESS) + LENGTH, THICKNESS, LENGTH],
57
+ [0, THICKNESS, THICKNESS, LENGTH],
58
+ [THICKNESS, THICKNESS + LENGTH, LENGTH, THICKNESS]
59
+ ].freeze
60
+
61
+ # The keypad, top row first, as on the board, with the SST switch at
62
+ # the end of the first row.
63
+ KEYPAD = [%w[GO ST RS SST], %w[AD DA PC +], %w[C D E F], %w[8 9 A B], %w[4 5 6 7], %w[0 1 2 3]].freeze
64
+ KEYPAD_LEFT = 24
65
+ KEYPAD_TOP = 100
66
+ KEY_WIDTH = 40
67
+ KEY_HEIGHT = 22
68
+ KEY_GAP = 6
69
+
70
+ # The host keys that press the panel's.
71
+ HINTS = ["0-9, A-F: HEX", "COMMA: AD", "PERIOD: DA", "SPACE, +: +", "G, RETURN: GO", "P: PC",
72
+ "S, ESC: ST", "R: RS", "T: SST", "CLICK: KEY", "F9: MENU"].freeze
73
+ HINT_LEFT = 216
74
+ HINT_TOP = 104
75
+ HINT_LINE = 13
76
+
77
+ attr_reader :display, :dirty_lines
78
+
79
+ def initialize
80
+ @display = Array.new(WIDTH * HEIGHT, BOARD)
81
+ @dirty_lines = Array.new(HEIGHT, true)
82
+ @font = ROM.read("character.rom")
83
+ @brightness = Array.new(KIM1::Panel::DIGITS * 7, 0.0)
84
+ @levels = Array.new(KIM1::Panel::DIGITS * 7, -1)
85
+ @held = ""
86
+ @sst = false
87
+ @cycles = 0
88
+ draw_board
89
+ end
90
+
91
+ def width = WIDTH
92
+
93
+ def height = HEIGHT
94
+
95
+ def palette = PALETTE
96
+
97
+ # The whole picture: the window has nothing to crop.
98
+ def crop = [0, 0, WIDTH, HEIGHT]
99
+
100
+ def clear_dirty_lines!
101
+ @dirty_lines.map! { false }
102
+ end
103
+
104
+ # Brings the picture up to the KIM-1 `computer`: the digits from the
105
+ # segments' lit cycles since the last frame, and the key held and
106
+ # the SST switch.
107
+ def follow(computer)
108
+ cycles = computer.cycles
109
+ elapsed = cycles - @cycles
110
+ @cycles = cycles
111
+ lit = computer.panel.lit_cycles
112
+ light(lit, elapsed) if elapsed.positive?
113
+ held = computer.panel.held.to_s
114
+ draw_keys(held, computer.sst) if held != @held || computer.sst != @sst
115
+ end
116
+
117
+ # The name of the panel's key at the window pixel `left`, `top`, as
118
+ # KIM1::Panel::KEYS names it, ST, RS or SST, or an empty string.
119
+ def key_at(left, top)
120
+ across = left - KEYPAD_LEFT
121
+ down = top - KEYPAD_TOP
122
+ column = across / (KEY_WIDTH + KEY_GAP)
123
+ row = down / (KEY_HEIGHT + KEY_GAP)
124
+ return "" if across.negative? || down.negative? || column > 3 || row >= KEYPAD.size
125
+ return "" if across % (KEY_WIDTH + KEY_GAP) >= KEY_WIDTH || down % (KEY_HEIGHT + KEY_GAP) >= KEY_HEIGHT
126
+
127
+ KEYPAD[row][column]
128
+ end
129
+
130
+ # The brightness, 0 to 1, of each segment, seven to a digit.
131
+ def brightness = @brightness.dup
132
+
133
+ private
134
+
135
+ def light(lit, elapsed)
136
+ smoothing = 1.0 - Math.exp(-elapsed / SMOOTHING_CYCLES)
137
+ lit.each_with_index do |cycles, index|
138
+ duty = cycles.fdiv(elapsed) / FULL_DUTY
139
+ target = Math.sqrt([duty, 1.0].min)
140
+ level = @brightness[index] + (smoothing * (target - @brightness[index]))
141
+ @brightness[index] = level
142
+ shade = (level * (LED_LEVELS - 1)).round
143
+ next if shade == @levels[index]
144
+
145
+ @levels[index] = shade
146
+ draw_segment(index / 7, index % 7, LED + shade)
147
+ end
148
+ end
149
+
150
+ def draw_board
151
+ text(16, 8, "MOS KIM-1", TITLE)
152
+ box(40, 22, 304, 64, WINDOW)
153
+ DIGIT_LEFT.each_index { |digit| 7.times { |segment| draw_segment(digit, segment, LED) } }
154
+ @levels.map! { 0 }
155
+ HINTS.each_with_index { |hint, line| text(HINT_LEFT, HINT_TOP + (line * HINT_LINE), hint, HINT) }
156
+ draw_keys("", false)
157
+ end
158
+
159
+ def draw_segment(digit, segment, color)
160
+ place = SEGMENTS[segment]
161
+ box(DIGIT_LEFT[digit] + place[0], DIGIT_TOP + place[1], place[2], place[3], color)
162
+ end
163
+
164
+ def draw_keys(held, sst)
165
+ @held = held
166
+ @sst = sst
167
+ KEYPAD.each_with_index do |names, row|
168
+ names.each_with_index do |name, column|
169
+ on = name == "SST" ? sst : name == held
170
+ draw_key(KEYPAD_LEFT + (column * (KEY_WIDTH + KEY_GAP)), KEYPAD_TOP + (row * (KEY_HEIGHT + KEY_GAP)),
171
+ name, on)
172
+ end
173
+ end
174
+ end
175
+
176
+ def draw_key(left, top, name, on)
177
+ face = on ? KEY_HELD : KEY
178
+ face = on ? SWITCH_ON : KEY if name == "SST"
179
+ box(left, top, KEY_WIDTH, KEY_HEIGHT, face)
180
+ text(left + ((KEY_WIDTH - (name.size * 8)) / 2), top + ((KEY_HEIGHT - 8) / 2), name, KEY_TEXT)
181
+ end
182
+
183
+ def box(left, top, width, height, color)
184
+ height.times do |row|
185
+ line = top + row
186
+ @display.fill(color, (line * WIDTH) + left, width)
187
+ @dirty_lines[line] = true
188
+ end
189
+ end
190
+
191
+ # Draws `string` from `left`, `top` in the character ROM's second set.
192
+ def text(left, top, string, color)
193
+ string.bytes.each_with_index do |byte, index|
194
+ glyph(left + (index * 8), top, Painter::SCREEN_CODES[byte & 0x7f], color)
195
+ end
196
+ end
197
+
198
+ def glyph(left, top, code, color)
199
+ 8.times do |row|
200
+ bits = @font[0x800 + (code * 8) + row]
201
+ line = top + row
202
+ 8.times { |dot| @display[(line * WIDTH) + left + dot] = color if bits.anybits?(0x80 >> dot) }
203
+ @dirty_lines[line] = true
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -26,6 +26,13 @@ module Badline
26
26
  path.empty? ? path : File.expand_path(path)
27
27
  end
28
28
 
29
+ # The model of the true drive on device 8, or an empty one for the
30
+ # traps.
31
+ def drive_model(computer)
32
+ drive = Media::TrueDrive.drive(computer)
33
+ drive.nil? ? "" : drive.model_name
34
+ end
35
+
29
36
  def eject_disk(computer)
30
37
  drive = Media::TrueDrive.drive(computer)
31
38
  drive.nil? ? computer.unmount : drive.insert(nil)
@@ -35,7 +42,7 @@ module Badline
35
42
  # either cartridge's slots.
36
43
  def cartridge?(computer)
37
44
  return !computer.cartridge_slots.empty? if computer.family == :plus4
38
- return false if %i[pet cbm2].include?(computer.family)
45
+ return false if %i[pet cbm2 kim1].include?(computer.family)
39
46
 
40
47
  !computer.address_bus.cartridge.nil?
41
48
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The pause menu's list of the true drives a PET or a CBM-II takes on
6
+ # device 8, which the DRIVE 8 page's DRIVE TYPE row brings up: a row
7
+ # for each model, the disks it reads at its right, and the one on the
8
+ # bus said so. Up and Down move along the list, Return or a click picks
9
+ # a model, and Left or Esc goes back to the page.
10
+ class DriveTypes
11
+ MODELS = %w[2031 2040 3040 4040 1001 8050 8250 9060 9090].freeze
12
+ ACTIONS = %i[type2031 type2040 type3040 type4040 type1001 type8050 type8250 type9060 type9090].freeze
13
+
14
+ def initialize(painter, buttons)
15
+ @painter = painter
16
+ @buttons = buttons
17
+ @open = false
18
+ @model = ""
19
+ end
20
+
21
+ def open? = @open
22
+
23
+ # Opens the list with the focus on `model`, the drive on the bus, or
24
+ # on the first if there's none.
25
+ def open(model)
26
+ @model = model
27
+ @open = true
28
+ index = MODELS.index(model)
29
+ @buttons.focus = ACTIONS[index.nil? ? 0 : index]
30
+ end
31
+
32
+ def close
33
+ @open = false
34
+ end
35
+
36
+ # The model `action` picks, or nil.
37
+ def model_for(action)
38
+ index = ACTIONS.index(action)
39
+ index.nil? ? nil : MODELS[index]
40
+ end
41
+
42
+ # Draws the list `width` wide.
43
+ def draw(left, top, width)
44
+ @painter.text(left, top, "DRIVE 8 TYPE", MenuTheme::BRIGHT)
45
+ top += 20
46
+ MODELS.each_with_index do |model, index|
47
+ media = Drives::IEEEModels::MEDIA.fetch(model).join(" ").upcase
48
+ label = model == @model ? "#{model}, ON THE BUS" : model
49
+ @buttons.row([left, top, width], label, ACTIONS[index], media)
50
+ top += Buttons::HEIGHT + 2
51
+ end
52
+ @painter.text(left, top + 6, "RETURN: PICK LEFT, ESC: BACK", MenuTheme::DIM)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -4,12 +4,14 @@ module Badline
4
4
  module Frontend
5
5
  # The pause menu's screens that take over its panel: the file browser
6
6
  # INSERT, Quick open and LOAD bring up, the question before a cartridge
7
- # or Quick open power cycles the machine, and the prompt for a save's
8
- # name. A disk or tape picked goes straight in, and a save loads.
7
+ # or Quick open power cycles the machine, the prompt for a save's name,
8
+ # and the list of a PET's or a CBM-II's drive types. A disk or tape
9
+ # picked goes straight in, a save loads, and a drive type picked goes on
10
+ # the bus.
9
11
  class MenuDialogs
10
12
  EXTENSIONS = {
11
- disk: Media::Extensions::DEVICE8, tape: Media::Extensions.of(%i[tape]),
12
- cartridge: Media::Extensions.of(%i[cartridge]), program: Media::Extensions::ALL, snapshot: %w[.vsf]
13
+ disk: Media::Extensions::DEVICE8, cartridge: Media::Extensions.of(%i[cartridge]),
14
+ program: Media::Extensions::ALL, snapshot: %w[.vsf]
13
15
  }.freeze
14
16
  TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
15
17
  program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
@@ -25,13 +27,14 @@ module Badline
25
27
  @browser = FileBrowser.new(painter, buttons)
26
28
  @confirmation = Confirmation.new(painter, buttons)
27
29
  @name_field = NameField.new(painter)
30
+ @drive_types = DriveTypes.new(painter, buttons)
28
31
  @snapshots = snapshots
29
32
  @browsing = nil
30
33
  @browse_from = media_path.empty? ? Dir.pwd : File.dirname(File.expand_path(media_path))
31
34
  @computer = nil
32
35
  end
33
36
 
34
- def open? = !@browsing.nil? || @confirmation.open? || @name_field.open?
37
+ def open? = !@browsing.nil? || @confirmation.open? || @name_field.open? || @drive_types.open?
35
38
 
36
39
  # Opens the browser on the files for `kind`: :disk, :tape, :cartridge,
37
40
  # :program, or :snapshot, which starts in the saves folder.
@@ -40,12 +43,16 @@ module Badline
40
43
  extensions = case kind
41
44
  when :cartridge then @media.cartridge_extensions
42
45
  when :disk then @media.disk_extensions
46
+ when :tape then @media.tape_extensions
43
47
  when :program then programs
44
48
  else EXTENSIONS.fetch(kind)
45
49
  end
46
50
  @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
47
51
  end
48
52
 
53
+ # Lists the drive types, with `model` the one on the bus.
54
+ def pick_drive(model) = @drive_types.open(model)
55
+
49
56
  # Asks for a save's name, starting from `name`.
50
57
  def ask_name(name) = @name_field.open("SAVE AS", name)
51
58
 
@@ -66,7 +73,7 @@ module Badline
66
73
  def drop(path)
67
74
  extension = File.extname(path).downcase
68
75
  return @media.insert(:disk, path) && false if @media.disk_extensions.include?(extension)
69
- return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
76
+ return @media.insert(:tape, path) && false if @media.tape_extensions.include?(extension)
70
77
  return false unless programs.include?(extension)
71
78
 
72
79
  @confirmation.ask(@media.cartridge_extensions.include?(extension) ? :cartridge : :start, path)
@@ -77,6 +84,7 @@ module Badline
77
84
  # answered yes, or nil.
78
85
  def key(scancode)
79
86
  return browsed(@browser.key(scancode)) unless @browsing.nil?
87
+ return drive_key(scancode) if @drive_types.open?
80
88
  if @name_field.open? && !@confirmation.open?
81
89
  return named(@name_field.key(scancode, SDL.event_mod(SDL.event).anybits?(SDL::KMOD_SHIFT)))
82
90
  end
@@ -88,15 +96,21 @@ module Badline
88
96
  nil
89
97
  end
90
98
 
91
- # Handles a click on the browser's list at `top`, or on a question's
92
- # answer, `action`.
93
- def click(action, top) = action == :browse ? browsed(@browser.click(top)) : answer(action)
99
+ # Handles a click on the browser's list at `top`, on a question's
100
+ # answer, `action`, or on a drive type.
101
+ def click(action, top)
102
+ return browsed(@browser.click(top)) if action == :browse
103
+ return picked(action) if @drive_types.open?
104
+
105
+ answer(action)
106
+ end
94
107
 
95
108
  def scroll(rows) = @browser.scroll(rows)
96
109
 
97
110
  def draw(left, top, width)
98
111
  return @browser.draw(left, top, width, TITLES.fetch(@browsing)) unless @browsing.nil?
99
112
  return @confirmation.draw(left, top) if @confirmation.open?
113
+ return @drive_types.draw(left, top, width) if @drive_types.open?
100
114
 
101
115
  @name_field.draw(left, top, width)
102
116
  end
@@ -104,12 +118,16 @@ module Badline
104
118
  private
105
119
 
106
120
  # What Quick open and a drop start: every medium, and the cartridges
107
- # the machine takes.
108
- def programs = (EXTENSIONS[:program] + @media.cartridge_extensions).uniq
121
+ # the machine takes, or on the KIM-1 its programs.
122
+ def programs
123
+ return Media::KIM1Media::EXTENSIONS if @media.kim1?
124
+
125
+ (EXTENSIONS[:program] + @media.cartridge_extensions).uniq
126
+ end
109
127
 
110
- # Puts the disk or tape picked in, or asks before a cartridge or
111
- # Quick open power cycles the machine, and leaves the browser, as Esc
112
- # does.
128
+ # Puts the disk or tape picked in, a tape Quick open picks on the
129
+ # KIM-1 too, or asks before a cartridge or Quick open power cycles the
130
+ # machine, and leaves the browser, as Esc does.
113
131
  def browsed(picked)
114
132
  return nil if picked.nil?
115
133
 
@@ -121,12 +139,41 @@ module Badline
121
139
 
122
140
  if %i[disk tape].include?(kind)
123
141
  @media.insert(kind, picked)
142
+ elsif @media.kim1? && @media.tape_extensions.include?(File.extname(picked).downcase)
143
+ @media.insert(:tape, picked)
124
144
  else
125
145
  @confirmation.ask(kind == :cartridge ? :cartridge : :start, picked)
126
146
  end
127
147
  nil
128
148
  end
129
149
 
150
+ # Up and Down move along the drive types, Return or Space picks one,
151
+ # and Left or Esc goes back to the page.
152
+ def drive_key(scancode)
153
+ return picked(@buttons.focus) if [Keys::RETURN, Keys::SPACE].include?(scancode)
154
+
155
+ direction = Keys::DIRECTIONS[scancode]
156
+ if scancode == Keys::ESCAPE || direction == :left
157
+ @drive_types.close
158
+ @buttons.focus = :drive_type
159
+ elsif %i[up down].include?(direction)
160
+ @buttons.shift(direction == :up ? -1 : 1, 1)
161
+ end
162
+ nil
163
+ end
164
+
165
+ # Puts the drive type `action` stands for on the bus, and goes back
166
+ # to the page.
167
+ def picked(action)
168
+ model = @drive_types.model_for(action)
169
+ return nil if model.nil?
170
+
171
+ @drive_types.close
172
+ @buttons.focus = :drive_type
173
+ @media.drive_type(model)
174
+ nil
175
+ end
176
+
130
177
  # Saves the machine under the name typed once Return takes it, and
131
178
  # asks first when a save has that name already.
132
179
  def named(result)
@@ -37,15 +37,22 @@ module Badline
37
37
 
38
38
  def writable? = @writable
39
39
 
40
+ def kim1? = @computer.family == :kim1
41
+
40
42
  def disk_path = MediaSlots.disk_path(@computer)
41
43
 
42
44
  def inserted? = MediaSlots.disk?(@computer)
43
45
 
44
- # Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
45
- # :tape, and says whether it went in. A list puts its first disk in.
46
+ # Puts a disk in device 8 or a tape in the datasette or the KIM-1's
47
+ # deck, `kind` :disk or :tape, and says whether it went in. A list
48
+ # puts its first disk in.
46
49
  def insert(kind, path)
47
50
  succeeds? do
48
- if kind == :disk
51
+ if @computer.family == :kim1
52
+ raise Machine::Unsupported, "the KIM-1 takes no disks" if kind == :disk
53
+
54
+ @computer.tape.insert_tape(Storage::KIMTape.read(path))
55
+ elsif kind == :disk
49
56
  disk = Media::DiskList.disk(path)
50
57
  Media.insert_disk(@computer, disk, read_only: !@writable)
51
58
  list(path)
@@ -70,6 +77,21 @@ module Badline
70
77
 
71
78
  def eject_disk = MediaSlots.eject_disk(@computer)
72
79
 
80
+ # Puts a true drive of `model` on the PET's or the CBM-II's bus as
81
+ # device 8, in place of the drive or the traps there, with the disk
82
+ # device 8 had in it if the model reads it, and says whether it went
83
+ # on. The disk comes out first, so what was written reaches its file.
84
+ def drive_type(model)
85
+ return true if MediaSlots.drive_model(@computer) == model
86
+
87
+ succeeds? do
88
+ path = disk_path
89
+ eject_disk
90
+ @computer.plug_ieee_drive(model)
91
+ Media::TrueDrive.insert(@computer, path, read_only: !@writable) if Drives::IEEEModels.reads?(model, path)
92
+ end
93
+ end
94
+
73
95
  # Sets whether disks are writable, for the disk in the drive too,
74
96
  # which goes in again as the notch now says.
75
97
  def writable(writable)
@@ -101,19 +123,25 @@ module Badline
101
123
  insert(:disk, set[index + step])
102
124
  end
103
125
 
126
+ # The extensions of the tapes the datasette takes, or the KIM-1's
127
+ # deck: .wav recordings.
128
+ def tape_extensions = kim1? ? [Media::KIM1Media::TAPE] : Media::Extensions.of(%i[tape])
129
+
104
130
  # The extensions of the cartridges the machine takes: .crt, on the
105
- # Plus/4 raw .bin ROM images too, and none on the PET, or on the
106
- # CBM-II, whose cartridge goes in from the command line.
131
+ # Plus/4 raw .bin ROM images too, and none on the PET or the KIM-1,
132
+ # or on the CBM-II, whose cartridge goes in from the command line.
107
133
  def cartridge_extensions
108
- extensions = Media::Extensions.of(%i[pet cbm2].include?(@computer.family) ? %i[none] : %i[cartridge])
134
+ extensions = Media::Extensions.of(%i[pet cbm2 kim1].include?(@computer.family) ? %i[none] : %i[cartridge])
109
135
  @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
110
136
  end
111
137
 
112
138
  # The extensions of what goes in device 8: on the PET and the CBM-II
113
- # the 8050's and 8250's disk images besides the others, and no raw
114
- # GCR, which only a serial true drive reads.
139
+ # the 8050's and 8250's disk images and the hard disks' besides the
140
+ # others, and no raw GCR, which only a serial true drive reads.
115
141
  def disk_extensions
116
- return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if %i[pet cbm2].include?(@computer.family)
142
+ if %i[pet cbm2].include?(@computer.family)
143
+ return Media::Extensions.of(%i[disk ieee_disk hard_disk archive disk_list])
144
+ end
117
145
 
118
146
  Media::Extensions::DEVICE8
119
147
  end
@@ -132,13 +160,15 @@ module Badline
132
160
  # or a PET's after its disk or tape, and a CBM-II's after its disk.
133
161
  def game_name
134
162
  vic20 = @computer.family == :vic20
135
- name = vic20 || %i[plus4 pet cbm2].include?(@computer.family) ? "" : cartridge_name
163
+ name = vic20 || %i[plus4 pet cbm2 kim1].include?(@computer.family) ? "" : cartridge_name
136
164
  disk = disk_path
137
165
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
138
- name = tape_name if name.empty? && !vic20 && @computer.family != :cbm2
166
+ name = tape_name if name.empty? && !vic20 && !%i[cbm2 kim1].include?(@computer.family)
139
167
  name = name.sub(/\s*[(\[].*\z/, "").strip
140
168
  return name unless name.empty?
141
169
 
170
+ return "KIM-1" if @computer.family == :kim1
171
+
142
172
  vic20 ? "VIC-20" : "BASIC"
143
173
  end
144
174
 
@@ -157,7 +187,7 @@ module Badline
157
187
  case @computer.family
158
188
  when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
159
189
  when :vic20 then @computer.cartridge_name
160
- when :pet, :cbm2 then ""
190
+ when :pet, :cbm2, :kim1 then ""
161
191
  else
162
192
  cartridge = @computer.address_bus.cartridge
163
193
  cartridge.nil? ? "" : cartridge.name