badline 0.9.0 → 0.10.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -21,7 +21,7 @@ module Badline
21
21
  rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
22
22
  Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
23
23
  Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
24
- Datasette::Missing => e
24
+ Datasette::Missing, Unsupported => e
25
25
  warn "#{options.program}: #{options.media_path}: #{e.message}"
26
26
  return 1
27
27
  end
@@ -45,14 +45,11 @@ module Badline
45
45
 
46
46
  # A new machine built from the options, with `media`, if not nil,
47
47
  # attached and started as the command line does, its disk writable if
48
- # `writable`. The model gives the chips, and a .sid tune or --sid the
49
- # SID.
48
+ # `writable`. The family and model give the machine, and a .sid tune
49
+ # or --sid the SID. The VIC-20 raises Unsupported for a .sid
50
+ # tune.
50
51
  def self.start(options, media, writable)
51
- model = Model.named(options.model)
52
- computer = Computer.new(vic_model: model.vic_model, cia_model: model.cia_model,
53
- sid_model: options.sid_model || Media.sid_model(media, otherwise: model.sid_model),
54
- region: model.region, reu: options.reu, kernal: model.kernal,
55
- datasette: model.datasette)
52
+ computer = options.family == :vic20 ? vic20(options, media) : c64(options, media)
56
53
  Media::TrueDrive.plug(computer) if options.true_drive?
57
54
  unless media.nil?
58
55
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
@@ -60,5 +57,33 @@ module Badline
60
57
  end
61
58
  computer
62
59
  end
60
+
61
+ def self.c64(options, media)
62
+ sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
63
+ Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
64
+ end
65
+
66
+ # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
67
+ # (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
68
+ # media a VIC-20 doesn't take.
69
+ def self.vic20(options, media)
70
+ ram = options.ram
71
+ unless media.nil?
72
+ unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
73
+ raise Unsupported, "doesn't go in a VIC-20" if unsupported
74
+
75
+ ram ||= vic20_ram(media)
76
+ end
77
+ Machine.build(:vic20, model: options.model, ram:)
78
+ end
79
+
80
+ def self.vic20_ram(media)
81
+ ram = Media::Vic20Media.ram_for(media)
82
+ puts "RAM expansion: #{ram}"
83
+ ram
84
+ end
85
+
86
+ # Media the machine the options name doesn't take.
87
+ class Unsupported < ArgumentError; end
63
88
  end
64
89
  end
@@ -4,11 +4,11 @@ module Badline
4
4
  module Frontend
5
5
  # Routes host input to the machine. Two settings decide where it goes:
6
6
  #
7
- # - The keys: every host key goes to the C64 keyboard, or, as Tab
7
+ # - The keys: every host key goes to the machine's keyboard, or, as Tab
8
8
  # switches, the arrow cluster and space drive one joystick and WASD
9
9
  # and left shift the other. The arrows start on joystick 2, and
10
- # #swap_ports swaps them.
11
- # - A pot device the pause menu plugs into a control port: a 1351
10
+ # #swap_ports swaps them. The VIC-20's one joystick takes both.
11
+ # - A pot device the pause menu plugs into a C64 control port: a 1351
12
12
  # mouse or a pair of paddles, which the host mouse drives. Motion
13
13
  # turns the paddles or moves the mouse, and the host's left and right
14
14
  # buttons go to whichever lines the device puts them on.
@@ -65,7 +65,7 @@ module Badline
65
65
  # The title bar's tag for the settings, empty for the keyboard alone.
66
66
  def tag
67
67
  tags = []
68
- tags << "JOY #{@arrows_port}" if @joystick
68
+ tags << (@computer.family == :vic20 ? "JOY" : "JOY #{@arrows_port}") if @joystick
69
69
  tags << TAGS[POTS.index(@pot)] if pot_device?
70
70
  tags.join(", ")
71
71
  end
@@ -105,7 +105,7 @@ module Badline
105
105
  # RESTORE isn't in the key matrix, so it goes to the machine instead.
106
106
  def c64_key(key, down)
107
107
  if key == :restore
108
- @computer.press_restore if down
108
+ down ? @computer.press_restore : @computer.release_restore
109
109
  else
110
110
  down ? @computer.keyboard.press(key) : @computer.keyboard.release(key)
111
111
  end
@@ -113,9 +113,15 @@ module Badline
113
113
 
114
114
  def paddles? = POTS.index(@pot) >= 3
115
115
 
116
- # A fresh pot device on its port, and none on the other.
116
+ # A fresh pot device on its port, and none on the other. A machine
117
+ # without the ports takes none.
117
118
  def attach_pot_device
118
119
  ports = @computer.control_ports
120
+ if ports.nil?
121
+ @pot = :none
122
+ return
123
+ end
124
+
119
125
  ports.device1 = nil
120
126
  ports.device2 = nil
121
127
  return unless pot_device?
@@ -8,8 +8,6 @@ module Badline
8
8
  class DriveLed
9
9
  WIDTH = 12
10
10
  HEIGHT = 4
11
- LEFT = Screen::WIDTH - WIDTH - 8
12
- TOP = Screen::HEIGHT - HEIGHT - 6
13
11
 
14
12
  LIT = [0xff, 0x20, 0x20].freeze
15
13
  DARK = [0x40, 0x00, 0x00].freeze
@@ -20,11 +18,12 @@ module Badline
20
18
  drive ? new(drive) : nil
21
19
  end
22
20
 
23
- # Sets SDL.led_rect to where the LED goes.
24
- def self.place
21
+ # Sets SDL.led_rect to where the LED goes on a screen `width` by
22
+ # `height`.
23
+ def self.place(width, height)
25
24
  rect = SDL.led_rect
26
- SDL.rect_x(rect, LEFT)
27
- SDL.rect_y(rect, TOP)
25
+ SDL.rect_x(rect, width - WIDTH - 8)
26
+ SDL.rect_y(rect, height - HEIGHT - 6)
28
27
  SDL.rect_w(rect, WIDTH)
29
28
  SDL.rect_h(rect, HEIGHT)
30
29
  end
@@ -37,10 +36,10 @@ module Badline
37
36
 
38
37
  def color = lit? ? LIT : DARK
39
38
 
40
- # Fills the LED's rectangle in its colour, then puts back the black
41
- # that RenderClear clears with.
42
- def draw(renderer)
43
- DriveLed.place
39
+ # Fills the LED's rectangle on a screen `width` by `height` in its
40
+ # colour, then puts back the black that RenderClear clears with.
41
+ def draw(renderer, width, height)
42
+ DriveLed.place(width, height)
44
43
  rgb = color
45
44
  SDL.SDL_SetRenderDrawColor(renderer, rgb[0], rgb[1], rgb[2], 255)
46
45
  SDL.SDL_RenderFillRect(renderer, SDL.led_rect)
@@ -31,10 +31,10 @@ module Badline
31
31
 
32
32
  attr_reader :base_cycles, :seconds, :refresh, :trim
33
33
 
34
- # One frame of the region's raster.
35
- def self.machine(region)
36
- cycles = region.cycles_per_line * region.lines_per_frame
37
- new(cycles, cycles.to_f / region.clock_hz, 0, region.clock_hz)
34
+ # One frame of the machine's raster, from its Timing.
35
+ def self.machine(timing)
36
+ cycles = timing.cycles_per_line * timing.lines_per_frame
37
+ new(cycles, cycles.to_f / timing.clock_hz, 0, timing.clock_hz)
38
38
  end
39
39
 
40
40
  def self.display(refresh, clock_hz)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The emulator's SDL window, its renderer and the streaming texture a
6
+ # Screen's pixels go into. The renderer's logical size is the screen's,
7
+ # with each of its pixels `pixel_width` square pixels wide, so the
8
+ # picture keeps the machine's aspect while the menu and the drive LED
9
+ # draw in square pixels over it. The window opens at SCALE times that.
10
+ class MachineWindow
11
+ SCALE = 2
12
+ TITLE = "Badline"
13
+
14
+ # The logical size, in square pixels.
15
+ attr_reader :width, :height
16
+
17
+ attr_reader :renderer, :texture
18
+
19
+ # Opens the window for `screen`, presenting in step with the display
20
+ # when `vsync`.
21
+ def initialize(screen, pixel_width, vsync:)
22
+ abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
23
+
24
+ SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
25
+ SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
26
+ measure(screen, pixel_width)
27
+ @window = SDL.SDL_CreateWindow(
28
+ TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
29
+ SDL::WINDOW_RESIZABLE
30
+ )
31
+ flags = SDL::RENDERER_ACCELERATED
32
+ flags |= SDL::RENDERER_PRESENTVSYNC if vsync
33
+ @renderer = SDL.SDL_CreateRenderer(@window, -1, flags)
34
+ create_texture
35
+ end
36
+
37
+ # The refresh rate of the display the window is on, in Hz.
38
+ def refresh_rate
39
+ SDL.SDL_GetWindowDisplayMode(@window, SDL.display_mode)
40
+ SDL.mode_refresh(SDL.display_mode)
41
+ end
42
+
43
+ # Fits the texture, the logical size and the window to another
44
+ # machine's screen, unless it measures the same.
45
+ def fit(screen, pixel_width)
46
+ before = [@width, @height, @texture_width]
47
+ measure(screen, pixel_width)
48
+ return if before == [@width, @height, @texture_width]
49
+
50
+ SDL.SDL_DestroyTexture(@texture)
51
+ create_texture
52
+ SDL.SDL_SetWindowSize(@window, @width * SCALE, @height * SCALE)
53
+ end
54
+
55
+ def upload(screen)
56
+ SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
57
+ end
58
+
59
+ # Titles the window, TITLE followed by `tags` in brackets.
60
+ def title(tags)
61
+ SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |tag| "[#{tag}]" }).join(" "))
62
+ end
63
+
64
+ def close
65
+ SDL.SDL_DestroyTexture(@texture)
66
+ SDL.SDL_DestroyRenderer(@renderer)
67
+ SDL.SDL_DestroyWindow(@window)
68
+ SDL.SDL_Quit
69
+ end
70
+
71
+ private
72
+
73
+ def measure(screen, pixel_width)
74
+ @texture_width = screen.width
75
+ @width = screen.width * pixel_width
76
+ @height = screen.height
77
+ end
78
+
79
+ def create_texture
80
+ SDL.SDL_RenderSetLogicalSize(@renderer, @width, @height)
81
+ @texture = SDL.SDL_CreateTexture(
82
+ @renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, @texture_width, @height
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
@@ -4,17 +4,17 @@ module Badline
4
4
  module Frontend
5
5
  # What the pause menu puts in the machine and takes out: disks, tapes
6
6
  # and cartridges, whether disks are writable, which carries over from
7
- # one disk to the next, and the disk's set, which PREVIOUS and NEXT step
8
- # through: the .m3u or .vfl list it came from, while that lists it, or
9
- # the set DiskSet finds. It keeps what last went wrong for the page to
10
- # show.
7
+ # one disk to the next, and the set of the disk in device 8, which
8
+ # PREVIOUS and NEXT step through: the .m3u or .vfl list it came from,
9
+ # while that lists it, or the set DiskSet finds. It keeps what last went
10
+ # wrong for the page to show.
11
11
  class MenuMedia
12
- attr_reader :disk_path, :problem
12
+ attr_reader :problem
13
13
  attr_writer :computer
14
14
 
15
- # Starts on the disk the command line's `media_path` put in.
15
+ # Takes the disk set from the command line's `media_path` when it's a
16
+ # list.
16
17
  def initialize(media_path, writable)
17
- @disk_path = PauseMenu.disk_path(media_path)
18
18
  @list = Media::DiskList.list?(media_path) ? media_path : ""
19
19
  @writable = writable
20
20
  @set_path = ""
@@ -25,26 +25,40 @@ module Badline
25
25
 
26
26
  def writable? = @writable
27
27
 
28
+ # The expanded path of the disk in device 8, however it went in, or
29
+ # an empty one.
30
+ def disk_path
31
+ drive = @computer.drive1541
32
+ unless drive.nil?
33
+ disk = drive.disk
34
+ return disk.nil? ? "" : disk.path.to_s
35
+ end
36
+
37
+ path = @computer.mounted_path
38
+ path.empty? ? path : File.expand_path(path)
39
+ end
40
+
28
41
  def inserted?
29
42
  drive = @computer.drive1541
30
43
  drive.nil? ? @computer.mounted? : !drive.disk.nil?
31
44
  end
32
45
 
33
46
  # Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
34
- # :tape. A list puts its first disk in.
47
+ # :tape, and says whether it went in. A list puts its first disk in.
35
48
  def insert(kind, path)
36
49
  @problem = ""
37
50
  if kind == :disk
38
51
  disk = Media::DiskList.disk(path)
39
52
  Media.insert_disk(@computer, disk, read_only: !@writable)
40
53
  list(path)
41
- @disk_path = disk
42
54
  else
43
55
  @computer.datasette.insert(Storage::TAP.new(path))
44
56
  end
57
+ true
45
58
  rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
46
59
  Storage::G64Image::FormatError => e
47
60
  @problem = e.message
61
+ false
48
62
  end
49
63
 
50
64
  # Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
@@ -61,24 +75,25 @@ module Badline
61
75
  def eject_disk
62
76
  drive = @computer.drive1541
63
77
  drive.nil? ? @computer.unmount : drive.insert(nil)
64
- @disk_path = ""
65
78
  end
66
79
 
67
80
  # Sets whether disks are writable, for the disk in the drive too,
68
81
  # which goes in again as the notch now says.
69
82
  def writable(writable)
70
83
  @writable = writable
71
- insert(:disk, @disk_path) if inserted? && !@disk_path.empty?
84
+ path = disk_path
85
+ insert(:disk, path) if inserted? && !path.empty?
72
86
  end
73
87
 
74
88
  # The disks of the set the disk in the drive belongs to.
75
89
  def disk_set
76
- return [] if @disk_path.empty?
90
+ path = disk_path
91
+ return [] if path.empty?
77
92
 
78
- unless @set_path == @disk_path
79
- listed = @list.empty? ? [] : Media::DiskList.disks(@list)
80
- @set = listed.include?(@disk_path) ? listed : Media::DiskSet.around(@disk_path)
81
- @set_path = @disk_path
93
+ unless @set_path == path
94
+ listed = @list.empty? ? [] : expanded(Media::DiskList.disks(@list))
95
+ @set = listed.include?(path) ? listed : expanded(Media::DiskSet.around(path))
96
+ @set_path = path
82
97
  end
83
98
  @set
84
99
  end
@@ -87,7 +102,7 @@ module Badline
87
102
  # drive, if there is one.
88
103
  def step_disk(step)
89
104
  set = disk_set
90
- index = set.index(@disk_path)
105
+ index = set.index(disk_path)
91
106
  return if index.nil? || !(index + step).between?(0, set.size - 1)
92
107
 
93
108
  insert(:disk, set[index + step])
@@ -105,14 +120,23 @@ module Badline
105
120
  end
106
121
 
107
122
  # What a save of the machine is named after: the cartridge, the disk,
108
- # the tape, or BASIC, without the bracketed parts of a file's name.
123
+ # the tape, or BASIC, without the bracketed parts of a file's name. A
124
+ # VIC-20's are named after its disk, or else the machine.
109
125
  def game_name
110
- name = cartridge_name
111
- name = File.basename(@disk_path, File.extname(@disk_path)) if name.empty? && !@disk_path.empty?
112
- tape = @computer.datasette.tape
113
- name = File.basename(tape.path, File.extname(tape.path)) if name.empty? && !tape.nil?
126
+ vic20 = @computer.family == :vic20
127
+ name = vic20 ? "" : cartridge_name
128
+ disk = disk_path
129
+ name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
130
+ name = tape_name if name.empty? && !vic20
114
131
  name = name.sub(/\s*[(\[].*\z/, "").strip
115
- name.empty? ? "BASIC" : name
132
+ return name unless name.empty?
133
+
134
+ vic20 ? "VIC-20" : "BASIC"
135
+ end
136
+
137
+ def tape_name
138
+ tape = @computer.datasette.tape
139
+ tape.nil? ? "" : File.basename(tape.path, File.extname(tape.path))
116
140
  end
117
141
 
118
142
  # The name the cartridge in the expansion port gives itself, or an
@@ -133,7 +157,6 @@ module Badline
133
157
  def start(options, path)
134
158
  @problem = ""
135
159
  computer = Frontend.start(options, path, @writable)
136
- @disk_path = PauseMenu.disk_path(path)
137
160
  list(path)
138
161
  computer
139
162
  rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
@@ -152,6 +175,8 @@ module Badline
152
175
  @list = path
153
176
  @set_path = ""
154
177
  end
178
+
179
+ def expanded(paths) = paths.map { |path| File.expand_path(path) }
155
180
  end
156
181
  end
157
182
  end
@@ -7,6 +7,8 @@ module Badline
7
7
  # row for each thing to do, a setting showing its value on the right,
8
8
  # which a press steps on.
9
9
  class MenuPages
10
+ include MenuPorts
11
+
10
12
  TEXT = PauseMenu::TEXT
11
13
  DIM = PauseMenu::DIM
12
14
  LINE = 12
@@ -16,10 +18,11 @@ module Badline
16
18
  WARNING = 0xff7a6b
17
19
  RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
18
20
 
19
- PORT_DEVICES = %i[joystick mouse paddles].freeze
20
- PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
21
- PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
22
- POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
21
+ SID_MODELS = %i[mos6581 mos8580].freeze
22
+ RAM_NAMES = {
23
+ unexpanded: "", "3k": "3K", "8k": "8K", "16k": "16K", "24k": "24K", "32k": "32K",
24
+ all: "35K, ALL BLOCKS"
25
+ }.freeze
23
26
 
24
27
  # The disks, tapes and cartridges the pages put in and take out.
25
28
  attr_reader :media
@@ -75,7 +78,7 @@ module Badline
75
78
  when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
76
79
  when :swap then @controls.swap_ports
77
80
  when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
78
- when :sid6581, :sid8580 then @computer.sid.model = action == :sid8580 ? :mos8580 : :mos6581
81
+ when :sid6581, :sid8580 then @computer.sound_source.model = action == :sid8580 ? :mos8580 : :mos6581
79
82
  when :reset then @computer.reset!
80
83
  when :power_cycle then @computer.power_cycle!
81
84
  else pick_port(action)
@@ -84,6 +87,14 @@ module Badline
84
87
 
85
88
  private
86
89
 
90
+ def vic20? = @computer.family == :vic20
91
+
92
+ # Says in the dim colour that the VIC-20 hasn't got what the page
93
+ # shows.
94
+ def note
95
+ @painter.text(@left, @top, "NOT ON THE VIC-20 YET", DIM)
96
+ end
97
+
87
98
  def draw_drive
88
99
  media = @media
89
100
  inserted = media.inserted?
@@ -114,6 +125,8 @@ module Badline
114
125
  end
115
126
 
116
127
  def draw_expansion
128
+ return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
129
+
117
130
  cartridge = @computer.address_bus.cartridge
118
131
  info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
119
132
  row("INSERT...", :insert_cartridge)
@@ -125,27 +138,17 @@ module Badline
125
138
  info("RAM EXPANSION", "REU, #{@reu_kb} KB")
126
139
  end
127
140
 
128
- def draw_ports
129
- toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
130
- toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
131
- joystick = @controls.joystick_mode?
132
- toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
133
- return unless joystick
134
-
135
- row("SWAP JOYSTICKS", :swap)
136
- skip
137
- arrows = @controls.arrows_port
138
- info("ARROWS, SPACE", "PORT #{arrows}")
139
- info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
140
- end
141
-
141
+ # The SID's model is a setting, and other sound chips have none.
142
142
  def draw_sound
143
143
  if @sound.on?
144
144
  toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
145
145
  else
146
146
  info("OUTPUT", "OFF")
147
147
  end
148
- toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
148
+ model = @computer.sound_source.model
149
+ return unless SID_MODELS.include?(model)
150
+
151
+ toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
149
152
  end
150
153
 
151
154
  # Saving now and by name, loading a named save, and the quicksaves
@@ -195,26 +198,6 @@ module Badline
195
198
  def skip
196
199
  @top += LINE
197
200
  end
198
-
199
- def keys(mode) = @controls.joystick_mode = mode == :joystick
200
-
201
- def port_device(port)
202
- index = POTS[port - 1].index(@controls.pot)
203
- index.nil? ? :joystick : PORT_DEVICES[index]
204
- end
205
-
206
- # Plugs the device a choice such as :port1_mouse names into its port.
207
- def pick_port(action)
208
- index = PORT_ACTIONS[0].include?(action) ? 0 : 1
209
- choice = PORT_ACTIONS[index].index(action)
210
- plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
211
- end
212
-
213
- # Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
214
- # the pot device.
215
- def plug_device(port, choice)
216
- @controls.plug(POTS[port - 1][choice])
217
- end
218
201
  end
219
202
  end
220
203
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The pause menu's PORTS page, which MenuPages draws: what each C64
6
+ # control port has plugged in, a joystick or a pot device, and whether
7
+ # the keys type or drive the joysticks. The VIC-20 has one port, a
8
+ # joystick's.
9
+ module MenuPorts
10
+ PORT_DEVICES = %i[joystick mouse paddles].freeze
11
+ PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
12
+ PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
13
+ POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
14
+
15
+ private
16
+
17
+ def draw_ports
18
+ return draw_vic20_port if @computer.family == :vic20
19
+
20
+ toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
21
+ toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
22
+ return unless draw_keys("C64")
23
+
24
+ row("SWAP JOYSTICKS", :swap)
25
+ skip
26
+ arrows = @controls.arrows_port
27
+ info("ARROWS, SPACE", "PORT #{arrows}")
28
+ info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
29
+ end
30
+
31
+ # The VIC-20's one joystick, which both sets of keys drive.
32
+ def draw_vic20_port
33
+ return unless draw_keys("VIC-20")
34
+
35
+ skip
36
+ info("ARROWS, SPACE", "JOYSTICK")
37
+ info("WASD, LEFT SHIFT", "JOYSTICK")
38
+ end
39
+
40
+ # The toggle between the keyboard named and the joysticks, and
41
+ # whether the keys drive the joysticks.
42
+ def draw_keys(keyboard)
43
+ joystick = @controls.joystick_mode?
44
+ toggle("KEYS", :keys, [[keyboard, "JOYSTICK"], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
45
+ joystick
46
+ end
47
+
48
+ def keys(mode) = @controls.joystick_mode = mode == :joystick
49
+
50
+ def port_device(port)
51
+ index = POTS[port - 1].index(@controls.pot)
52
+ index.nil? ? :joystick : PORT_DEVICES[index]
53
+ end
54
+
55
+ # Plugs the device a choice such as :port1_mouse names into its port.
56
+ def pick_port(action)
57
+ index = PORT_ACTIONS[0].include?(action) ? 0 : 1
58
+ choice = PORT_ACTIONS[index].index(action)
59
+ plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
60
+ end
61
+
62
+ # Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
63
+ # the pot device.
64
+ def plug_device(port, choice)
65
+ @controls.plug(POTS[port - 1][choice])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -13,7 +13,8 @@ module Badline
13
13
  # SAFETY, so no samples are dropped. #measure fits the frames to the
14
14
  # rate the display presents at, measured over the whole run.
15
15
  #
16
- # Without vsync, a frame is one of the machine's frames, 20 ms on PAL.
16
+ # Without vsync, a frame is one of the machine's frames, as its Timing
17
+ # gives them, 20 ms on PAL.
17
18
  # With the sound playing, #wait lasts until the audio queue is down to
18
19
  # Sound::AHEAD; otherwise it waits out the rest of the frame.
19
20
  class Pacer
@@ -23,12 +24,12 @@ module Badline
23
24
 
24
25
  attr_reader :rate
25
26
 
26
- def initialize(paced:, vsync:, verbose:, region: Region::PAL)
27
+ def initialize(paced:, vsync:, verbose:, timing:)
27
28
  @paced = paced
28
29
  @verbose = verbose
29
30
  @vsync = paced && vsync
30
- @region = region
31
- @rate = FrameRate.machine(region)
31
+ @timing = timing
32
+ @rate = FrameRate.machine(timing)
32
33
  @deadline = 0.0
33
34
  @measured_frames = 0
34
35
  @measured_seconds = 0.0
@@ -39,7 +40,7 @@ module Badline
39
40
  # Fits the frames to the display's refresh rate, once the window has
40
41
  # been opened with vsync.
41
42
  def fit(refresh)
42
- @rate = FrameRate.display(refresh, @region.clock_hz)
43
+ @rate = FrameRate.display(refresh, @timing.clock_hz)
43
44
  puts "Display #{@rate.refresh} Hz, vsync: #{@rate.base_cycles} cycles a frame" if @verbose
44
45
  end
45
46