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
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What a KIM-1 loads (Media.attach): a MOS papertape, a .ptp or .pap
6
+ # (Storage::Papertape), at the addresses its records give, or a .bin
7
+ # of bare bytes, which says nothing of where it goes, at its Loading's
8
+ # load address.
9
+ #
10
+ # Once the monitor runs, the bytes go straight into memory, after
11
+ # the NMI and IRQ vectors at $17FA and $17FE, which nothing sets at
12
+ # power-on, are pointed at the monitor's $1C00, so ST and BRK stop
13
+ # the program and show where (User Manual, section 4.1). A papertape
14
+ # can go in through the teletype instead, as the monitor's L command
15
+ # reads it, in real time. The program then starts at its Loading's
16
+ # start address, or else where it loads, as the user would start it:
17
+ # AD, the address and GO on the keypad, or the address, a space and G
18
+ # on the teletype. Without autostart the monitor stops at the
19
+ # address.
20
+ #
21
+ # A .wav, a recording of a KIM tape (Storage::KIMTape), goes in the
22
+ # cassette deck, for the monitor's LOADT to read once PLAY is down.
23
+ module KIM1Media
24
+ PAPERTAPES = Extensions.of(%i[papertape])
25
+
26
+ # A .bin, bare bytes.
27
+ BINARY = ".bin"
28
+
29
+ # A recording of a tape.
30
+ TAPE = ".wav"
31
+
32
+ # What a KIM-1 takes.
33
+ EXTENSIONS = (PAPERTAPES + [BINARY, TAPE]).freeze
34
+
35
+ # Where a .bin loads without a load address, as most of the First
36
+ # Book of KIM's programs do.
37
+ DEFAULT_LOAD_ADDRESS = 0x0200
38
+
39
+ # Where the monitor saves the registers and shows the PC, for the
40
+ # NMI and IRQ vectors.
41
+ MONITOR = 0x1c00
42
+
43
+ NMI_VECTOR = 0x17fa
44
+ IRQ_VECTOR = 0x17fe
45
+
46
+ # How a KIM-1 loads and starts a program: at `load_address`, a .bin's,
47
+ # or -1 for DEFAULT_LOAD_ADDRESS, from `start_address`, or -1 for
48
+ # where it loads, and through the teletype's L command if
49
+ # `teletype`.
50
+ class Loading
51
+ attr_reader :load_address, :start_address
52
+
53
+ def initialize(load_address: -1, start_address: -1, teletype: false)
54
+ @load_address = load_address
55
+ @start_address = start_address
56
+ @teletype = teletype
57
+ end
58
+
59
+ def teletype? = @teletype
60
+ end
61
+
62
+ # Loads straight into memory, from where it loads.
63
+ DIRECT = Loading.new
64
+
65
+ class << self
66
+ # The Loading the command line's `options` ask for, with
67
+ # --load-address, --start-address and --tty-load.
68
+ def loading(options)
69
+ load_address = options.load_address
70
+ start_address = options.start_address
71
+ Loading.new(load_address: load_address.nil? ? -1 : load_address,
72
+ start_address: start_address.nil? ? -1 : start_address, teletype: options.tty_load?)
73
+ end
74
+
75
+ # Whether a KIM-1 takes the file at `path`.
76
+ def takes?(path) = EXTENSIONS.include?(File.extname(path).downcase)
77
+
78
+ # Loads the program at `path` into the KIM-1 `machine` as
79
+ # `loading` says, and starts it if `autostart`. Raises
80
+ # Machine::Unsupported when it loads outside the RAM.
81
+ def attach(machine, path, loading, autostart:)
82
+ raise Machine::Unsupported, "doesn't go in a KIM-1" unless takes?(path)
83
+ return insert_tape(machine, path) if File.extname(path).downcase == TAPE
84
+
85
+ papertape = PAPERTAPES.include?(File.extname(path).downcase)
86
+ tape = papertape ? Storage::Papertape.read(path) : nil
87
+ records = tape.nil? ? [binary(path, loading.load_address)] : tape.records
88
+ refuse_outside_ram(machine, records)
89
+ start = loading.start_address.negative? ? records.first.address : loading.start_address
90
+ if !tape.nil? && loading.teletype? && machine.tty?
91
+ machine.teletype.type("L#{tape.text}")
92
+ else
93
+ machine.on_init { deposit(machine, records) }
94
+ end
95
+ machine.on_init { machine.type_text(start_keys(machine, start, autostart)) }
96
+ "Loading #{path} at $#{hex(records.first.address)}"
97
+ end
98
+
99
+ private
100
+
101
+ def insert_tape(machine, path)
102
+ machine.tape.insert_tape(Storage::KIMTape.read(path))
103
+ "Inserted #{path} in the tape deck"
104
+ end
105
+
106
+ def binary(path, load_address)
107
+ Storage::Papertape::Record.new(load_address.negative? ? DEFAULT_LOAD_ADDRESS : load_address,
108
+ File.binread(path).bytes)
109
+ end
110
+
111
+ def refuse_outside_ram(machine, records)
112
+ records.each do |record|
113
+ address = record.address
114
+ last = address + record.bytes.size - 1
115
+ raise Machine::Unsupported, "has no bytes to load" if last < address
116
+ next if last <= 0xffff && (address..last).all? { |byte_address| machine.bus.ram?(byte_address) }
117
+
118
+ raise Machine::Unsupported, "loads at $#{hex(address)}-$#{hex(last)}, outside the KIM-1's " \
119
+ "#{machine.bus.ram_kb}K of RAM; --ram 5k or --ram expanded adds more"
120
+ end
121
+ end
122
+
123
+ def deposit(machine, records)
124
+ [NMI_VECTOR, IRQ_VECTOR].each do |vector|
125
+ machine.poke(vector, MONITOR & 0xff)
126
+ machine.poke(vector + 1, MONITOR >> 8)
127
+ end
128
+ records.each do |record|
129
+ address = record.address
130
+ record.bytes.each_with_index { |byte, offset| machine.poke(address + offset, byte) }
131
+ end
132
+ end
133
+
134
+ # What starts the program at `start`, or stops at it without
135
+ # `autostart`.
136
+ def start_keys(machine, start, autostart)
137
+ if machine.tty?
138
+ opened = "#{hex(start)} "
139
+ return autostart ? "#{opened}G" : opened
140
+ end
141
+
142
+ keys = "AD #{hex(start).chars.join(' ')}"
143
+ autostart ? "#{keys} GO" : keys
144
+ end
145
+
146
+ def hex(value) = value.to_s(16).upcase.rjust(4, "0")
147
+ end
148
+ end
149
+ end
150
+ end
@@ -27,8 +27,10 @@ module Badline
27
27
  # BASIC 2 and 4 load the first program off the disk and run it.
28
28
  # BASIC 1's LOAD finds no device on the IEEE-488 bus, so the first
29
29
  # program, which the block gives, goes in as a program file does,
30
- # once the KERNAL has booted.
30
+ # once the KERNAL has booted. The SuperPET's 6809 starts its own
31
+ # menu instead.
31
32
  def disk_autostart(machine)
33
+ return unless machine.boots_basic?
32
34
  return machine.type_text(AUTOSTART) unless machine.model.basic1?
33
35
 
34
36
  data = yield
@@ -4,17 +4,22 @@ module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
6
  # the C128 its 1571, or on the Plus/4 a 1551 if it asks for one, or a
7
- # 1581, or on the PET or the CBM-II a 2031 on the IEEE-488 bus, or for
8
- # a .d80 or .d82 an 8050 or 8250. Once a true drive is plugged in as device 8, disk media go into
9
- # it rather than into the traps, so a LOAD runs the DOS and every byte
10
- # crosses the serial bus or TCBM. A .g64 holds the
11
- # disk's raw GCR, which only a true drive reads, so it plugs one in
12
- # when there's none yet. The 1541 reads only .d64 and .g64 images, the
13
- # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
14
- # the drive on device 8 for a 1581 (Drives::Model1581::Slot), and another disk
15
- # image swaps a 1581 back for the machine's own drive. The PET and the
16
- # CBM-II have no 1581, and their 2031 reads .d64 and .g64 images only.
17
- # The traps' other disk media, a .t64 or a host directory, raise Error.
7
+ # 1581, or on the PET or the CBM-II a drive on the IEEE-488 bus. Once a
8
+ # true drive is plugged in as device 8, disk media go into it rather
9
+ # than into the traps, so a LOAD runs the DOS and every byte crosses the
10
+ # serial bus, TCBM or the IEEE-488 bus. A .g64 holds the disk's raw
11
+ # GCR, which only a true drive reads, so it plugs one in when there's
12
+ # none yet. The 1541 reads only .d64 and .g64 images, the 1571 .d71 and
13
+ # .g71 as well, and the 1581 only .d81, so a .d81 swaps the drive on
14
+ # device 8 for a 1581 (Drives::Model1581::Slot), and another disk image
15
+ # swaps a 1581 back for the machine's own drive. On the PET and
16
+ # the CBM-II an image goes in the drive on device 8 if it reads it, or
17
+ # else in a drive of the machine's own model, plug_true_drive's, if
18
+ # that reads it, or else in the model made for it, swapped in
19
+ # (Drives::IEEEModels): a 2031 for a .d64, an 8050 for a .d80 and an
20
+ # 8250 for a .d82, in the drive of a dual unit the machine names
21
+ # (disk_drive_number): drive 1 on a SuperPET running its 6809. The
22
+ # traps' other disk media, a .t64 or a host directory, raise Error.
18
23
  module TrueDrive
19
24
  class Error < ArgumentError; end
20
25
 
@@ -34,12 +39,12 @@ module Badline
34
39
  drive if drive && drive.device == KernalTrap::Routine::DEVICE
35
40
  end
36
41
 
37
- # Whether the path goes in a true drive: a .g64 or .g71 always, and
38
- # other disk media, which the traps would mount, when a true drive
42
+ # Whether the path goes in a true drive: a .g64, .g71 or .d90 always,
43
+ # and other disk media, which the traps would mount, when a true drive
39
44
  # is device 8.
40
45
  def takes?(computer, path)
41
46
  kind = Extensions.kind(path)
42
- return true if kind == :gcr
47
+ return true if %i[gcr hard_disk].include?(kind)
43
48
 
44
49
  !drive(computer).nil? && (File.directory?(path) || %i[disk archive ieee_disk].include?(kind))
45
50
  end
@@ -49,16 +54,13 @@ module Badline
49
54
  # mounted through the traps, so LOAD and SAVE reach the drive too.
50
55
  # `read_only` puts it in write-protected.
51
56
  def insert(computer, path, read_only: false)
52
- swapped = insert_swapping(computer, path, read_only)
53
- return swapped if swapped
57
+ return insert_ieee(computer, path, read_only) if IEEE_FAMILIES.include?(computer.family)
58
+ return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
54
59
 
55
- pet = IEEE_FAMILIES.include?(computer.family)
56
60
  drive = own_drive(computer)
57
61
  name = drive&.model_name || default_model(computer)
58
62
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
59
63
  unless types.include?(File.extname(path).downcase)
60
- raise Error, "#{path} is not a #{types.join(' or ')} image, which the #{name} reads" if pet
61
-
62
64
  raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
63
65
  end
64
66
 
@@ -71,39 +73,34 @@ module Badline
71
73
  private
72
74
 
73
75
  # The model plug_true_drive plugs in.
74
- def default_model(computer)
75
- case computer.family
76
- when :c128 then "1571"
77
- when :pet, :cbm2 then "2031"
78
- else "1541"
79
- end
80
- end
76
+ def default_model(computer) = computer.family == :c128 ? "1571" : "1541"
81
77
 
82
- # The drive on device 8 unless it's a 1581, or an 8050 or 8250,
83
- # which a .d64 swaps back out.
78
+ # The drive on device 8 unless it's a 1581, which a .d64 swaps back
79
+ # out.
84
80
  def own_drive(computer)
85
81
  drive = drive(computer)
86
- drive unless %w[1581 8050 8250].include?(drive&.model_name)
82
+ drive unless drive&.model_name == "1581"
87
83
  end
88
84
 
89
- # A .d81 in a 1581, and on the PET or the CBM-II a .d80 or .d82 in
90
- # an 8050 or 8250, each swapped in for the drive there, or nil for
91
- # the rest.
92
- def insert_swapping(computer, path, read_only)
93
- extension = File.extname(path).downcase
94
- if IEEE_FAMILIES.include?(computer.family)
95
- insert_d80(computer, path, read_only) if %w[.d80 .d82].include?(extension)
96
- elsif extension == ".d81"
97
- insert_d81(computer, path, read_only)
85
+ # The image in the model Drives::IEEEModels.taking picks, swapped in
86
+ # for the drive on device 8 unless it's that model already.
87
+ def insert_ieee(computer, path, read_only)
88
+ drive = drive(computer)
89
+ model = Drives::IEEEModels.taking(path, drive&.model_name, computer.true_drive_model)
90
+ unless model
91
+ name = computer.true_drive_model
92
+ types = Drives::IEEEModels::MEDIA.fetch(name)
93
+ raise Error, "#{path} is not a #{types.join(' or ')} image, which the #{name} reads"
94
+ end
95
+
96
+ drive = computer.plug_ieee_drive(model) unless drive&.model_name == model
97
+ number = computer.disk_drive_number
98
+ if number.positive? && drive.is_a?(Drives::Model8050)
99
+ drive.insert_image(path, drive: number, read_only:)
100
+ computer.unmount
101
+ return "Inserted #{path} in the #{model}'s drive #{number} as device 8"
98
102
  end
99
- end
100
103
 
101
- # An 8050 for a .d80 and an 8250 for a .d82, swapped in for the
102
- # drive on device 8 unless it's that model already.
103
- def insert_d80(computer, path, read_only)
104
- model = File.extname(path).casecmp?(".d82") ? "8250" : "8050"
105
- drive = drive(computer)
106
- drive = computer.plug_drive8050(model) unless drive&.model_name == model
107
104
  drive.insert_image(path, read_only:)
108
105
  computer.unmount
109
106
  "Inserted #{path} in the #{model} as device 8"
data/lib/badline/media.rb CHANGED
@@ -13,6 +13,7 @@ require "badline/media/vic20_media"
13
13
  require "badline/media/plus4_media"
14
14
  require "badline/media/pet_media"
15
15
  require "badline/media/cbm2_media"
16
+ require "badline/media/kim1_media"
16
17
  require "badline/media/tune"
17
18
  require "badline/media/program"
18
19
 
@@ -29,7 +30,8 @@ module Badline
29
30
  ".d81" => Storage::D81Image,
30
31
  ".t64" => Storage::T64,
31
32
  ".d80" => Storage::D80Image,
32
- ".d82" => Storage::D82Image
33
+ ".d82" => Storage::D82Image,
34
+ ".d67" => Storage::D67Image
33
35
  }.freeze
34
36
 
35
37
  class << self
@@ -56,8 +58,11 @@ module Badline
56
58
  # and .d82 disks too, which no other machine takes, and has no
57
59
  # cartridges, tunes or true drives (PETMedia), and so does the CBM-II,
58
60
  # which takes its own .crt cartridges and no tapes (CBM2Media). The
59
- # C128 in C64 mode takes a C64's.
60
- def attach(computer, path, autostart: true, subtune: nil, **)
61
+ # C128 in C64 mode takes a C64's. The KIM-1 takes only its programs,
62
+ # which load as `kim1`, a KIM1Media::Loading, says (KIM1Media).
63
+ def attach(computer, path, autostart: true, subtune: nil, kim1: KIM1Media::DIRECT, **)
64
+ return KIM1Media.attach(computer, path, kim1, autostart:) if computer.family == :kim1
65
+
61
66
  path = DiskList.disk(path)
62
67
  refuse_unsupported(computer, path)
63
68
 
@@ -128,11 +133,12 @@ module Badline
128
133
  private
129
134
 
130
135
  # The VIC-20 and the 264 series have no SID to play a .sid tune on,
131
- # and only the PET and the CBM-II have the IEEE-488 drive a .d80 or
132
- # .d82 goes in.
136
+ # only the PET and the CBM-II have the IEEE-488 drive a .d80, .d82
137
+ # or .d90 goes in, and only the KIM-1 reads papertape.
133
138
  def refuse_unsupported(computer, path)
134
139
  family = computer.family
135
- if Extensions.kind(path) == :ieee_disk && !%i[pet cbm2].include?(family)
140
+ raise Machine::Unsupported, "#{path} needs a KIM-1" if Extensions.kind(path) == :papertape
141
+ if %i[ieee_disk hard_disk].include?(Extensions.kind(path)) && !%i[pet cbm2].include?(family)
136
142
  raise ArgumentError, "#{path} needs a PET's or a CBM-II's IEEE-488 drive"
137
143
  end
138
144
 
@@ -12,10 +12,94 @@ module Badline
12
12
  # Where C128 BASIC starts, which a program for the C128 loads at.
13
13
  C128_BASIC_START = 0x1c01
14
14
 
15
+ # Whether the KIM-1 talks to a teletype: with --tty or --tty-load,
16
+ # and always on the terminal with --headless.
17
+ def tty? = @tty || @tty_load || kim1_terminal?
18
+
19
+ # The KIM-1 teletype's baud.
20
+ def baud = @baud
21
+
22
+ # Whether the KIM-1 loads a papertape through its monitor's L
23
+ # command, with --tty-load.
24
+ def tty_load? = @tty_load
25
+
26
+ # Where the KIM-1 loads a .bin, and where it starts its program, as
27
+ # --load-address and --start-address give them, or nil.
28
+ def load_address = @load_address
29
+
30
+ def start_address = @start_address
31
+
32
+ # Whether the KIM-1 runs on the terminal, its teletype there.
33
+ def kim1_terminal? = @family == :kim1 && @headless
34
+
15
35
  private
16
36
 
17
- # Takes `sid`, `c64`, `vic20`, `c128`, `plus4`, `pet` or `cbm2` when
18
- # given first.
37
+ # The KIM-1's keypad, until --tty or --baud name its teletype, and
38
+ # its programs where they load.
39
+ def initialize_kim1
40
+ @tty = false
41
+ @tty_load = false
42
+ @baud = 9600
43
+ @load_address = nil
44
+ @start_address = nil
45
+ end
46
+
47
+ # The KIM-1 teletype's baud, and where it loads a .bin and starts a
48
+ # program.
49
+ def kim1_option(name, flag, value)
50
+ case name
51
+ when "--baud" then @baud = baud_for(value)
52
+ when "--load-address" then @load_address = address(flag, value)
53
+ else @start_address = address(flag, value)
54
+ end
55
+ end
56
+
57
+ def kim1_switch(name)
58
+ if name == "--tty"
59
+ @tty = true
60
+ else
61
+ @tty_load = true
62
+ end
63
+ end
64
+
65
+ # An address in hex, with or without a $ or 0x before it.
66
+ def address(flag, value)
67
+ digits = value.delete_prefix("$").delete_prefix("0x").delete_prefix("0X")
68
+ raise Error, "invalid argument: #{flag} #{value}" unless digits.match?(/\A[0-9a-fA-F]{1,4}\z/)
69
+
70
+ digits.to_i(16)
71
+ end
72
+
73
+ def kim1_ram(value)
74
+ raise Error, "invalid argument: --ram #{value}" unless KIM1_RAM.key?(value)
75
+
76
+ KIM1_RAM[value]
77
+ end
78
+
79
+ # The KIM-1 runs in the window, or with --headless on the terminal,
80
+ # without the C64's SID and REU. Only a papertape goes in through
81
+ # the teletype, and only a .bin has a load address to give.
82
+ def validate_kim1
83
+ raise Error, "--ntsc needs the C64" if @models.include?("ntsc")
84
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
85
+ raise Error, "--reu needs the C64" unless @reu.nil?
86
+ raise Error, "--audio-out needs a .sid tune" unless @audio_out.nil?
87
+
88
+ extension = @media_path.nil? ? "" : File.extname(@media_path).downcase
89
+ raise Error, "--tty-load needs a .ptp or .pap" if @tty_load && !%w[.ptp .pap].include?(extension)
90
+ raise Error, "--load-address needs a .bin" unless @load_address.nil? || extension == ".bin"
91
+ end
92
+
93
+ # Only the KIM-1 has a teletype, and programs without a load address.
94
+ def refuse_kim1_options
95
+ raise Error, "#{@tty ? '--tty' : '--baud'} needs kim1" if @tty || @baud != 9600
96
+ raise Error, "--tty-load needs kim1" if @tty_load
97
+ raise Error, "--load-address needs kim1" unless @load_address.nil?
98
+ raise Error, "--start-address needs kim1" unless @start_address.nil?
99
+ end
100
+
101
+ # Takes `sid`, `c64`, `vic20`, `c128`, `plus4`, `pet`, `cbm2` or `kim1`
102
+ # when given first.
19
103
  def subcommand(args)
20
104
  case args.first
21
105
  when "sid" then @sid_command = true
@@ -25,6 +109,7 @@ module Badline
25
109
  when "plus4" then @family = :plus4
26
110
  when "pet" then @family = :pet
27
111
  when "cbm2" then @family = :cbm2
112
+ when "kim1" then @family = :kim1
28
113
  else return
29
114
  end
30
115
  @family_named = !@sid_command
@@ -38,18 +123,22 @@ module Badline
38
123
  when :plus4 then PLUS4_MODELS
39
124
  when :pet then PET_MODELS
40
125
  when :cbm2 then CBM2_MODELS
126
+ when :kim1 then KIM1_MODELS
41
127
  else MODELS
42
128
  end
43
129
  end
44
130
 
45
131
  # Without a family named, a program that loads at C128_BASIC_START is
46
- # the C128's, an 8050's or 8250's disk or a PET tape the PET's, and a
47
- # cartridge signed as a CBM-II's the CBM-II's. A program that loads
48
- # where PET BASIC starts could be a VIC-20's too.
132
+ # the C128's, an 8050's or 8250's disk or a PET tape the PET's, a
133
+ # cartridge signed as a CBM-II's the CBM-II's, and a papertape the
134
+ # KIM-1's. A program that loads where PET BASIC starts could be a
135
+ # VIC-20's too.
49
136
  def pick_family
50
137
  return if @family_named || @sid_command || @media_path.nil? || !File.file?(@media_path)
51
138
 
52
- if pet_media?
139
+ if Media::Extensions.kind(@media_path) == :papertape
140
+ @family = :kim1
141
+ elsif pet_media?
53
142
  @family = :pet
54
143
  elsif cbm2_cartridge?
55
144
  @family = :cbm2
@@ -63,10 +152,11 @@ module Badline
63
152
  Media::Extensions.kind(@media_path) == :cartridge && Storage::CRTFile.cbm2?(@media_path)
64
153
  end
65
154
 
66
- # Whether the media is an 8050's or 8250's disk or a PET tape.
155
+ # Whether the media is an 8050's or 8250's disk, a D9060's or D9090's
156
+ # hard disk, or a PET tape.
67
157
  def pet_media?
68
158
  kind = Media::Extensions.kind(@media_path)
69
- kind == :ieee_disk || (kind == :tape && Storage::TAP.platform_of(@media_path) == Storage::TAP::PET_PLATFORM)
159
+ %i[ieee_disk hard_disk].include?(kind) || (kind == :tape && Storage::TAP.platform_of(@media_path) == Storage::TAP::PET_PLATFORM)
70
160
  end
71
161
  end
72
162
  end
@@ -49,10 +49,11 @@ module Badline
49
49
  # wrapped as the banner is.
50
50
  def media_kinds
51
51
  text = "Media can be #{listed(Media::Extensions::PROGRAMS)} program, #{listed(%i[disk])} disk image, " \
52
- "#{listed(%i[ieee_disk])} disk image for a PET, #{listed(%i[gcr])} disk image for a true drive, " \
53
- "#{listed(%i[disk_list])} list of disk images, " \
52
+ "#{listed(%i[ieee_disk])} disk image or #{listed(%i[hard_disk])} hard disk for a PET, " \
53
+ "#{listed(%i[gcr])} disk image for a true drive, #{listed(%i[disk_list])} list of disk images, " \
54
54
  "#{listed(%i[archive])} tape archive, #{listed(%i[tape])} tape, #{listed(%i[cartridge])} " \
55
- "cartridge, #{listed(%i[tune])} tune, #{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
55
+ "cartridge, #{listed(%i[tune])} tune, #{listed(%i[papertape])} papertape for a KIM-1, " \
56
+ "#{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
56
57
  wrap(text)
57
58
  end
58
59
 
@@ -87,17 +88,19 @@ module Badline
87
88
  #{@program} plus4 [options] [media]
88
89
  #{@program} pet [options] [media]
89
90
  #{@program} cbm2 [options] [media]
91
+ #{@program} kim1 [options] [program]
90
92
  #{@program} --headless [options] tune.sid
91
93
  #{@program} [options] tune.sid --audio-out FILE
92
94
  #{@program} sid [options] FILE|DIR...
93
95
 
94
96
  Commands:
95
97
  c64 Run a C64, the one --model names (the default)
96
- vic20 Run a PAL VIC-20, with the RAM --ram names
98
+ vic20 Run a VIC-20, PAL or --ntsc, with the RAM --ram names
97
99
  c128 Run a C128 in C128 mode, the one --model names
98
100
  plus4 Run a Plus/4 or a C16, the one --model names
99
101
  pet Run a PET or a CBM, the one --model names
100
102
  cbm2 Run a CBM-II, a B series or a P500, the one --model names
103
+ kim1 Run a MOS KIM-1, on its keypad and LED display
101
104
  sid Play .sid tunes and directories of them
102
105
 
103
106
  #{media_kinds}
@@ -122,33 +125,60 @@ module Badline
122
125
  ESC, Page Down RUN/STOP and F4 HELP.
123
126
 
124
127
  The PET and the CBM take programs, disks, .d80 and .d82 among them,
125
- and tapes, and a .d80, a .d82 or a PET tape starts one without
126
- `pet`. Their keys type what the host's keys type, Alt with a key
128
+ and tapes, and a .d80, .d82, .d67 or .d90 or a PET tape starts one
129
+ without `pet`. Their keys type what the host's keys type, Alt with a key
127
130
  presses the PET's SHIFT and the key its unshifted character is on,
128
131
  and the pause menu's KEYBOARD page says where the rest are. --basic
129
132
  and --editor fit the upgrades Commodore sold, as in --model 2001
130
133
  --basic 2. BASIC 1 can't LOAD from a disk drive, though a disk still
131
134
  starts its first program, and BASIC 2 can. Their --true-drive makes
132
135
  device 8 an emulated 2031 on the IEEE-488 bus, which reads .d64 and
133
- .g64 images. The 8032 and the 8096 have two 50 Hz editors:
134
- 4-80-b-50hz, the default, and 4-80-b-50hz-0283, another chip's,
136
+ .g64 images, and a .d80 or a .d82 then goes in an emulated 8050 or
137
+ 8250. --drive-type 8050 or 8250 plugs either in instead, and
138
+ --drive-type 1001 an SFD-1001, which takes a .d82 in its one drive.
139
+ --drive-type 4040 or 3040 puts a .d64 in a 4040 or a 3040 instead
140
+ of the 2031, and a .d67, DOS 1's disk, goes in a 2040. A .d90 hard
141
+ disk always goes in a D9060 or a D9090, by its size. The pause
142
+ menu's DRIVE 8 page picks the drive type on the bus.
143
+ The 8032 and the 8096 have two 50 Hz editors: 4-80-b-50hz, the
144
+ default, and 4-80-b-50hz-0283, another chip's,
135
145
  with 64-cycle lines and 313 lines a frame. The 8296 runs its own,
136
146
  4-80-b-50hz-8296, and takes the 8032's too. --characters din fits
137
147
  it with the DIN character ROM, and --jumpers ram9,rama puts RAM at
138
- $9000 and $A000, as xpet's -petram9 and -petramA do.
148
+ $9000 and $A000, as xpet's -petram9 and -petramA do. --model
149
+ superpet runs the SuperPET, and --cpu 6809 its Waterloo 6809 side.
150
+ --bank-ram read write-protects its 64K bank RAM.
139
151
 
140
152
  The CBM-II takes programs, disks, .d80 and .d82 among them, and
141
153
  cartridges, and a cartridge made for it starts one without `cbm2`.
142
154
  --kernal 03b fits the 1983 KERNAL in place of 04a, and --60hz runs
143
155
  it on 60 Hz mains. Its --true-drive makes device 8 an emulated 2031
144
156
  on its IEEE-488 bus, which reads .d64 and .g64 images, and a .d80
145
- or a .d82 then goes in an emulated 8050 or 8250. Its keys type what
157
+ or a .d82 then goes in an emulated 8050 or 8250, and --drive-type
158
+ plugs in either from the start, as on the PET. Its keys type what
146
159
  the host's keys type, F12 is RUN/STOP, Alt with F9 or F10 presses
147
160
  its F9 or F10, and the pause menu's KEYBOARD page says where the
148
161
  rest are. --model 510 or 510ntsc runs the P500, on the VIC-II with
149
162
  KERNAL 02, whose keys are the B series' and whose two joysticks Tab
150
163
  switches to.
151
164
 
165
+ The KIM-1 starts in its monitor, on the keypad and the six LED
166
+ digits, which the window draws with the keys under them. 0-9 and A-F
167
+ are the hex keys, comma AD, period DA, space or + +, G or Return GO,
168
+ P PC, S or Esc ST, R RS and T the SST switch, and a click presses a
169
+ key too. The pause menu's PANEL page sets SST, the TTY/KB jumper and
170
+ what U3's PB7 and PA0 are wired to. --tty puts the monitor on a
171
+ teletype at --baud, shown in the window, or with --headless on the
172
+ terminal, where Ctrl-C quits.
173
+ It loads .ptp and .pap papertapes, which start a KIM-1 without
174
+ `kim1`, and .bin files at --load-address. The program goes into RAM,
175
+ the NMI and IRQ vectors at $17FA and $17FE point at the monitor, and
176
+ the monitor starts it where it loads, or at --start-address.
177
+ --tty-load reads a papertape through the monitor's L command
178
+ instead. --ram 5k fills $0400-$13FF, and --ram expanded $2000-$DFFF
179
+ too. A .wav recording of a KIM tape goes in the cassette deck, which
180
+ the pause menu's DATASETTE page plays for LOADT at $1873.
181
+
152
182
  --headless plays a .sid tune on the host's audio device without the
153
183
  window, and --audio-out renders it to 16-bit PCM instead. The
154
184
  container follows the file's extension, .wav or .aiff. Played on a
@@ -171,10 +201,12 @@ module Badline
171
201
  key=NAME presses a key (space, return, a, f1, run_stop, restore, and
172
202
  on the C128 esc, help, keypad1 and the rest, on the Plus/4
173
203
  esc, help and f1 to f3, on the PET rvs, escape and kp1, and on
174
- the CBM-II escape, norm_graph and kp00) or a joystick's
204
+ the CBM-II escape, norm_graph and kp00, and on the KIM-1 its keys,
205
+ as 0 to f, ad, da, plus, go, pc, st and rs) or a joystick's
175
206
  direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
176
207
  pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
177
- types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's
208
+ types it, with \\n for RETURN, or on the KIM-1 presses the keys it
209
+ names, as in type=AD 0200 DA A9. insert=FILE swaps in a disk, a list's
178
210
  first disk, a tape or a cartridge, and eject=disk, tape or
179
211
  cartridge takes one out. screenshot=FILE saves the frame as a .bmp,
180
212
  with the frame's number in place of %d. menu opens the pause menu,