badline 0.16.0 → 0.17.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -6,8 +6,9 @@ module Badline
6
6
  # control port has plugged in, a joystick or a pot device, and whether
7
7
  # the keys type or drive the joysticks. The VIC-20 has one port, a
8
8
  # joystick's, and the Plus/4 two joysticks and no pot devices. The PET
9
- # has no ports, and the page, KEYBOARD there, says where its keys are
10
- # instead.
9
+ # and the CBM-II's B series have no ports, and the page, KEYBOARD
10
+ # there, says where their keys are instead. The P500 has two joysticks,
11
+ # and the page says where its keys are while they type.
11
12
  module MenuPorts
12
13
  PORT_DEVICES = %i[joystick mouse paddles].freeze
13
14
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
@@ -21,15 +22,21 @@ module Badline
21
22
  PET_GRAPHICS_KEYS = ["ESC: RUN/STOP", "SHIFT+LETTER: GRAPHIC", "ALT+SYMBOL: GRAPHIC"].freeze
22
23
  PET_BUSINESS_KEYS = ["ESC: ESC", "TAB: TAB", "PAGE UP: REPEAT", "KEYPAD: KEYPAD", "ALT+KEY: SHIFT+KEY"].freeze
23
24
 
25
+ # Where the CBM-II's keys without a character of their own are.
26
+ CBM2_KEYS = ["ESC: ESC", "F12: RUN/STOP", "HOME: CLR/HOME", "BACKSPACE: INST/DEL", "F1-F8: F1-F8",
27
+ "ALT+F9, ALT+F10: F9, F10", "KEYPAD: KEYPAD", "ALT+0: KEYPAD 00", "ALT+C: C= ALT+E: CE",
28
+ "ALT+G: NORM/GRAPH", "ALT+R: OFF/RVS ALT+P: PI", "_: LEFT ARROW"].freeze
29
+
24
30
  private
25
31
 
26
32
  def draw_ports
27
33
  return draw_vic20_port if @computer.family == :vic20
28
34
  return draw_plus4_ports if @computer.family == :plus4
29
35
  return draw_pet_keys if @computer.family == :pet
36
+ return draw_p500_ports if Controls.p500?(@computer)
37
+ return draw_cbm2_keys if @computer.family == :cbm2
30
38
 
31
- toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
32
- toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
39
+ draw_pot_ports
33
40
  return unless draw_keys(@computer.family == :c128 ? "C128" : "C64")
34
41
 
35
42
  row("SWAP JOYSTICKS", :swap)
@@ -70,6 +77,35 @@ module Badline
70
77
  end
71
78
  end
72
79
 
80
+ # What each control port has plugged in, a joystick, a mouse or
81
+ # paddles.
82
+ def draw_pot_ports
83
+ toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
84
+ toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
85
+ end
86
+
87
+ # The P500's two ports, its joysticks one for each set of keys, and
88
+ # while the keys type, where its keys are.
89
+ def draw_p500_ports
90
+ draw_pot_ports
91
+ return draw_cbm2_keys unless draw_keys("P500")
92
+
93
+ row("SWAP JOYSTICKS", :swap)
94
+ skip
95
+ arrows = @controls.arrows_port
96
+ info("ARROWS, SPACE", "JOYSTICK #{arrows}")
97
+ info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
98
+ end
99
+
100
+ # The CBM-II's keyboard, which types as the PET's does, and the keys
101
+ # that stand for the rest of its keys.
102
+ def draw_cbm2_keys
103
+ CBM2_KEYS.each do |line|
104
+ @painter.text(@left, @top, line, MenuTheme::TEXT)
105
+ @top += MenuPages::LINE
106
+ end
107
+ end
108
+
73
109
  # The toggle between the keyboard named and the joysticks, and
74
110
  # whether the keys drive the joysticks.
75
111
  def draw_keys(keyboard)
@@ -238,7 +238,7 @@ module Badline
238
238
  def draw_sections
239
239
  top = @top + 32
240
240
  SECTIONS.each_with_index do |name, index|
241
- name = "KEYBOARD" if PAGES[index] == :ports && @pages.pet?
241
+ name = "KEYBOARD" if PAGES[index] == :ports && (@pages.pet? || @pages.b_series?)
242
242
  @buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
243
243
  top += Buttons::HEIGHT + 2
244
244
  end
@@ -12,16 +12,22 @@ module Badline
12
12
  # With either Alt down, a key that types a character presses the PET
13
13
  # key its unshifted character is on with the PET's SHIFT, so the
14
14
  # graphics on the graphics keyboard's symbol keys can be typed.
15
+ #
16
+ # The CBM-II's keyboard goes the same way (Keys.cbm2_key), and there
17
+ # Alt with a key presses one of the keys the host has none for
18
+ # (Keys.cbm2_alt_key).
15
19
  class PETKeyboard
16
20
  LSHIFT = 225
17
21
  RSHIFT = 229
18
22
  LALT = 226
19
23
  RALT = 230
20
24
 
21
- # +layout+ is the PET's keyboard, :graphics or :business.
25
+ # +layout+ is the PET's keyboard, :graphics or :business, or :cbm2
26
+ # for the CBM-II's.
22
27
  def initialize(keyboard, layout)
23
28
  @keyboard = keyboard
24
29
  @layout = layout
30
+ @shift = layout == :cbm2 ? :shift : :lshift
25
31
  @held = {}
26
32
  @left_shift = false
27
33
  @right_shift = false
@@ -43,6 +49,11 @@ module Badline
43
49
  end
44
50
  end
45
51
 
52
+ # Whether a key the scancode pressed is still down.
53
+ def holds?(scancode) = @held.key?(scancode)
54
+
55
+ def alt? = @left_alt || @right_alt
56
+
46
57
  # Lets go of every key, SHIFT among them.
47
58
  def release_all
48
59
  @held.each_value { |key| @keyboard.release(key) }
@@ -59,7 +70,7 @@ module Badline
59
70
 
60
71
  def key_down(scancode)
61
72
  key_up(scancode) if @held.key?(scancode)
62
- key, shift = alt? ? shifted_key(scancode) : Keys.pet_key(scancode, shifted?, @layout)
73
+ key, shift = alt? ? shifted_key(scancode) : layout_key(scancode, shifted?)
63
74
  return if key == :none
64
75
 
65
76
  unless shift == :host
@@ -99,10 +110,17 @@ module Badline
99
110
  end
100
111
  end
101
112
 
113
+ def layout_key(scancode, shift)
114
+ @layout == :cbm2 ? Keys.cbm2_key(scancode, shift) : Keys.pet_key(scancode, shift, @layout)
115
+ end
116
+
102
117
  # The PET key for the scancode with Alt down: for a letter or a key
103
118
  # that types a character, the key its unshifted character is on, with
104
- # SHIFT, and for any other key the one it presses without Alt.
119
+ # SHIFT, and for any other key the one it presses without Alt. The
120
+ # CBM-II's is the one Keys.cbm2_alt_key names.
105
121
  def shifted_key(scancode)
122
+ return Keys.cbm2_alt_key(scancode) if @layout == :cbm2
123
+
106
124
  key, shift = Keys.pet_key(scancode, false, @layout)
107
125
  return [key, shift] if key == :none
108
126
  return [key, shift] unless scancode.between?(4, 29) || Keys::US_CHARACTERS.key?(scancode)
@@ -112,12 +130,10 @@ module Badline
112
130
 
113
131
  def shifted? = @left_shift || @right_shift
114
132
 
115
- def alt? = @left_alt || @right_alt
116
-
117
133
  def sync_shift
118
134
  shift = @forced == -1 ? shifted? : @forced_shift
119
- @keyboard.release(:lshift)
120
- @keyboard.press(:lshift) if shift
135
+ @keyboard.release(@shift)
136
+ @keyboard.press(@shift) if shift
121
137
  end
122
138
  end
123
139
  end
@@ -25,7 +25,8 @@ module Badline
25
25
  LOCKS = %w[caps_lock forty_eighty].freeze
26
26
 
27
27
  # What's wrong with the events for the family's machine, the first
28
- # key or menu page that names nothing, or an empty string.
28
+ # key or menu page that names nothing, or an empty string. The
29
+ # CBM-II's P500 is family :p500 here, for its joysticks.
29
30
  def self.error(events, family)
30
31
  event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument, family) }
31
32
  return "no such key: #{event.argument}" unless event.nil?
@@ -38,23 +39,26 @@ module Badline
38
39
  # sound. An empty one is its first.
39
40
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
41
 
41
- # Whether the family's machine has a key of the name. The Plus/4 has
42
- # no RESTORE, and the PET neither that nor joysticks.
42
+ # Whether the family's machine has a key of the name, the CBM-II's
43
+ # P500's as family :p500. The Plus/4 has no RESTORE, the P500 none
44
+ # either, and the PET and the CBM-II's B series neither that nor
45
+ # joysticks.
43
46
  def self.key?(name, family)
44
- pet = family == :pet
45
- (name == "restore" && !pet && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
47
+ pet = %i[pet cbm2].include?(family)
48
+ (name == "restore" && !pet && !%i[plus4 p500].include?(family)) || (family == :c128 && LOCKS.include?(name)) ||
46
49
  !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
47
50
  end
48
51
 
49
52
  # The family's Keyboard key for a name such as space, return or a,
50
53
  # on the C128 esc or keypad1, on the Plus/4 help or crsr_up, or on
51
- # the PET rvs or kp1, of either of its keyboards. The shifted cursor
52
- # keys, such as cursor_up, are the C64's and the C128's.
54
+ # the PET rvs or kp1, of either of its keyboards, or on the CBM-II
55
+ # escape, norm_graph or kp00. The shifted cursor keys, such as
56
+ # cursor_up, are the C64's and the C128's.
53
57
  def self.keyboard_key(name, family)
54
58
  return nil if name == "none"
55
59
 
56
60
  key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
57
- return key unless key.nil? && !%i[plus4 pet].include?(family)
61
+ return key unless key.nil? && !%i[plus4 pet cbm2 p500].include?(family)
58
62
 
59
63
  Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
60
64
  end
@@ -64,6 +68,7 @@ module Badline
64
68
  when :c128 then C128::KEYBOARD_MATRIX
65
69
  when :plus4 then Plus4::KeyboardPort::MATRIX
66
70
  when :pet then PET::KeyboardPorts::GRAPHICS_MATRIX + PET::KeyboardPorts::BUSINESS_MATRIX
71
+ when :cbm2, :p500 then CBM2::KeyboardPorts::MATRIX
67
72
  else Input::Keyboard::C64_MATRIX
68
73
  end
69
74
  end
@@ -228,9 +233,13 @@ module Badline
228
233
  end
229
234
 
230
235
  # Whether there's one in to eject. A VIC-20 has no cartridge to take
231
- # out yet, and a PET no expansion port.
236
+ # out yet, a PET no expansion port, and a CBM-II no datasette, and
237
+ # its cartridge stays in.
232
238
  def ejectable?(computer, what)
233
- (what != "cartridge" || !%i[vic20 pet].include?(computer.family)) && inserted?(computer, what)
239
+ family = computer.family
240
+ return false if family == :cbm2 && what != "disk"
241
+
242
+ (what != "cartridge" || !%i[vic20 pet].include?(family)) && inserted?(computer, what)
234
243
  end
235
244
 
236
245
  def inserted?(computer, what)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ class DOS
6
+ # N, R and C: the commands that format a disk, rename a file and copy
7
+ # files. Each takes its names after a colon. A name can't hold a
8
+ # wildcard, and R and C take their new name before an "=". The DOS
9
+ # here has one drive, so a file on drive 1 is never found, and C
10
+ # without a colon, which copies a whole disk between the drives of a
11
+ # dual drive, and D, which duplicates one, are invalid commands, as on
12
+ # a single drive.
13
+ module DiskCommands
14
+ WILDCARD = /[*?]/
15
+ OTHER_DRIVE = /\A[1-9]\d*:/
16
+ DRIVE_ZERO = /\A0+:/
17
+
18
+ private
19
+
20
+ # N with an ID after a comma formats the disk, and without one only
21
+ # clears it. The ID is the two bytes after the comma, which takes the
22
+ # carriage return PRINT# ends the command with when the ID is one
23
+ # character. A comma with nothing after it gives no ID.
24
+ def new_disk(text)
25
+ colon = text.index(":")
26
+ return report(NO_FILE_GIVEN) unless colon
27
+
28
+ command = text.chomp("\r")
29
+ name, id = command[(colon + 1)..].split(",", 2)
30
+ return report(SYNTAX_ERROR) if name.to_s.match?(/[*?=]/)
31
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:format)
32
+
33
+ id = id.to_s.empty? ? nil : text.bytes[(command.index(",") + 1), 2]
34
+ writing { @storage.format(name.to_s, id && (id + [Storage::DiskImage::NAME_PADDING]).first(2)) }
35
+ end
36
+
37
+ # R takes the new name, then the file's.
38
+ def rename_file(text)
39
+ new_name, old_names = file_names(text, NO_FILE_GIVEN)
40
+ return unless new_name
41
+ return report(SYNTAX_ERROR) unless old_names.one?
42
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:rename)
43
+ return report(FILE_NOT_FOUND) if old_names.first.match?(OTHER_DRIVE)
44
+
45
+ writing { @storage.rename(Storage.strip_drive_prefix(old_names.first), new_name) }
46
+ end
47
+
48
+ # C takes the new name, then the files it joins, separated by
49
+ # commas. A first file named with drive 0 that is the new file
50
+ # itself has the others added to its end.
51
+ def copy_file(text)
52
+ new_name, old_names = file_names(text, INVALID_COMMAND)
53
+ return unless new_name
54
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:copy)
55
+ return report(FILE_NOT_FOUND) if old_names.any? { |name| name.match?(OTHER_DRIVE) }
56
+
57
+ names = old_names.map { |name| Storage.strip_drive_prefix(name) }
58
+ in_place = old_names.first.match?(DRIVE_ZERO) && names.first == new_name
59
+ writing { in_place ? @storage.concat(new_name, names.drop(1)) : @storage.copy(new_name, names) }
60
+ end
61
+
62
+ # The new name and the old ones of "new=old,...", or nil once it
63
+ # has reported what's wrong.
64
+ def file_names(text, missing_colon)
65
+ colon = text.index(":")
66
+ return report(missing_colon) unless colon
67
+
68
+ new_name, rest = text[(colon + 1)..].split("=", 2)
69
+ new_name = new_name.to_s
70
+ return report(SYNTAX_ERROR) if rest.nil? || new_name.include?(",")
71
+
72
+ old_names = rest.empty? ? [""] : rest.split(",", -1)
73
+ return report(SYNTAX_ERROR) if [new_name, *old_names].any? { |name| name.match?(WILDCARD) }
74
+
75
+ [new_name, old_names]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -15,8 +15,11 @@ module Badline
15
15
  28 => "WRITE ERROR",
16
16
  29 => "DISK ID MISMATCH",
17
17
  30 => "SYNTAX ERROR",
18
+ 31 => "SYNTAX ERROR",
19
+ 34 => "SYNTAX ERROR",
18
20
  62 => "FILE NOT FOUND",
19
21
  63 => "FILE EXISTS",
22
+ 64 => "FILE TYPE MISMATCH",
20
23
  65 => "NO BLOCK",
21
24
  66 => "ILLEGAL TRACK OR SECTOR",
22
25
  70 => "NO CHANNEL",
@@ -3,6 +3,7 @@
3
3
  require "badline/kernal_trap/dos/block_commands"
4
4
  require "badline/kernal_trap/dos/channel"
5
5
  require "badline/kernal_trap/dos/channels"
6
+ require "badline/kernal_trap/dos/disk_commands"
6
7
  require "badline/kernal_trap/dos/memory"
7
8
  require "badline/kernal_trap/dos/parameters"
8
9
  require "badline/kernal_trap/dos/status"
@@ -15,10 +16,12 @@ module Badline
15
16
  # command channel and its status message. `U1` reads a raw block into a
16
17
  # buffer channel, which is how block-access loaders bypass the KERNAL's
17
18
  # LOAD. A disk image whose host file takes writes takes files, blocks
18
- # and BAM changes too. Other disks are write-protected, though storage
19
- # that can write files still takes a SAVE handed over whole.
19
+ # and BAM changes too, and N, R and C format it, rename and copy files.
20
+ # Other disks are write-protected, though storage that can write files
21
+ # still takes a SAVE handed over whole.
20
22
  class DOS
21
23
  include BlockCommands
24
+ include DiskCommands
22
25
  include Writes
23
26
 
24
27
  COMMAND_CHANNEL = 15
@@ -28,6 +31,8 @@ module Badline
28
31
  FILES_SCRATCHED = 1
29
32
  WRITE_PROTECT_ON = 26
30
33
  SYNTAX_ERROR = 30
34
+ INVALID_COMMAND = 31
35
+ NO_FILE_GIVEN = 34
31
36
  FILE_NOT_FOUND = 62
32
37
  FILE_EXISTS = 63
33
38
  NO_BLOCK = 65
@@ -60,11 +65,15 @@ module Badline
60
65
  /\AV/i => :initialized,
61
66
  /\AB-A\s*:?\s*(.*)/i => :block_allocate,
62
67
  /\AB-F\s*:?\s*(.*)/i => :block_free,
63
- /\AS[^:]*:(.*)/im => :scratch
68
+ /\AS[^:]*:(.*)/im => :scratch,
69
+ /\AN(.*)/im => :new_disk,
70
+ /\AR(.*)/im => :rename_file,
71
+ /\AC(.*)/im => :copy_file,
72
+ /\AD/i => :invalid
64
73
  }.freeze
65
74
 
66
75
  # Commands that take their argument as text rather than numbers.
67
- TEXT_COMMANDS = %i[scratch].freeze
76
+ TEXT_COMMANDS = %i[scratch new_disk rename_file copy_file].freeze
68
77
 
69
78
  # A drive powers on reporting its DOS version, as a reset does. It
70
79
  # can take the RAM of a drive it replaces, and initializes its disk.
@@ -197,7 +206,7 @@ module Badline
197
206
  return report(SYNTAX_ERROR) unless action
198
207
 
199
208
  action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
200
- argument = pattern.match(command)[1]
209
+ argument = pattern.match(action == :new_disk ? text.lstrip : command)[1]
201
210
  run_command(action, TEXT_COMMANDS.include?(action) ? argument : Parameters.parse(argument))
202
211
  end
203
212
 
@@ -218,6 +227,10 @@ module Badline
218
227
  when :block_allocate then block_allocate(arguments)
219
228
  when :block_free then block_free(arguments)
220
229
  when :scratch then scratch(arguments)
230
+ when :new_disk then new_disk(arguments)
231
+ when :rename_file then rename_file(arguments)
232
+ when :copy_file then copy_file(arguments)
233
+ when :invalid then report(INVALID_COMMAND)
221
234
  when :cold_reset then reset(cold: true)
222
235
  when :warm_reset then reset(cold: false)
223
236
  when :memory_write then memory_write(arguments)
@@ -23,6 +23,9 @@ module Badline
23
23
  UNTALK_COMMAND = 0x5f
24
24
  UNLISTEN_COMMAND = 0x3f
25
25
 
26
+ # The device the traps serve, or nil to serve none.
27
+ attr_accessor :device
28
+
26
29
  def initialize(cpu:, bus:, layout:, dos:)
27
30
  @cpu = cpu
28
31
  @bus = bus
@@ -51,7 +54,9 @@ module Badline
51
54
  address_device if @listening
52
55
  end
53
56
 
54
- def addressed? = @bus.peek(zero_page.device) == @device
57
+ # The device TALK or LISTEN addresses: the one in A or in FA, as the
58
+ # layout says.
59
+ def addressed? = (@layout.device_in_a ? @cpu.a : @bus.peek(zero_page.device)) == @device
55
60
 
56
61
  # TALK and LISTEN end with the command sent under ATN, which stays
57
62
  # low, and A holding $FF.
@@ -113,7 +118,7 @@ module Badline
113
118
  def acptr
114
119
  return unless @talking
115
120
 
116
- @layout.listen_to_talker(@bus)
121
+ @layout.byte_received(@bus)
117
122
  byte, status = received
118
123
  time_serial_byte(Routine::ACPTR_TIMEOUT)
119
124
  @cpu.a = byte
@@ -2,70 +2,122 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # Where a PET KERNAL keeps the eight IEEE-488 bus primitives, which are
6
- # internal routines, not jump table entries, and the ZeroPage of its
7
- # file variables. Each ROM set has its own.
8
- #
9
- # TALK and LISTEN take no device number in A: they send $40 or $20
10
- # ORed with FA. UNTALK and UNLISTEN send $5F and $3F through the same
11
- # code, and SECOND, TKSA, UNTALK and UNLISTEN end by letting go of ATN
12
- # (VIA PB2) themselves. Each handshake times out on VIA timer 1, which
13
- # the ROM starts with a write of $FF to $E845. LOAD and SAVE run in the
14
- # ROM, through the primitives. +overwrites_device+ says UNTALK and
15
- # UNLISTEN store their command in FA before they send it.
5
+ # The +keep+ of an IEEELineStep that only reads its register.
6
+ IEEE_LINE_READ = -1
7
+
8
+ # One step a KERNAL takes on the IEEE-488 bus's lines: a write of +set+
9
+ # to the register at +address+, or with a +keep+ mask the register read
10
+ # first, its +keep+ bits kept and +set+ ORed in, or with keep
11
+ # IEEE_LINE_READ only a read. Returns the value written, or read.
12
+ IEEELineStep = Data.define(:address, :keep, :set) do
13
+ def act_on(bus)
14
+ return bus.peek(address) if keep == IEEE_LINE_READ
15
+
16
+ value = keep.zero? ? set : (bus.peek(address) & keep) | set
17
+ bus.poke(address, value)
18
+ value
19
+ end
20
+ end
21
+
22
+ # The steps a KERNAL takes on the bus's lines at each moment the traps
23
+ # stand in for, each run in order (#run_steps), the last one's value
24
+ # returned: +timer+ starts the timer that times out each byte's
25
+ # handshake, +attention+ readies the bus and pulls ATN low for TALK
26
+ # and LISTEN, +byte_sent+ ends a byte sent, +release_attention+ ends
27
+ # SECOND, TKSA, UNTALK and UNLISTEN by letting ATN go, +listen_to_talker+
28
+ # ends TKSA with the computer listening, +byte_received+ ends ACPTR,
29
+ # and +release+ ends UNTALK and UNLISTEN.
30
+ IEEELines = Data.define(:timer, :attention, :byte_sent, :release_attention, :listen_to_talker, :byte_received,
31
+ :release) do
32
+ def run_steps(steps, bus)
33
+ value = 0
34
+ steps.each { |step| value = step.act_on(bus) }
35
+ value
36
+ end
37
+ end
38
+
39
+ # Where a KERNAL keeps the eight IEEE-488 bus primitives, which are
40
+ # internal routines, not jump table entries, the ZeroPage of its file
41
+ # variables, and the steps it takes on the bus's lines (IEEELines).
42
+ # Each ROM set has its own.
16
43
  #
17
- # The ROM drives the bus's control lines from three chips: ATN from VIA
18
- # PB2 and NRFD from PB1 at $E840, NDAC from PIA 2's CA2 and DAV from its
19
- # CB2, through the control registers at $E821 and $E823, which the ROM
20
- # writes $34 to pull a line low and $3C to let it go, and the data lines
21
- # from PIA 2's port B at $E822. The steps below leave the lines as each
22
- # primitive leaves them.
44
+ # On a PET, TALK and LISTEN take no device number in A: they send $40
45
+ # or $20 ORed with FA. UNTALK and UNLISTEN send $5F and $3F through the
46
+ # same code, and SECOND, TKSA, UNTALK and UNLISTEN end by letting go of
47
+ # ATN themselves. LOAD and SAVE run in the ROM, through the primitives.
48
+ # +overwrites_device+ says UNTALK and UNLISTEN store their command in
49
+ # FA before they send it. +device_in_a+ says TALK and LISTEN take the
50
+ # device in A, as the CBM-II's do, where a PET's ignore A and read FA.
23
51
  IEEELayout = Data.define(:talk, :listen, :second, :tksa, :ciout, :untalk, :unlisten, :acptr, :overwrites_device,
24
- :zero_page) do
25
- # The KERNAL is mapped unless an 8096's board lies over it.
52
+ :device_in_a, :zero_page, :lines) do
53
+ # The KERNAL is mapped unless something lies over it.
26
54
  def kernal_mapped?(bus) = bus.kernal_mapped?
27
55
 
28
- # Starts VIA timer 1, which times out the handshake of each byte.
56
+ # Starts the timer that times out the handshake of each byte.
29
57
  def time_serial_byte(bus, _timer_high)
30
- bus.poke(0xe845, 0xff)
58
+ lines.run_steps(lines.timer, bus)
31
59
  end
32
60
 
33
- # TALK and LISTEN: NRFD and NDAC let go, then ATN pulled low.
61
+ # TALK and LISTEN: the handshake lines let go, then ATN pulled low.
34
62
  def attention(bus)
35
- bus.poke(0xe840, bus.peek(0xe840) | 0x02)
36
- bus.poke(0xe821, 0x3c)
37
- bus.poke(0xe840, bus.peek(0xe840) & 0xfb)
63
+ lines.run_steps(lines.attention, bus)
38
64
  end
39
65
 
40
66
  # The end of a byte sent: DAV and the data lines let go.
41
67
  def byte_sent(bus)
42
- bus.poke(0xe823, 0x3c)
43
- bus.poke(0xe822, 0xff)
68
+ lines.run_steps(lines.byte_sent, bus)
44
69
  end
45
70
 
46
71
  # The end of SECOND, TKSA, UNTALK and UNLISTEN: ATN let go. Returns
47
72
  # the value written, which the ROM leaves in A.
48
73
  def release_attention(bus)
49
- value = bus.peek(0xe840) | 0x04
50
- bus.poke(0xe840, value)
51
- value
74
+ lines.run_steps(lines.release_attention, bus)
52
75
  end
53
76
 
54
- # TKSA and ACPTR end listening: NRFD and NDAC pulled low.
77
+ # TKSA ends listening: NRFD and NDAC pulled low.
55
78
  def listen_to_talker(bus)
56
- bus.poke(0xe840, bus.peek(0xe840) & 0xfd)
57
- bus.poke(0xe821, 0x34)
79
+ lines.run_steps(lines.listen_to_talker, bus)
80
+ end
81
+
82
+ # ACPTR ends listening as well.
83
+ def byte_received(bus)
84
+ lines.run_steps(lines.byte_received, bus)
58
85
  end
59
86
 
60
87
  # The end of UNTALK and UNLISTEN, which address the bus as TALK and
61
- # LISTEN do, send their command and let ATN go.
88
+ # LISTEN do, send their command and let ATN go. Returns the value
89
+ # the ROM leaves in A.
62
90
  def release_serial_lines(bus)
63
- attention(bus)
64
- byte_sent(bus)
65
- release_attention(bus)
91
+ lines.run_steps(lines.release, bus)
66
92
  end
67
93
  end
68
94
 
95
+ # A PET drives the bus's control lines from three chips: ATN from VIA
96
+ # PB2 and NRFD from PB1 at $E840, NDAC from PIA 2's CA2 and DAV from its
97
+ # CB2, through the control registers at $E821 and $E823, which the ROM
98
+ # writes $34 to pull a line low and $3C to let it go, and the data lines
99
+ # from PIA 2's port B at $E822. Each handshake times out on VIA timer 1,
100
+ # which the ROM starts with a write of $FF to $E845. The steps leave the
101
+ # lines as each primitive leaves them.
102
+ PET_ATTENTION = [
103
+ IEEELineStep.new(address: 0xe840, keep: 0xff, set: 0x02),
104
+ IEEELineStep.new(address: 0xe821, keep: 0, set: 0x3c),
105
+ IEEELineStep.new(address: 0xe840, keep: 0xfb, set: 0)
106
+ ].freeze
107
+ PET_BYTE_SENT = [
108
+ IEEELineStep.new(address: 0xe823, keep: 0, set: 0x3c),
109
+ IEEELineStep.new(address: 0xe822, keep: 0, set: 0xff)
110
+ ].freeze
111
+ PET_RELEASE_ATTENTION = [IEEELineStep.new(address: 0xe840, keep: 0xff, set: 0x04)].freeze
112
+ PET_LISTEN = [IEEELineStep.new(address: 0xe840, keep: 0xfd, set: 0),
113
+ IEEELineStep.new(address: 0xe821, keep: 0, set: 0x34)].freeze
114
+ PET_LINES = IEEELines.new(
115
+ timer: [IEEELineStep.new(address: 0xe845, keep: 0, set: 0xff)].freeze,
116
+ attention: PET_ATTENTION, byte_sent: PET_BYTE_SENT, release_attention: PET_RELEASE_ATTENTION,
117
+ listen_to_talker: PET_LISTEN, byte_received: PET_LISTEN,
118
+ release: (PET_ATTENTION + PET_BYTE_SENT + PET_RELEASE_ATTENTION).freeze
119
+ )
120
+
69
121
  # The file variables of the BASIC 2 and BASIC 4 KERNALs, which keep
70
122
  # them at the same addresses. SAVE copies STAL at $FB to SAL at $C7 and
71
123
  # walks SAL up to EAL at $C9. LOAD keeps no MEMUSS: it stores each byte
@@ -90,24 +142,24 @@ module Badline
90
142
  # LISTEN without a secondary address.
91
143
  PET_BASIC1_LAYOUT = IEEELayout.new(
92
144
  talk: 0xf0b6, listen: 0xf0ba, second: 0xf12c, tksa: 0xf15b,
93
- ciout: 0xf167, untalk: 0xf17a, unlisten: 0xf17e, acptr: 0xf187, overwrites_device: true,
94
- zero_page: PET_BASIC1_ZERO_PAGE
145
+ ciout: 0xf167, untalk: 0xf17a, unlisten: 0xf17e, acptr: 0xf187, overwrites_device: true, device_in_a: false,
146
+ zero_page: PET_BASIC1_ZERO_PAGE, lines: PET_LINES
95
147
  )
96
148
 
97
149
  # BASIC 2, 901465-03. Its LOAD sends the secondary address after TALK
98
150
  # with SECOND, not TKSA.
99
151
  PET_BASIC2_LAYOUT = IEEELayout.new(
100
152
  talk: 0xf0b6, listen: 0xf0ba, second: 0xf128, tksa: 0xf164,
101
- ciout: 0xf16f, untalk: 0xf17f, unlisten: 0xf183, acptr: 0xf18c, overwrites_device: false,
102
- zero_page: PET_ZERO_PAGE
153
+ ciout: 0xf16f, untalk: 0xf17f, unlisten: 0xf183, acptr: 0xf18c, overwrites_device: false, device_in_a: false,
154
+ zero_page: PET_ZERO_PAGE, lines: PET_LINES
103
155
  )
104
156
 
105
157
  # BASIC 4, 901465-22. LISTEN at $F0D5 is the operand of the BIT
106
158
  # instruction that skips it on the way from TALK.
107
159
  PET_BASIC4_LAYOUT = IEEELayout.new(
108
160
  talk: 0xf0d2, listen: 0xf0d5, second: 0xf143, tksa: 0xf193,
109
- ciout: 0xf19e, untalk: 0xf1ae, unlisten: 0xf1b9, acptr: 0xf1c0, overwrites_device: false,
110
- zero_page: PET_ZERO_PAGE
161
+ ciout: 0xf19e, untalk: 0xf1ae, unlisten: 0xf1b9, acptr: 0xf1c0, overwrites_device: false, device_in_a: false,
162
+ zero_page: PET_ZERO_PAGE, lines: PET_LINES
111
163
  )
112
164
  end
113
165
  end