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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -0
- data/CLAUDE.md +11 -7
- data/CONTRIBUTING.md +7 -1
- data/README.md +169 -20
- data/doc/pinned-behaviour.md +128 -0
- data/doc/snapshot-format.md +253 -18
- data/doc/snapshots.md +57 -23
- data/exe/badline-ruby +5 -0
- data/lib/badline/audio/terminal.rb +34 -2
- data/lib/badline/c128/keyed_state.rb +2 -0
- data/lib/badline/c128.rb +1 -0
- data/lib/badline/c64/keyed_state.rb +2 -0
- data/lib/badline/c64.rb +1 -0
- data/lib/badline/cbm2/keyed_state.rb +4 -0
- data/lib/badline/cbm2/p500.rb +5 -3
- data/lib/badline/cbm2.rb +6 -3
- data/lib/badline/chips/cia/timer.rb +2 -2
- data/lib/badline/chips/cpu/instruction_set.rb +1 -1
- data/lib/badline/chips/m6809/arithmetic.rb +140 -0
- data/lib/badline/chips/m6809/bus_steps.rb +223 -0
- data/lib/badline/chips/m6809/core.rb +149 -0
- data/lib/badline/chips/m6809/indexed.rb +82 -0
- data/lib/badline/chips/m6809/interrupts.rb +159 -0
- data/lib/badline/chips/m6809/keyed_state.rb +147 -0
- data/lib/badline/chips/m6809/opcodes.rb +159 -0
- data/lib/badline/chips/m6809/operations.rb +258 -0
- data/lib/badline/chips/m6809/plans.rb +143 -0
- data/lib/badline/chips/m6809/registers.rb +82 -0
- data/lib/badline/chips/m6809/stack.rb +140 -0
- data/lib/badline/chips/m6809.rb +13 -0
- data/lib/badline/chips/riot/timer.rb +14 -3
- data/lib/badline/chips/rriot.rb +4 -2
- data/lib/badline/chips/sid/filter.rb +4 -4
- data/lib/badline/chips/sid/waveform.rb +6 -6
- data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
- data/lib/badline/chips/vic_i.rb +84 -21
- data/lib/badline/data_path.rb +3 -3
- data/lib/badline/drives/ieee_models.rb +54 -0
- data/lib/badline/drives/model1001.rb +37 -0
- data/lib/badline/drives/model2031.rb +6 -0
- data/lib/badline/drives/model4040/disk.rb +62 -0
- data/lib/badline/drives/model4040/ip_bus.rb +46 -0
- data/lib/badline/drives/model4040/keyed_state.rb +57 -0
- data/lib/badline/drives/model4040/mechanism.rb +192 -0
- data/lib/badline/drives/model4040/parts.rb +20 -0
- data/lib/badline/drives/model4040.rb +179 -0
- data/lib/badline/drives/model8050/board.rb +77 -0
- data/lib/badline/drives/model8050/dos_board.rb +91 -0
- data/lib/badline/drives/model8050/idle.rb +11 -7
- data/lib/badline/drives/model8050/keyed_state.rb +5 -2
- data/lib/badline/drives/model8050.rb +18 -130
- data/lib/badline/drives/model9060/idle.rb +149 -0
- data/lib/badline/drives/model9060/keyed_state.rb +123 -0
- data/lib/badline/drives/model9060/parts.rb +98 -0
- data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
- data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
- data/lib/badline/drives/model9060/sasi_target.rb +161 -0
- data/lib/badline/drives/model9060.rb +195 -0
- data/lib/badline/frontend/app.rb +8 -0
- data/lib/badline/frontend/boot.rb +3 -2
- data/lib/badline/frontend/controls.rb +62 -6
- data/lib/badline/frontend/keys.rb +36 -0
- data/lib/badline/frontend/kim1_panel.rb +208 -0
- data/lib/badline/frontend/media_slots.rb +8 -1
- data/lib/badline/frontend/menu/drive_types.rb +56 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
- data/lib/badline/frontend/menu/menu_media.rb +42 -12
- data/lib/badline/frontend/menu/menu_pages.rb +97 -2
- data/lib/badline/frontend/menu/menu_ports.rb +38 -1
- data/lib/badline/frontend/menu/pause_menu.rb +5 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -1
- data/lib/badline/frontend/screen.rb +1 -1
- data/lib/badline/frontend/screens.rb +40 -2
- data/lib/badline/frontend/teletype_pane.rb +152 -0
- data/lib/badline/frontend/timeline.rb +31 -2
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/kim1/bus.rb +117 -0
- data/lib/badline/kim1/cpu.rb +25 -0
- data/lib/badline/kim1/host_terminal.rb +74 -0
- data/lib/badline/kim1/key_entry.rb +74 -0
- data/lib/badline/kim1/keyed_modules.rb +53 -0
- data/lib/badline/kim1/keyed_state.rb +133 -0
- data/lib/badline/kim1/panel.rb +176 -0
- data/lib/badline/kim1/rriots.rb +28 -0
- data/lib/badline/kim1/sound.rb +29 -0
- data/lib/badline/kim1/tape.rb +116 -0
- data/lib/badline/kim1/teletype.rb +193 -0
- data/lib/badline/kim1.rb +190 -0
- data/lib/badline/machine/ieee_drives.rb +94 -117
- data/lib/badline/machine/keyboard_buffer.rb +20 -3
- data/lib/badline/machine/line_sound.rb +122 -0
- data/lib/badline/machine.rb +46 -15
- data/lib/badline/media/extensions.rb +8 -4
- data/lib/badline/media/kim1_media.rb +150 -0
- data/lib/badline/media/pet_media.rb +3 -1
- data/lib/badline/media/true_drive.rb +42 -45
- data/lib/badline/media.rb +12 -6
- data/lib/badline/options/family.rb +98 -8
- data/lib/badline/options/help.rb +44 -12
- data/lib/badline/options/table.rb +43 -11
- data/lib/badline/options/validation.rb +44 -8
- data/lib/badline/options.rb +27 -8
- data/lib/badline/pet/bus.rb +16 -2
- data/lib/badline/pet/keyboard_ports.rb +9 -2
- data/lib/badline/pet/keyed_state.rb +4 -0
- data/lib/badline/pet/model.rb +11 -2
- data/lib/badline/pet/rom_sets.rb +1 -0
- data/lib/badline/pet/sound.rb +5 -105
- data/lib/badline/pet/superpet/acia.rb +12 -0
- data/lib/badline/pet/superpet/board.rb +221 -0
- data/lib/badline/pet/superpet/dongle.rb +81 -0
- data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
- data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
- data/lib/badline/pet/superpet/m6809.rb +32 -0
- data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
- data/lib/badline/pet/superpet.rb +216 -0
- data/lib/badline/pet.rb +19 -3
- data/lib/badline/plus4/keyed_state.rb +2 -0
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
- data/lib/badline/roms/4040/dos-1.2.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
- data/lib/badline/roms/4040/dos-2.1.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
- data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
- data/lib/badline/roms/9060/dos-3.0.rom +0 -0
- data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
- data/lib/badline/roms/README +166 -0
- data/lib/badline/roms/kim1/6530-002.rom +0 -0
- data/lib/badline/roms/kim1/6530-003.rom +0 -0
- data/lib/badline/roms/pet/characters-superpet.rom +0 -0
- data/lib/badline/roms/pet/superpet-a000.rom +0 -0
- data/lib/badline/roms/pet/superpet-c000.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
- data/lib/badline/roms/pet/superpet-e000.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +27 -6
- data/lib/badline/snapshot/image.rb +4 -1
- data/lib/badline/snapshot/kim1_setup.rb +28 -0
- data/lib/badline/snapshot/pet_setup.rb +27 -11
- data/lib/badline/snapshot/storage_setup.rb +4 -1
- data/lib/badline/snapshot/vic20_setup.rb +11 -7
- data/lib/badline/snapshot/vice/pet.rb +13 -7
- data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
- data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
- data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
- data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
- data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
- data/lib/badline/snapshot.rb +4 -0
- data/lib/badline/status.rb +30 -30
- data/lib/badline/storage/d67_image.rb +55 -0
- data/lib/badline/storage/d90_image.rb +180 -0
- data/lib/badline/storage/disk_image/writing.rb +2 -2
- data/lib/badline/storage/kim_tape.rb +254 -0
- data/lib/badline/storage/papertape.rb +90 -0
- data/lib/badline/storage.rb +4 -0
- data/lib/badline/support/kim1_checkpoint.rb +41 -0
- data/lib/badline/support/superpet_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/keyed_modules.rb +6 -3
- data/lib/badline/vic20/keyed_state.rb +6 -4
- data/lib/badline/vic20.rb +26 -12
- data/lib/badline.rb +1 -0
- 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,
|
|
8
|
-
#
|
|
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,
|
|
12
|
-
|
|
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
|
|
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`,
|
|
92
|
-
# answer, `action
|
|
93
|
-
def click(action, top)
|
|
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
|
|
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,
|
|
111
|
-
#
|
|
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
|
|
45
|
-
# :tape, and says whether it went in. A list
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|