badline 0.7.0 → 0.8.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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # What the pause menu puts in the machine and takes out: disks, tapes
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. It keeps what last went wrong for the page to show.
9
+ class MenuMedia
10
+ attr_reader :disk_path, :problem
11
+ attr_writer :computer
12
+
13
+ def initialize(disk_path, writable)
14
+ @disk_path = disk_path
15
+ @writable = writable
16
+ @set_path = ""
17
+ @set = []
18
+ @problem = ""
19
+ @computer = nil
20
+ end
21
+
22
+ def writable? = @writable
23
+
24
+ def inserted?
25
+ drive = @computer.drive1541
26
+ drive.nil? ? @computer.mounted? : !drive.disk.nil?
27
+ end
28
+
29
+ # Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
30
+ # :tape.
31
+ def insert(kind, path)
32
+ @problem = ""
33
+ if kind == :disk
34
+ Media.insert_disk(@computer, path, read_only: !@writable)
35
+ @disk_path = path
36
+ else
37
+ @computer.datasette.insert(Storage::TAP.new(path))
38
+ end
39
+ rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
40
+ Storage::G64Image::FormatError => e
41
+ @problem = e.message
42
+ end
43
+
44
+ # Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
45
+ # :protect or :unprotect asks.
46
+ def drive(action)
47
+ case action
48
+ when :eject_disk then eject_disk
49
+ when :previous_disk then step_disk(-1)
50
+ when :next_disk then step_disk(1)
51
+ else writable(action == :unprotect)
52
+ end
53
+ end
54
+
55
+ def eject_disk
56
+ drive = @computer.drive1541
57
+ drive.nil? ? @computer.unmount : drive.insert(nil)
58
+ @disk_path = ""
59
+ end
60
+
61
+ # Sets whether disks are writable, for the disk in the drive too,
62
+ # which goes in again as the notch now says.
63
+ def writable(writable)
64
+ @writable = writable
65
+ insert(:disk, @disk_path) if inserted? && !@disk_path.empty?
66
+ end
67
+
68
+ # The disks of the set the disk in the drive belongs to.
69
+ def disk_set
70
+ return [] if @disk_path.empty?
71
+
72
+ unless @set_path == @disk_path
73
+ @set = Media::DiskSet.around(@disk_path)
74
+ @set_path = @disk_path
75
+ end
76
+ @set
77
+ end
78
+
79
+ # Puts the disk `step` before or after this one in its set in the
80
+ # drive, if there is one.
81
+ def step_disk(step)
82
+ set = disk_set
83
+ index = set.index(@disk_path)
84
+ return if index.nil? || !(index + step).between?(0, set.size - 1)
85
+
86
+ insert(:disk, set[index + step])
87
+ end
88
+
89
+ # Swaps the cartridge for the one at `path`, which power cycles the
90
+ # machine, and says whether it went in.
91
+ def attach_cartridge(path)
92
+ @problem = ""
93
+ Media.attach(@computer, path)
94
+ true
95
+ rescue ArgumentError, SystemCallError, Storage::CRTFile::FormatError, Cartridge::UnsupportedTypeError => e
96
+ @problem = e.message
97
+ false
98
+ end
99
+
100
+ # The name the cartridge in the expansion port gives itself, or an
101
+ # empty one.
102
+ def cartridge_name
103
+ cartridge = @computer.address_bus.cartridge
104
+ cartridge.nil? ? "" : cartridge.name
105
+ end
106
+
107
+ # Takes the cartridge out with the power off.
108
+ def remove_cartridge
109
+ @computer.address_bus.detach_cartridge
110
+ @computer.power_cycle!
111
+ end
112
+
113
+ # A new machine started on `path`, as the command line would, or nil
114
+ # when it won't start.
115
+ def start(options, path)
116
+ @problem = ""
117
+ computer = Frontend.start(options, path, @writable)
118
+ @disk_path = PauseMenu.disk_path(path)
119
+ computer
120
+ rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
121
+ Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
122
+ Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError => e
123
+ @problem = e.message
124
+ nil
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The pause menu's sections, each showing a part of the machine's
6
+ # hardware as it stands, in one column: what's there, and under it a
7
+ # row for each thing to do, a setting showing its value on the right,
8
+ # which a press steps on.
9
+ class MenuPages
10
+ TEXT = PauseMenu::TEXT
11
+ DIM = PauseMenu::DIM
12
+ LINE = 12
13
+ COLUMNS = 25
14
+ ROW_WIDTH = 200
15
+
16
+ WARNING = 0xff7a6b
17
+
18
+ PORT_DEVICES = %i[joystick mouse paddles].freeze
19
+ PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
20
+ PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
21
+ POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
22
+
23
+ # The disks, tapes and cartridges the pages put in and take out.
24
+ attr_reader :media
25
+
26
+ def initialize(painter, buttons, disk_path, options)
27
+ @painter = painter
28
+ @buttons = buttons
29
+ @media = MenuMedia.new(disk_path, options.writable?)
30
+ @reu_kb = options.reu
31
+ @computer = nil
32
+ @controls = nil
33
+ @sound = nil
34
+ @left = 0
35
+ @top = 0
36
+ end
37
+
38
+ # The machine the pages show and change.
39
+ def machine(computer, controls, sound)
40
+ @computer = computer
41
+ @media.computer = computer
42
+ @controls = controls
43
+ @sound = sound
44
+ end
45
+
46
+ def draw(page, left, top)
47
+ problem = @media.problem
48
+ @painter.text(left, top + 186, problem[0, COLUMNS], WARNING) unless problem.empty?
49
+ @left = left
50
+ @top = top
51
+ case page
52
+ when :drive then draw_drive
53
+ when :datasette then draw_datasette
54
+ when :expansion then draw_expansion
55
+ when :ports then draw_ports
56
+ when :sound then draw_sound
57
+ else draw_power
58
+ end
59
+ end
60
+
61
+ def perform(action)
62
+ case action
63
+ when :eject_disk, :previous_disk, :next_disk, :protect, :unprotect then @media.drive(action)
64
+ when :tape_stop then @computer.datasette.stop!
65
+ when :tape_down then @computer.datasette.play!
66
+ when :tape_rewind then @computer.datasette.rewind
67
+ when :tape_eject then @computer.datasette.eject
68
+ when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
69
+ when :swap then @controls.swap_ports
70
+ when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
71
+ when :sid6581, :sid8580 then @computer.sid.model = action == :sid8580 ? :mos8580 : :mos6581
72
+ when :reset then @computer.reset!
73
+ when :power_cycle then @computer.power_cycle!
74
+ else pick_port(action)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def draw_drive
81
+ media = @media
82
+ inserted = media.inserted?
83
+ set = inserted ? media.disk_set : []
84
+ place = set.size < 2 ? "" : " #{set.index(media.disk_path).to_i + 1} OF #{set.size}"
85
+ info("DISK#{place}", inserted ? File.basename(media.disk_path) : "")
86
+ row("INSERT...", :insert_disk)
87
+ row("EJECT", :eject_disk)
88
+ unless set.size < 2
89
+ row("PREVIOUS DISK", :previous_disk)
90
+ row("NEXT DISK", :next_disk)
91
+ end
92
+ toggle("WRITABLE", :writable, [%w[OFF ON], %i[protect unprotect], media.writable? ? 1 : 0])
93
+ skip
94
+ info("DEVICE", @computer.drive1541.nil? ? "FAST LOADING" : "1541, TRUE DRIVE")
95
+ end
96
+
97
+ def draw_datasette
98
+ datasette = @computer.datasette
99
+ tape = datasette.tape
100
+ info("TAPE", tape.nil? ? "" : File.basename(tape.path))
101
+ row("INSERT...", :insert_tape)
102
+ row("EJECT", :tape_eject)
103
+ toggle("PLAY", :tape_play, [%w[UP DOWN], %i[tape_stop tape_down], datasette.playing? ? 1 : 0])
104
+ row("REWIND", :tape_rewind)
105
+ skip
106
+ info("MOTOR", datasette.motor? ? "ON" : "OFF")
107
+ end
108
+
109
+ def draw_expansion
110
+ cartridge = @computer.address_bus.cartridge
111
+ info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
112
+ row("INSERT...", :insert_cartridge)
113
+ row("REMOVE", :remove_cartridge)
114
+ row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
115
+ return if @reu_kb.nil?
116
+
117
+ skip
118
+ info("RAM EXPANSION", "REU, #{@reu_kb} KB")
119
+ end
120
+
121
+ def draw_ports
122
+ toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
123
+ toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
124
+ joystick = @controls.joystick_mode?
125
+ toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
126
+ return unless joystick
127
+
128
+ row("SWAP JOYSTICKS", :swap)
129
+ skip
130
+ arrows = @controls.arrows_port
131
+ info("ARROWS, SPACE", "PORT #{arrows}")
132
+ info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
133
+ end
134
+
135
+ def draw_sound
136
+ if @sound.on?
137
+ toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
138
+ else
139
+ info("OUTPUT", "OFF")
140
+ end
141
+ toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
142
+ end
143
+
144
+ def draw_power
145
+ row("RESET", :reset)
146
+ row("POWER CYCLE", :power_cycle)
147
+ skip
148
+ row("QUIT", :quit)
149
+ end
150
+
151
+ # A label, and its value under it, or NONE in the dim colour when the
152
+ # value is empty.
153
+ def info(label, value)
154
+ @painter.text(@left, @top, label, DIM)
155
+ if value.empty?
156
+ @painter.text(@left, @top + LINE, "NONE", DIM)
157
+ else
158
+ @painter.text(@left, @top + LINE, Painter.fit(value, COLUMNS), TEXT)
159
+ end
160
+ @top += (LINE * 2) + 6
161
+ end
162
+
163
+ def row(label, action, value = "")
164
+ @buttons.row([@left, @top, ROW_WIDTH], label, action, value)
165
+ @top += Buttons::HEIGHT + 2
166
+ end
167
+
168
+ def toggle(label, action, choices)
169
+ @buttons.toggle([@left, @top, ROW_WIDTH], label, action, choices)
170
+ @top += Buttons::HEIGHT + 2
171
+ end
172
+
173
+ def skip
174
+ @top += LINE
175
+ end
176
+
177
+ def keys(mode) = @controls.joystick_mode = mode == :joystick
178
+
179
+ def port_device(port)
180
+ index = POTS[port - 1].index(@controls.pot)
181
+ index.nil? ? :joystick : PORT_DEVICES[index]
182
+ end
183
+
184
+ # Plugs the device a choice such as :port1_mouse names into its port.
185
+ def pick_port(action)
186
+ index = PORT_ACTIONS[0].include?(action) ? 0 : 1
187
+ choice = PORT_ACTIONS[index].index(action)
188
+ plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
189
+ end
190
+
191
+ # Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
192
+ # the pot device.
193
+ def plug_device(port, choice)
194
+ @controls.plug(POTS[port - 1][choice])
195
+ end
196
+ end
197
+ end
198
+ end
@@ -95,6 +95,16 @@ module Badline
95
95
 
96
96
  def self.width(string, scale: 1) = string.length * GLYPH * scale
97
97
 
98
+ # The string cut to `columns` characters in its middle, so both its
99
+ # start and its end show.
100
+ def self.fit(string, columns)
101
+ return string if string.length <= columns
102
+
103
+ head = (columns - 3) / 2
104
+ tail = columns - 3 - head
105
+ "#{string[0, head]}...#{string[string.length - tail, tail]}"
106
+ end
107
+
98
108
  def icon(name, left, top, rgb, scale: 1)
99
109
  tint(rgb)
100
110
  place(top, GLYPH * scale)
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The emulator's pause menu, which F9 opens over the frozen picture.
6
+ # The machine stands still while it's open. Its sections follow the
7
+ # machine's hardware: the drive, the datasette, the expansion port, the
8
+ # control ports, the sound and the power. It's drawn in colours outside
9
+ # the C64's palette, so it reads as the emulator's and not the
10
+ # machine's, and F9 or Esc closes it again.
11
+ #
12
+ # INSERT on a page opens the file browser for that device. Quick open
13
+ # starts a file as the command line does, in a new machine, and it and
14
+ # a cartridge going in or out ask first, as each power cycles the
15
+ # machine.
16
+ class PauseMenu
17
+ SECTIONS = ["DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
18
+ PAGES = %i[drive datasette expansion ports sound power].freeze
19
+ DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
20
+
21
+ PANEL = 0x1d2230
22
+ EDGE = 0x3a4256
23
+ TEXT = 0xc9d1e0
24
+ BRIGHT = 0xffc66d
25
+ DIM = 0x6b7489
26
+ FILL = 0x343c50
27
+
28
+ LEFT = 16
29
+ TOP = 16
30
+ WIDTH = Screen::WIDTH - 32
31
+ HEIGHT = Screen::HEIGHT - 32
32
+ BODY = 154
33
+
34
+ ESCAPE = 41
35
+ RETURN = 40
36
+ SPACE = 44
37
+ ARROWS = { 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
38
+ MOUSEWHEEL = 0x403
39
+
40
+ # Takes the media, the REU and whether disks are writable from Options.
41
+ def initialize(painter, options)
42
+ media_path = options.media_path.to_s
43
+ @painter = painter
44
+ @buttons = Buttons.new(painter, [TEXT, BRIGHT, PANEL, FILL])
45
+ @pages = MenuPages.new(painter, @buttons, PauseMenu.disk_path(media_path), options)
46
+ @dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options)
47
+ @open = false
48
+ @section = 0
49
+ end
50
+
51
+ # The path of the disk the command line puts in device 8, or an empty
52
+ # one for other media.
53
+ def self.disk_path(path)
54
+ return "" if path.empty?
55
+ return path if File.directory?(path)
56
+
57
+ DISKS.include?(File.extname(path).downcase) ? path : ""
58
+ end
59
+
60
+ def open? = @open
61
+
62
+ # The machine Quick open started, which the app runs in place of the
63
+ # one before once #frame returns :swap.
64
+ def computer = @dialogs.computer
65
+
66
+ def show(computer, controls, sound)
67
+ @open = true
68
+ @buttons.focus = PAGES[@section]
69
+ @pages.machine(computer, controls, sound)
70
+ SDL.SDL_SetRelativeMouseMode(0)
71
+ draw
72
+ end
73
+
74
+ def close
75
+ @open = false
76
+ end
77
+
78
+ # Opens on the file the SDL event says was dropped on the window,
79
+ # and says whether the menu stays open to ask about it: a disk or tape
80
+ # goes straight in, and anything else asks first.
81
+ def drop(computer, controls, sound)
82
+ show(computer, controls, sound)
83
+ file = SDL.event_file(SDL.event)
84
+ asks = @dialogs.drop(LibC.strstr(file, ""))
85
+ LibC.free(file)
86
+ asks
87
+ end
88
+
89
+ # Handles a key pressed while the menu is open: the arrows move
90
+ # between the buttons, and a section opens as they reach it, Return
91
+ # or Space presses the button, and F9 or Esc closes the menu. While
92
+ # the file browser or a question is open, the keys go to it, and Esc
93
+ # goes back to the page. Returns the action the app has to take on,
94
+ # as #click does.
95
+ def key(scancode)
96
+ return :resume if scancode == Keys::F9
97
+ return @dialogs.key(scancode) if @dialogs.open?
98
+ return :resume if scancode == ESCAPE
99
+ return choose(@buttons.focus) if [RETURN, SPACE].include?(scancode)
100
+
101
+ direction = ARROWS[scancode]
102
+ move(direction) unless direction.nil?
103
+ nil
104
+ end
105
+
106
+ # Handles a click, and returns the action the app has to take on, if
107
+ # any: :resume, :quit, :freeze or :swap.
108
+ def click(left, top)
109
+ action = @buttons.action_at(left, top)
110
+ return @dialogs.click(action, top) if %i[browse confirm cancel].include?(action)
111
+
112
+ choose(action)
113
+ end
114
+
115
+ # Handles the events that came in, draws the menu over the frozen
116
+ # picture, dimmed, and returns the action the app has to take on, if
117
+ # any, leaving the events after one for the machine. Key releases
118
+ # still reach the machine, so no key stays held down while it stands
119
+ # still.
120
+ def frame(renderer, texture, controls)
121
+ action = nil
122
+ action = event(SDL.event_type(SDL.event), controls) while action.nil? && SDL.SDL_PollEvent(SDL.event) != 0
123
+ SDL.SDL_SetTextureColorMod(texture, 96, 96, 96)
124
+ SDL.SDL_RenderClear(renderer)
125
+ SDL.SDL_RenderCopy(renderer, texture, nil, nil)
126
+ SDL.SDL_SetTextureColorMod(texture, 255, 255, 255)
127
+ draw
128
+ SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
129
+ SDL.SDL_RenderPresent(renderer)
130
+ SDL.SDL_Delay(10)
131
+ action
132
+ end
133
+
134
+ def draw
135
+ painter = @painter
136
+ @buttons.forget
137
+ painter.box(LEFT, TOP, WIDTH, HEIGHT, PANEL)
138
+ painter.box(LEFT, TOP + 24, WIDTH, 1, EDGE)
139
+ painter.text(LEFT + 8, TOP + 9, "PAUSED", BRIGHT)
140
+ resume = "RESUME"
141
+ @buttons.plain(LEFT + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), TOP + 7, resume, :resume)
142
+ return @dialogs.draw(LEFT + 8, TOP + 34, WIDTH - 16) if @dialogs.open?
143
+
144
+ painter.box(BODY - 8, TOP + 32, 1, HEIGHT - 56, EDGE)
145
+ draw_sections
146
+ @buttons.plain(LEFT + 6, TOP + HEIGHT - 28, "QUICK OPEN...", :quick_open)
147
+ painter.text(LEFT + 8, TOP + HEIGHT - 12, "F9/ESC: RESUME", DIM)
148
+ @pages.draw(PAGES[@section], BODY, TOP + 34)
149
+ end
150
+
151
+ private
152
+
153
+ def choose(pressed)
154
+ return nil if pressed.nil?
155
+
156
+ action = @buttons.resolve(pressed)
157
+ index = PAGES.index(action)
158
+ unless index.nil?
159
+ @section = index
160
+ return nil
161
+ end
162
+ return action if %i[resume quit freeze].include?(action)
163
+
164
+ menu_action(action)
165
+ end
166
+
167
+ def menu_action(action)
168
+ case action
169
+ when :insert_disk then @dialogs.browse(:disk)
170
+ when :insert_tape then @dialogs.browse(:tape)
171
+ when :insert_cartridge then @dialogs.browse(:cartridge)
172
+ when :quick_open then @dialogs.browse(:program)
173
+ when :remove_cartridge then @dialogs.ask_remove
174
+ else @pages.perform(action)
175
+ end
176
+ nil
177
+ end
178
+
179
+ # Moves the focus: down from RESUME to the sections, through them,
180
+ # where each opens as it's reached, on to Quick open, right into a
181
+ # page, up and down it, and left out of it again.
182
+ def move(direction)
183
+ focus = @buttons.focus
184
+ section = focus == :quick_open ? PAGES.size : PAGES.index(focus)
185
+ if focus == :resume
186
+ @buttons.focus = PAGES[@section] if %i[down left].include?(direction)
187
+ elsif section.nil?
188
+ move_in_page(direction)
189
+ else
190
+ move_in_sections(section, direction)
191
+ end
192
+ end
193
+
194
+ # Up and down go through the page's rows, and left goes back to its
195
+ # section.
196
+ def move_in_page(direction)
197
+ case direction
198
+ when :up, :down then @buttons.shift(direction == :up ? -1 : 1, PAGES.size + 2)
199
+ when :left then @buttons.focus = PAGES[@section]
200
+ end
201
+ end
202
+
203
+ def move_in_sections(section, direction)
204
+ case direction
205
+ when :up then section.zero? ? @buttons.focus = :resume : open_section(section - 1)
206
+ when :down then section < PAGES.size - 1 ? open_section(section + 1) : @buttons.focus = :quick_open
207
+ when :right then @buttons.focus = @buttons.action(PAGES.size + 2) || @buttons.focus if section < PAGES.size
208
+ end
209
+ end
210
+
211
+ # Opens a section, laid out at once, so keys that come before the next
212
+ # frame find its rows.
213
+ def open_section(index)
214
+ @section = index
215
+ @buttons.focus = PAGES[index]
216
+ draw
217
+ end
218
+
219
+ def event(type, controls)
220
+ event = SDL.event
221
+ case type
222
+ when SDL::QUIT then :quit
223
+ when SDL::KEYDOWN then key_down(SDL.event_scancode(event), SDL.event_repeat(event))
224
+ when SDL::KEYUP then controls.key(SDL.event_scancode(event), false) && nil
225
+ when SDL::MOUSEMOTION then @buttons.point(SDL.event_x(event), SDL.event_y(event)) && nil
226
+ when SDL::MOUSEBUTTONDOWN
227
+ SDL.event_button(event) == 1 ? click(SDL.event_x(event), SDL.event_y(event)) : nil
228
+ when MOUSEWHEEL then @dialogs.scroll(-3 * SDL.event_x(event)) && nil
229
+ end
230
+ end
231
+
232
+ # A key held down repeats only the arrows.
233
+ def key_down(scancode, repeat)
234
+ repeat.zero? || ARROWS.key?(scancode) ? key(scancode) : nil
235
+ end
236
+
237
+ def draw_sections
238
+ top = TOP + 32
239
+ SECTIONS.each_with_index do |name, index|
240
+ @buttons.plain(LEFT + 6, top, name.ljust(14), PAGES[index], on: index == @section)
241
+ top += Buttons::HEIGHT + 2
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -10,7 +10,7 @@ module Badline
10
10
  KEYS = [Keys::F11, Keys::F12].freeze
11
11
 
12
12
  # The machine running, which F12 replaces with the restored one.
13
- attr_reader :computer
13
+ attr_accessor :computer
14
14
 
15
15
  def initialize(computer, options)
16
16
  @computer = computer
@@ -145,7 +145,6 @@ module Badline
145
145
 
146
146
  def underrun!
147
147
  @underruns += 1
148
- puts "Running below real time, so the sound will stutter." if @underruns == 1
149
148
  halt
150
149
  end
151
150
  end
@@ -15,7 +15,7 @@ module Badline
15
15
  unless options.screenshot.empty? || options.frames.zero?
16
16
  @events += [Options::Event.new(options.frames, "screenshot", options.screenshot)]
17
17
  end
18
- @read_only = options.read_only?
18
+ @read_only = !options.writable?
19
19
  @program = options.program
20
20
  @releases = []
21
21
  @failed = false
@@ -63,6 +63,10 @@ module Badline
63
63
  .map { |event| Timeline.path(event.argument, frame) }
64
64
  end
65
65
 
66
+ # Presses the freeze button after `frame`, and lets go of it as the
67
+ # events do.
68
+ def press_freeze(computer, frame) = freeze(computer, Options::Event.new(frame, "freeze", ""))
69
+
66
70
  # Whether the run ends at the frame: at a quit, or once an event failed.
67
71
  def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
68
72
 
@@ -11,6 +11,7 @@ require "badline/frontend/screenshot"
11
11
  require "badline/frontend/timeline"
12
12
  require "badline/frontend/sound"
13
13
  require "badline/frontend/pacer"
14
+ require "badline/frontend/frame_report"
14
15
  require "badline/frontend/controls"
15
16
  require "badline/frontend/pad_port"
16
17
  require "badline/frontend/gamepads"
@@ -30,3 +31,9 @@ require "badline/frontend/info_view"
30
31
  require "badline/frontend/player_commands"
31
32
  require "badline/frontend/player_screen"
32
33
  require "badline/frontend/player_window"
34
+ require "badline/frontend/pause_menu"
35
+ require "badline/frontend/file_browser"
36
+ require "badline/frontend/confirmation"
37
+ require "badline/frontend/menu_dialogs"
38
+ require "badline/frontend/menu_media"
39
+ require "badline/frontend/menu_pages"