badline 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +3 -1
- data/README.md +104 -292
- data/doc/library.md +69 -0
- data/doc/media.md +78 -0
- data/doc/pinned-behaviour.md +53 -9
- data/doc/sid-player.md +136 -0
- data/doc/snapshots.md +102 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +4 -4
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/computer/attachments.rb +5 -5
- data/lib/badline/computer.rb +12 -10
- data/lib/badline/core.rb +48 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +24 -9
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/frontend/app.rb +41 -37
- data/lib/badline/frontend/boot.rb +16 -4
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +56 -0
- data/lib/badline/frontend/controls.rb +44 -32
- data/lib/badline/frontend/file_browser.rb +225 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/menu_dialogs.rb +160 -0
- data/lib/badline/frontend/menu_media.rb +157 -0
- data/lib/badline/frontend/menu_pages.rb +220 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +264 -0
- data/lib/badline/frontend/player_window.rb +3 -3
- data/lib/badline/frontend/snapshots.rb +146 -18
- data/lib/badline/frontend/sound.rb +11 -6
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +8 -0
- data/lib/badline/kernal_trap/load.rb +10 -0
- data/lib/badline/kernal_trap/routine.rb +28 -0
- data/lib/badline/kernal_trap/serial.rb +19 -7
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +118 -0
- data/lib/badline/media.rb +9 -3
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +16 -12
- data/lib/badline/options/table.rb +6 -2
- data/lib/badline/options/validation.rb +9 -0
- data/lib/badline/options.rb +10 -7
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +14 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/storage/t64.rb +7 -4
- data/lib/badline/storage/tap.rb +7 -5
- data/lib/badline/storage/unavailable.rb +1 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline.rb +2 -38
- metadata +22 -1
|
@@ -85,17 +85,24 @@ module Badline
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
88
|
-
# in the secondary address that follows.
|
|
88
|
+
# in the secondary address that follows. Each byte sent under ATN,
|
|
89
|
+
# and each byte of a frame, times its handshake with CIA 1's timer B
|
|
90
|
+
# as ISOUR does.
|
|
89
91
|
def talk
|
|
90
92
|
@talk_channel = nil
|
|
91
93
|
@talking = @cpu.a == @device
|
|
92
|
-
|
|
94
|
+
sent_under_atn if @talking
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def listen
|
|
96
98
|
@listen_channel = nil
|
|
97
99
|
@listening = @cpu.a == @device
|
|
98
|
-
|
|
100
|
+
sent_under_atn if @listening
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def sent_under_atn
|
|
104
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
105
|
+
return_to_caller
|
|
99
106
|
end
|
|
100
107
|
|
|
101
108
|
def second
|
|
@@ -104,19 +111,22 @@ module Badline
|
|
|
104
111
|
@frame = @cpu.a & 0xf0
|
|
105
112
|
@listen_channel = @cpu.a & 0x0f
|
|
106
113
|
@buffer = []
|
|
107
|
-
|
|
114
|
+
sent_under_atn
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
def tksa
|
|
111
118
|
return unless @talking
|
|
112
119
|
|
|
113
120
|
@talk_channel = @cpu.a & 0x0f
|
|
114
|
-
|
|
121
|
+
sent_under_atn
|
|
115
122
|
end
|
|
116
123
|
|
|
124
|
+
# CIOUT holds each byte back until the next one, or the UNLISTEN,
|
|
125
|
+
# comes, so the first byte of a frame sends nothing.
|
|
117
126
|
def ciout
|
|
118
127
|
return unless @listening
|
|
119
128
|
|
|
129
|
+
time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
|
|
120
130
|
@buffer << @cpu.a
|
|
121
131
|
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
122
132
|
@cpu.status.carry = false
|
|
@@ -135,6 +145,7 @@ module Badline
|
|
|
135
145
|
|
|
136
146
|
@listening = false
|
|
137
147
|
deliver_frame
|
|
148
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
138
149
|
release_bus
|
|
139
150
|
end
|
|
140
151
|
|
|
@@ -142,14 +153,14 @@ module Badline
|
|
|
142
153
|
return unless @talking
|
|
143
154
|
|
|
144
155
|
@talking = false
|
|
156
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
145
157
|
release_bus
|
|
146
158
|
end
|
|
147
159
|
|
|
148
160
|
# UNLSN and UNTLK end by releasing ATN, the clock and the data line on
|
|
149
161
|
# CIA 2's port A, and leave the port's last read in the accumulator.
|
|
150
162
|
def release_bus
|
|
151
|
-
@
|
|
152
|
-
@cpu.a = @bus.peek(0xdd00) & 0xdf
|
|
163
|
+
@cpu.a = release_serial_lines
|
|
153
164
|
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
154
165
|
@cpu.status.zero = @cpu.a.zero?
|
|
155
166
|
return_to_caller
|
|
@@ -159,6 +170,7 @@ module Badline
|
|
|
159
170
|
return unless @talking
|
|
160
171
|
|
|
161
172
|
byte, status = received
|
|
173
|
+
time_serial_byte(ACPTR_TIMEOUT)
|
|
162
174
|
@cpu.a = byte
|
|
163
175
|
update_status(status)
|
|
164
176
|
@cpu.status.carry = false
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A list of the disks of a set: an .m3u playlist, one path a line, or
|
|
6
|
+
# a VICE .vfl flip list, whose paths follow a `UNIT 8` line. Paths are
|
|
7
|
+
# relative to the list or absolute, and blank lines and lines starting
|
|
8
|
+
# with # are skipped. Only the disk images the list names that are
|
|
9
|
+
# there count.
|
|
10
|
+
module DiskList
|
|
11
|
+
# A list naming no disk image that's there.
|
|
12
|
+
class Error < ArgumentError; end
|
|
13
|
+
|
|
14
|
+
EXTENSIONS = %w[.m3u .vfl].freeze
|
|
15
|
+
|
|
16
|
+
def self.list?(path) = EXTENSIONS.include?(File.extname(path).downcase)
|
|
17
|
+
|
|
18
|
+
# The disk `path` puts in the drive: a list's first disk, or the
|
|
19
|
+
# path itself.
|
|
20
|
+
def self.disk(path)
|
|
21
|
+
return path unless list?(path)
|
|
22
|
+
|
|
23
|
+
disks = disks(path)
|
|
24
|
+
raise Error, "#{path} lists no disk image that's there" if disks.empty?
|
|
25
|
+
|
|
26
|
+
disks.first
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The disk images the list names that are there, in its order.
|
|
30
|
+
def self.disks(list)
|
|
31
|
+
directory = File.dirname(list)
|
|
32
|
+
found = []
|
|
33
|
+
entries(list).each do |entry|
|
|
34
|
+
disk = entry.start_with?("/") ? entry : File.join(directory, entry)
|
|
35
|
+
found << disk if DiskSet::EXTENSIONS.include?(File.extname(disk).downcase) && File.file?(disk)
|
|
36
|
+
end
|
|
37
|
+
found
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The paths of the list's lines, only unit 8's for a .vfl, which
|
|
41
|
+
# starts on unit 8.
|
|
42
|
+
def self.entries(list)
|
|
43
|
+
flip = File.extname(list).casecmp?(".vfl")
|
|
44
|
+
unit = true
|
|
45
|
+
found = []
|
|
46
|
+
File.foreach(list) do |line|
|
|
47
|
+
text = line.strip
|
|
48
|
+
next if text.empty? || text.start_with?("#")
|
|
49
|
+
|
|
50
|
+
if flip && text.match?(/\AUNIT\s+\d+\z/i)
|
|
51
|
+
unit = text.split.last.to_i == 8
|
|
52
|
+
elsif unit
|
|
53
|
+
found << text
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
found
|
|
57
|
+
rescue SystemCallError
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The disks of the first list in the disk's folder that names it,
|
|
62
|
+
# with the disk as given, or none.
|
|
63
|
+
def self.beside(path)
|
|
64
|
+
directory = File.dirname(path)
|
|
65
|
+
own = File.expand_path(path)
|
|
66
|
+
lists = DiskSet.children(directory).sort.select { |name| list?(name) }
|
|
67
|
+
sets = lists.map { |name| disks(File.join(directory, name)) }
|
|
68
|
+
set = sets.find { |listed| listed.any? { |disk| File.expand_path(disk) == own } }
|
|
69
|
+
return [] if set.nil?
|
|
70
|
+
|
|
71
|
+
set.map { |disk| File.expand_path(disk) == own ? path : disk }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# The disks of a set a disk image belongs to, found by name in its
|
|
6
|
+
# folder: images whose names differ only in their disk, side or part
|
|
7
|
+
# markers, such as "Disk 1", "Side B", "d2", "(Disk 1 of 3)", ordered
|
|
8
|
+
# by those markers. A name with no marker can end in a number or a
|
|
9
|
+
# letter, as in game_1.d64 and game_2.d64, which counts only when the
|
|
10
|
+
# folder has the first of the set too.
|
|
11
|
+
#
|
|
12
|
+
# A list in the disk's folder that names it (DiskList) gives its set
|
|
13
|
+
# ahead of the names.
|
|
14
|
+
module DiskSet
|
|
15
|
+
KINDS = %w[disk disc side part].freeze
|
|
16
|
+
EXTENSIONS = %w[.d64 .d71 .d81 .g64].freeze
|
|
17
|
+
|
|
18
|
+
# The paths of the set's disks in order, or just `path` when it
|
|
19
|
+
# belongs to none. A list's set is the disks it lists.
|
|
20
|
+
def self.around(path)
|
|
21
|
+
return DiskList.disks(path) if DiskList.list?(path)
|
|
22
|
+
return [path] unless EXTENSIONS.include?(File.extname(path).downcase)
|
|
23
|
+
|
|
24
|
+
listed = DiskList.beside(path)
|
|
25
|
+
listed.empty? ? named(path) : listed
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The set of the disk's name.
|
|
29
|
+
def self.named(path)
|
|
30
|
+
extension = File.extname(path).downcase
|
|
31
|
+
marked = !parse(stem(path))[1].negative?
|
|
32
|
+
base = key(stem(path), marked)[0]
|
|
33
|
+
found = siblings(File.dirname(path), extension, base, marked)
|
|
34
|
+
return [path] if found.size < 2 || (!marked && found.none? { |pair| pair[0] == 1 })
|
|
35
|
+
|
|
36
|
+
found.sort_by { |pair| pair[0] }.map { |pair| File.join(File.dirname(path), pair[1]) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The name's words without their markers, joined, and the order the
|
|
40
|
+
# markers give it, -1 for none.
|
|
41
|
+
def self.parse(stem)
|
|
42
|
+
words = words(stem)
|
|
43
|
+
base = []
|
|
44
|
+
values = []
|
|
45
|
+
index = 0
|
|
46
|
+
index = take(words, index, base, values) while index < words.size
|
|
47
|
+
[base.join(" "), order(values)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The words before a trailing number or letter, joined, and its
|
|
51
|
+
# order, -1 for a name that doesn't end in one.
|
|
52
|
+
def self.trailing(stem)
|
|
53
|
+
words = words(stem)
|
|
54
|
+
last = words.last.to_s
|
|
55
|
+
return ["", -1] if words.size < 2 || !value?(last) || last.length > 2
|
|
56
|
+
|
|
57
|
+
[words[0, words.size - 1].join(" "), value(last)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.words(stem) = stem.downcase.split(/[^a-z0-9]+/).reject(&:empty?)
|
|
61
|
+
|
|
62
|
+
def self.stem(path) = File.basename(path, File.extname(path))
|
|
63
|
+
|
|
64
|
+
def self.key(stem, marked) = marked ? parse(stem) : trailing(stem)
|
|
65
|
+
|
|
66
|
+
# The order and name of each image in the folder in the set.
|
|
67
|
+
def self.siblings(directory, extension, base, marked)
|
|
68
|
+
found = []
|
|
69
|
+
children(directory).each do |name|
|
|
70
|
+
next unless File.extname(name).downcase == extension
|
|
71
|
+
|
|
72
|
+
name_key = key(stem(name), marked)
|
|
73
|
+
found << [name_key[1], name] if name_key[0] == base && !name_key[1].negative?
|
|
74
|
+
end
|
|
75
|
+
found
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Adds the word at `index` to the base, or its marker's value to the
|
|
79
|
+
# values, and returns the index of the next word.
|
|
80
|
+
def self.take(words, index, base, values)
|
|
81
|
+
word = words[index]
|
|
82
|
+
following = words[index + 1].to_s
|
|
83
|
+
if KINDS.include?(word) && value?(following)
|
|
84
|
+
values << value(following)
|
|
85
|
+
index += 2
|
|
86
|
+
index += 2 if words[index] == "of" && words[index + 1].to_s.match?(/\A\d+\z/)
|
|
87
|
+
index
|
|
88
|
+
elsif marker?(word)
|
|
89
|
+
values << value(word.sub(/\A(disk|disc|side|part|d|s)/, ""))
|
|
90
|
+
index + 1
|
|
91
|
+
else
|
|
92
|
+
base << word
|
|
93
|
+
index + 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.marker?(word) = word.match?(/\A(disk|disc|side|part)(\d+|[a-z])\z/) || word.match?(/\A[ds]\d+\z/)
|
|
98
|
+
|
|
99
|
+
def self.value?(word) = word.match?(/\A(\d+|[a-z])\z/)
|
|
100
|
+
|
|
101
|
+
# A number counts as itself and a letter by its place in the
|
|
102
|
+
# alphabet, so side A is 1.
|
|
103
|
+
def self.value(word) = word.match?(/\A\d+\z/) ? word.to_i : word.ord - 96
|
|
104
|
+
|
|
105
|
+
def self.order(values)
|
|
106
|
+
return -1 if values.empty?
|
|
107
|
+
|
|
108
|
+
values.reduce(0) { |order, value| (order * 1000) + value }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.children(directory)
|
|
112
|
+
Dir.children(directory)
|
|
113
|
+
rescue SystemCallError
|
|
114
|
+
[]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/media/true_drive"
|
|
4
4
|
require "badline/media/queue"
|
|
5
|
+
require "badline/media/disk_set"
|
|
6
|
+
require "badline/media/disk_list"
|
|
5
7
|
|
|
6
8
|
module Badline
|
|
7
9
|
module Media
|
|
@@ -26,7 +28,11 @@ module Badline
|
|
|
26
28
|
# A .g64 plugs in a true drive as device 8 (TrueDrive), and with one
|
|
27
29
|
# there, a .d64 goes into it instead of the KERNAL traps too. The
|
|
28
30
|
# autostart then loads through it.
|
|
31
|
+
#
|
|
32
|
+
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
33
|
+
# (DiskList).
|
|
29
34
|
def attach(computer, path, autostart: true, subtune: nil, **options)
|
|
35
|
+
path = DiskList.disk(path)
|
|
30
36
|
if TrueDrive.takes?(computer, path)
|
|
31
37
|
attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
|
|
32
38
|
elsif File.directory?(path)
|
|
@@ -64,9 +70,9 @@ module Badline
|
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
# The SID a machine for `path` should be built with. A .sid tune names
|
|
67
|
-
# its own; everything else gets
|
|
68
|
-
def sid_model(path)
|
|
69
|
-
return
|
|
73
|
+
# its own; everything else gets `otherwise`.
|
|
74
|
+
def sid_model(path, otherwise: :mos6581)
|
|
75
|
+
return otherwise unless path && File.extname(path).downcase == ".sid"
|
|
70
76
|
|
|
71
77
|
Storage::SIDFile.new(path).sid_model
|
|
72
78
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# = Model
|
|
5
|
+
#
|
|
6
|
+
# The C64s badline builds by name, as `--model` selects them: the chips
|
|
7
|
+
# each was sold with, its video standard, its KERNAL and whether it has
|
|
8
|
+
# a datasette. The names are x64sc's.
|
|
9
|
+
module Model
|
|
10
|
+
# Each field but the name is a keyword Computer.new takes.
|
|
11
|
+
Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette)
|
|
12
|
+
|
|
13
|
+
# The breadbin PAL C64: the 6569 VIC-II, 6526 CIAs and the 6581 SID.
|
|
14
|
+
C64 = Profile.new(name: "c64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
15
|
+
region: Region::PAL, kernal: :c64, datasette: true)
|
|
16
|
+
|
|
17
|
+
# The PAL C64C: the HMOS 8565 VIC-II, 6526A CIAs and the 8580 SID.
|
|
18
|
+
C64C = Profile.new(name: "c64c", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
|
|
19
|
+
region: Region::PAL, kernal: :c64, datasette: true)
|
|
20
|
+
|
|
21
|
+
# The NTSC C64 with the 6567R8 VIC-II.
|
|
22
|
+
NTSC = Profile.new(name: "ntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
23
|
+
region: Region::NTSC, kernal: :c64, datasette: true)
|
|
24
|
+
|
|
25
|
+
# The NTSC C64C: the 8562, the 6567R8's HMOS successor, with the
|
|
26
|
+
# C64C's CIAs and SID.
|
|
27
|
+
NEW_NTSC = Profile.new(name: "newntsc", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
|
|
28
|
+
region: Region::NTSC, kernal: :c64, datasette: true)
|
|
29
|
+
|
|
30
|
+
# The first NTSC C64s, with the 6567R56A VIC-II.
|
|
31
|
+
OLD_NTSC = Profile.new(name: "oldntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
32
|
+
region: Region::NTSC_OLD, kernal: :c64, datasette: true)
|
|
33
|
+
|
|
34
|
+
# The Drean C64 of Argentina, on PAL-N with the 6572 VIC-II.
|
|
35
|
+
DREAN = Profile.new(name: "drean", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
36
|
+
region: Region::DREAN, kernal: :c64, datasette: true)
|
|
37
|
+
|
|
38
|
+
# The portable SX-64: a PAL C64's chips with its own KERNAL, a 1541
|
|
39
|
+
# built in as device 8 and no cassette port.
|
|
40
|
+
SX64 = Profile.new(name: "sx64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
41
|
+
region: Region::PAL, kernal: :sx64, datasette: false)
|
|
42
|
+
|
|
43
|
+
ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64].freeze
|
|
44
|
+
|
|
45
|
+
def self.named(name)
|
|
46
|
+
model = ALL.find { |candidate| candidate.name == name }
|
|
47
|
+
raise ArgumentError, "no C64 model named #{name}" if model.nil?
|
|
48
|
+
|
|
49
|
+
model
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The model whose chips, region, KERNAL and datasette `machine` has, or
|
|
53
|
+
# nil for none.
|
|
54
|
+
def self.of(machine)
|
|
55
|
+
ALL.find do |model|
|
|
56
|
+
model.vic_model == machine.vic_model && model.cia_model == machine.cia_model &&
|
|
57
|
+
model.sid_model == machine.sid_model && model.region == machine.region &&
|
|
58
|
+
model.kernal == machine.kernal && model.datasette == machine.datasette
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -17,8 +17,8 @@ module Badline
|
|
|
17
17
|
|
|
18
18
|
EJECTABLE = %w[disk tape cartridge].freeze
|
|
19
19
|
|
|
20
|
-
# What insert takes: disk images, tapes and cartridges, or
|
|
21
|
-
# directory to mount as device 8.
|
|
20
|
+
# What insert takes: disk images, tapes and cartridges, or an .m3u
|
|
21
|
+
# or .vfl list of disks or a directory to mount as device 8.
|
|
22
22
|
INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
|
|
23
23
|
|
|
24
24
|
attr_reader :frame, :action, :argument
|
|
@@ -87,7 +87,9 @@ module Badline
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def self.insertable?(path)
|
|
90
|
-
|
|
90
|
+
return true if File.directory?(path)
|
|
91
|
+
|
|
92
|
+
File.exist?(path) && (INSERTABLE.include?(File.extname(path).downcase) || Media::DiskList.list?(path))
|
|
91
93
|
end
|
|
92
94
|
end
|
|
93
95
|
end
|
data/lib/badline/options/help.rb
CHANGED
|
@@ -43,14 +43,18 @@ module Badline
|
|
|
43
43
|
#{@program} sid [options] FILE|DIR...
|
|
44
44
|
|
|
45
45
|
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
|
|
46
|
-
.g64 disk image for the true 1541,
|
|
47
|
-
|
|
48
|
-
to mount as device 8. It
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
.g64 disk image for the true 1541, an .m3u or .vfl list of disk
|
|
47
|
+
images, a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid
|
|
48
|
+
tune, a .vsf snapshot, or a directory to mount as device 8. It
|
|
49
|
+
opens in the emulator window. There F11 quicksaves the machine to
|
|
50
|
+
the next of five slots in badline's data folder, replacing the
|
|
51
|
+
oldest, and F12 restores the newest quicksave or named save. The
|
|
52
|
+
window also autosaves every two minutes of play, which only the
|
|
53
|
+
pause menu loads. Device 8 answers through traps on the KERNAL's
|
|
54
|
+
disk routines, unless --true-drive puts an emulated 1541 there,
|
|
55
|
+
which runs its own DOS and reads .d64 and .g64 images only. A .sid
|
|
56
|
+
tune opens in the SID player, as `sid` plays it, unless an option
|
|
57
|
+
of the emulator's window asks for the machine.
|
|
54
58
|
|
|
55
59
|
--headless plays a .sid tune on the host's audio device without the
|
|
56
60
|
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
@@ -74,10 +78,10 @@ module Badline
|
|
|
74
78
|
key=NAME presses a C64 key (space, return, a, f1, run_stop, restore)
|
|
75
79
|
or a joystick's direction or fire (joy1-up, joy2-fire) for #{Event::HOLD}
|
|
76
80
|
frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
|
|
77
|
-
in a disk,
|
|
78
|
-
takes one out. screenshot=FILE saves
|
|
79
|
-
frame's number in place of %d. reset,
|
|
80
|
-
argument.
|
|
81
|
+
in a disk, a list's first disk, a tape or a cartridge, and
|
|
82
|
+
eject=disk, tape or cartridge takes one out. screenshot=FILE saves
|
|
83
|
+
the frame as a .bmp, with the frame's number in place of %d. reset,
|
|
84
|
+
freeze and quit take no argument.
|
|
81
85
|
BANNER
|
|
82
86
|
end
|
|
83
87
|
|
|
@@ -4,6 +4,9 @@ module Badline
|
|
|
4
4
|
class Options
|
|
5
5
|
DEFAULT_RATE = 44_100
|
|
6
6
|
|
|
7
|
+
# The names Badline::Model builds, in the order the help lists them.
|
|
8
|
+
MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
|
|
9
|
+
|
|
7
10
|
SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
|
|
8
11
|
|
|
9
12
|
# Every option of both builds, in the order the help lists them.
|
|
@@ -16,7 +19,7 @@ module Badline
|
|
|
16
19
|
Option.new("--version", "Show the version and what built it", section: "Options", build: :native),
|
|
17
20
|
Option.new("--no-autostart", "Boot to READY. instead of running the program",
|
|
18
21
|
section: "Window options", needs: :window),
|
|
19
|
-
Option.new("--
|
|
22
|
+
Option.new("--writable", "Let the machine write to disk images, changing their files",
|
|
20
23
|
section: "Window options", needs: :window),
|
|
21
24
|
Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
|
|
22
25
|
section: "Window options", needs: :window),
|
|
@@ -25,8 +28,9 @@ module Badline
|
|
|
25
28
|
section: "Window options", needs: :window),
|
|
26
29
|
Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
|
|
27
30
|
section: "Window options", needs: :window),
|
|
28
|
-
Option.new("--
|
|
31
|
+
Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first})",
|
|
29
32
|
section: "Window options", needs: :window),
|
|
33
|
+
Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
|
|
30
34
|
Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
|
|
31
35
|
section: "Window options", needs: :window),
|
|
32
36
|
Option.new("--verbose", "Print the display, sound and gamepad setup and the frame timing",
|
|
@@ -10,6 +10,7 @@ module Badline
|
|
|
10
10
|
|
|
11
11
|
def validate
|
|
12
12
|
validate_mode
|
|
13
|
+
validate_models
|
|
13
14
|
validate_numbers
|
|
14
15
|
validate_paths
|
|
15
16
|
end
|
|
@@ -33,6 +34,14 @@ module Badline
|
|
|
33
34
|
raise Error, "not a .sid tune: #{path}"
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# --model and --ntsc may be given more than once, but only for one
|
|
38
|
+
# model.
|
|
39
|
+
def validate_models
|
|
40
|
+
unknown = @models.find { |name| !MODELS.include?(name) }
|
|
41
|
+
raise Error, "invalid argument: --model #{unknown}" unless unknown.nil?
|
|
42
|
+
raise Error, "conflicting models: #{@models.uniq.join(' and ')}" unless @models.uniq.size <= 1
|
|
43
|
+
end
|
|
44
|
+
|
|
36
45
|
def validate_numbers
|
|
37
46
|
positive("--subtune", @subtune)
|
|
38
47
|
positive("--seconds", @seconds)
|
data/lib/badline/options.rb
CHANGED
|
@@ -59,16 +59,16 @@ module Badline
|
|
|
59
59
|
@tune_paths = []
|
|
60
60
|
@subtune = nil
|
|
61
61
|
@sid_model = nil
|
|
62
|
+
@models = []
|
|
62
63
|
@reu = nil
|
|
63
64
|
@autostart = true
|
|
64
|
-
@
|
|
65
|
+
@writable = false
|
|
65
66
|
@sound = native
|
|
66
67
|
@frames = 0
|
|
67
68
|
@paced = true
|
|
68
69
|
@vsync = true
|
|
69
70
|
@verbose = false
|
|
70
71
|
@true_drive = false
|
|
71
|
-
@ntsc = false
|
|
72
72
|
@screenshot = ""
|
|
73
73
|
@save_snapshot = ""
|
|
74
74
|
@timeline = []
|
|
@@ -135,7 +135,7 @@ module Badline
|
|
|
135
135
|
|
|
136
136
|
def autostart? = @autostart
|
|
137
137
|
|
|
138
|
-
def
|
|
138
|
+
def writable? = @writable
|
|
139
139
|
|
|
140
140
|
def sound? = @sound
|
|
141
141
|
|
|
@@ -147,10 +147,12 @@ module Badline
|
|
|
147
147
|
|
|
148
148
|
def true_drive? = @true_drive
|
|
149
149
|
|
|
150
|
-
def ntsc? = @ntsc
|
|
151
|
-
|
|
152
150
|
def jit? = @jit
|
|
153
151
|
|
|
152
|
+
# The name of the C64 to build (Badline::Model). --ntsc is short for
|
|
153
|
+
# --model ntsc.
|
|
154
|
+
def model = @models.empty? ? MODELS.first : @models.first
|
|
155
|
+
|
|
154
156
|
def help? = @help
|
|
155
157
|
|
|
156
158
|
def version? = @version
|
|
@@ -222,6 +224,7 @@ module Badline
|
|
|
222
224
|
case name
|
|
223
225
|
when "--subtune" then @subtune = number(flag, value)
|
|
224
226
|
when "--sid" then @sid_model = sid_model_for(value)
|
|
227
|
+
when "--model" then @models << value
|
|
225
228
|
when "--reu" then @reu = reu_size(value)
|
|
226
229
|
when "--frames" then @frames = number(flag, value)
|
|
227
230
|
when "--screenshot" then @screenshot = value
|
|
@@ -238,13 +241,13 @@ module Badline
|
|
|
238
241
|
def switch(name)
|
|
239
242
|
case name
|
|
240
243
|
when "--no-autostart" then @autostart = false
|
|
241
|
-
when "--
|
|
244
|
+
when "--writable" then @writable = true
|
|
242
245
|
when "--sound" then @sound = true
|
|
243
246
|
when "--no-sound" then @sound = false
|
|
244
247
|
when "--no-vsync" then @vsync = false
|
|
245
248
|
when "--verbose" then @verbose = true
|
|
246
249
|
when "--true-drive" then @true_drive = true
|
|
247
|
-
when "--ntsc" then @
|
|
250
|
+
when "--ntsc" then @models << "ntsc"
|
|
248
251
|
when "--unpaced" then @paced = false
|
|
249
252
|
when "--disable-jit" then @jit = false
|
|
250
253
|
when "--help" then @help = true
|
data/lib/badline/region.rb
CHANGED
|
@@ -21,8 +21,8 @@ module Badline
|
|
|
21
21
|
# `sprite_cycle` is Bauer's cycle for sprite 0's p-access, and each
|
|
22
22
|
# sprite after it takes two more. `sprite_display_cycle` is the cycle
|
|
23
23
|
# that turns a sprite's display on or off. `x_hold` is how many pixels
|
|
24
|
-
# the X counter spends past its run,
|
|
25
|
-
# of more than 512 pixels needs them.
|
|
24
|
+
# the X counter spends past its run, going over $180-$187 a second
|
|
25
|
+
# time, and a raster of more than 512 pixels needs them.
|
|
26
26
|
Profile = Data.define(:name, :clock_hz, :cycles_per_line, :lines_per_frame, :hblank, :vblank,
|
|
27
27
|
:display_x_bounds, :crop, :mains_hz, :sprite_cycle, :sprite_display_cycle,
|
|
28
28
|
:x_hold)
|
|
@@ -40,8 +40,8 @@ module Badline
|
|
|
40
40
|
# The 6567R8 of an NTSC C64: 65 cycles by 263 lines at 1,022,727 Hz,
|
|
41
41
|
# with 60 Hz mains. The sprite fetches and the compares ahead of them
|
|
42
42
|
# run a cycle later than on the 6569 (the spritesteal, spritex and
|
|
43
|
-
# phi1timing testprogs), and the X counter
|
|
44
|
-
#
|
|
43
|
+
# phi1timing testprogs), and the X counter runs over $180-$187 twice,
|
|
44
|
+
# 8 pixels more, as the 6567R8's lightpen dump shows. The
|
|
45
45
|
# blanked lines are the ones VICE's NTSC view leaves out, 12-27. It shows lines 0-11 below
|
|
46
46
|
# line 262, and the crop leaves those out.
|
|
47
47
|
NTSC = Profile.new(
|
|
@@ -65,6 +65,18 @@ module Badline
|
|
|
65
65
|
sprite_cycle: 59, sprite_display_cycle: 58, x_hold: 0
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
+
# The 6572 of the Drean C64, sold in Argentina on PAL-N: 65 cycles by
|
|
69
|
+
# 312 lines at 1,023,440 Hz, with 50 Hz mains. It lays out the line
|
|
70
|
+
# as the 6567R8 does, sprite fetches and X counter hold included
|
|
71
|
+
# (spritescan_drean), and blanks and crops lines as PAL does.
|
|
72
|
+
DREAN = Profile.new(
|
|
73
|
+
name: :drean, clock_hz: 1_023_440, cycles_per_line: 65, lines_per_frame: 312,
|
|
74
|
+
hblank: [63, 9].freeze, vblank: [300, 15].freeze,
|
|
75
|
+
display_x_bounds: [[135, 438].freeze, [128, 447].freeze].freeze,
|
|
76
|
+
crop: [96, 20, 384, 272].freeze, mains_hz: 50,
|
|
77
|
+
sprite_cycle: 59, sprite_display_cycle: 59, x_hold: 8
|
|
78
|
+
)
|
|
79
|
+
|
|
68
80
|
# Whether a column or line lies in a [first, last] blanking span.
|
|
69
81
|
def self.blanked?(position, span)
|
|
70
82
|
first, last = span
|
data/lib/badline/reu/ram.rb
CHANGED
|
@@ -52,8 +52,35 @@ module Badline
|
|
|
52
52
|
@banks.each_index { |number| @banks[number] = input.boolean? ? input.blob : [] }
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Every byte, as one string, with a bank the REC hasn't touched at
|
|
56
|
+
# its power-on contents.
|
|
57
|
+
def contents
|
|
58
|
+
power_on = power_on_strings
|
|
59
|
+
Array.new(@banks.length) do |number|
|
|
60
|
+
bank = @banks[number]
|
|
61
|
+
bank.empty? ? power_on[(number >> 1) & 1] : bank.pack("C*")
|
|
62
|
+
end.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Takes every byte from a string as long as the RAM. A bank that
|
|
66
|
+
# holds its power-on contents stays untouched.
|
|
67
|
+
def restore(bytes)
|
|
68
|
+
power_on = power_on_strings
|
|
69
|
+
@banks.each_index do |number|
|
|
70
|
+
slice = bytes.byteslice(number * BANK_SIZE, BANK_SIZE).to_s
|
|
71
|
+
@banks[number] = slice == power_on[(number >> 1) & 1] ? [] : slice.bytes
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
55
75
|
private
|
|
56
76
|
|
|
77
|
+
def power_on_strings
|
|
78
|
+
[0, 1].map do |half|
|
|
79
|
+
@power_on[half] = power_on_bank(half == 1) if @power_on[half].empty?
|
|
80
|
+
@power_on[half].pack("C*")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
# A bank takes its power-on contents the first time it is touched,
|
|
58
85
|
# so an REU costs only the memory a program uses.
|
|
59
86
|
def bank(number)
|
|
@@ -32,6 +32,33 @@ module Badline
|
|
|
32
32
|
@ram.load_state(input)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# The sixteen bytes the REC's registers read from $DF00, without
|
|
36
|
+
# what reading the status does, as VICE's REU1764 module holds them.
|
|
37
|
+
def register_file = Array.new(16) { |register| register_value(register) }
|
|
38
|
+
|
|
39
|
+
# Sets the registers from a register file, as writing them would but
|
|
40
|
+
# without starting a transfer: each address and the length set both
|
|
41
|
+
# the counter and the value it starts from. A command with the
|
|
42
|
+
# execute bit set and the $FF00 trigger on arms the REC.
|
|
43
|
+
def restore_registers(values)
|
|
44
|
+
@status = (values[STATUS] & (INTERRUPT_PENDING | END_OF_BLOCK | FAULT)) | @chips
|
|
45
|
+
@command = values[COMMAND]
|
|
46
|
+
@c64 = @c64_start = values[C64_LOW] | (values[C64_HIGH] << 8)
|
|
47
|
+
@expansion = @expansion_start = values[EXPANSION_LOW] | (values[EXPANSION_HIGH] << 8)
|
|
48
|
+
@bank = @bank_start = values[EXPANSION_BANK] & @bank_bits
|
|
49
|
+
@length = @length_start = values[LENGTH_LOW] | (values[LENGTH_HIGH] << 8)
|
|
50
|
+
@interrupt_mask = values[INTERRUPT_MASK] | 0x1f
|
|
51
|
+
@address_control = values[ADDRESS_CONTROL] | 0x3f
|
|
52
|
+
@armed = @command.anybits?(EXECUTE) && @command.nobits?(NO_FF00_TRIGGER)
|
|
53
|
+
@requested = @running = @ba_was_low = false
|
|
54
|
+
interrupt(@status.anybits?(INTERRUPT_PENDING))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The RAM, every byte of it, as one string.
|
|
58
|
+
def ram_contents = @ram.contents
|
|
59
|
+
|
|
60
|
+
def restore_ram(bytes) = @ram.restore(bytes)
|
|
61
|
+
|
|
35
62
|
private
|
|
36
63
|
|
|
37
64
|
def load_registers(input)
|