badline 0.15.0 → 0.16.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 +102 -0
- data/CLAUDE.md +15 -7
- data/README.md +142 -16
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +274 -31
- data/doc/snapshots.md +14 -0
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/machine_player.rb +2 -14
- data/lib/badline/c64/common.rb +1 -1
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/crtc/keyed_state.rb +46 -0
- data/lib/badline/chips/crtc.rb +163 -0
- data/lib/badline/chips/pia/control_lines.rb +138 -0
- data/lib/badline/chips/pia/keyed_parts.rb +56 -0
- data/lib/badline/chips/pia.rb +138 -0
- data/lib/badline/chips/tia.rb +16 -0
- data/lib/badline/core.rb +3 -0
- data/lib/badline/drives/datasette/keyed_state.rb +8 -3
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/ieee_bus.rb +28 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +107 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +71 -0
- data/lib/badline/drives/model1551.rb +192 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/drives.rb +1 -0
- data/lib/badline/frontend/app.rb +1 -1
- data/lib/badline/frontend/boot.rb +20 -1
- data/lib/badline/frontend/controls.rb +23 -3
- data/lib/badline/frontend/gamepads.rb +10 -3
- data/lib/badline/frontend/keys.rb +88 -0
- data/lib/badline/frontend/media_slots.rb +1 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
- data/lib/badline/frontend/menu/menu_media.rb +96 -19
- data/lib/badline/frontend/menu/menu_pages.rb +25 -7
- data/lib/badline/frontend/menu/menu_ports.rb +22 -1
- data/lib/badline/frontend/menu/pause_menu.rb +1 -0
- data/lib/badline/frontend/pet_keyboard.rb +124 -0
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/timeline.rb +16 -9
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/dos/writes.rb +3 -1
- data/lib/badline/kernal_trap/dos.rb +6 -1
- data/lib/badline/kernal_trap/frames.rb +117 -0
- data/lib/badline/kernal_trap/ieee.rb +148 -0
- data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
- data/lib/badline/kernal_trap/keyed_state.rb +4 -4
- data/lib/badline/kernal_trap/routine.rb +3 -53
- data/lib/badline/kernal_trap/serial.rb +4 -102
- data/lib/badline/kernal_trap/steps.rb +64 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/machine/keyboard_buffer.rb +8 -0
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +37 -11
- data/lib/badline/media/extensions.rb +5 -2
- data/lib/badline/media/pet_media.rb +40 -0
- data/lib/badline/media/program.rb +14 -2
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +36 -16
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +23 -5
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +28 -4
- data/lib/badline/options/validation.rb +117 -14
- data/lib/badline/options.rb +65 -21
- data/lib/badline/pet/bus/expansion.rb +106 -0
- data/lib/badline/pet/bus.rb +204 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +26 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +142 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_ports.rb +66 -0
- data/lib/badline/pet/ieee_traps.rb +75 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +85 -0
- data/lib/badline/pet/keyed_parts.rb +123 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +113 -0
- data/lib/badline/pet/rom_sets.rb +65 -0
- data/lib/badline/pet/sound.rb +128 -0
- data/lib/badline/pet/via.rb +10 -0
- data/lib/badline/pet/via_ports.rb +19 -0
- data/lib/badline/pet.rb +226 -0
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +181 -45
- data/lib/badline/plus4/keyed_modules.rb +22 -5
- data/lib/badline/plus4/keyed_parts.rb +33 -4
- data/lib/badline/plus4/keyed_state.rb +10 -5
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +134 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +104 -22
- data/lib/badline/roms/README +146 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/roms/pet/basic1.rom +0 -0
- data/lib/badline/roms/pet/basic2.rom +0 -0
- data/lib/badline/roms/pet/basic4.rom +0 -0
- data/lib/badline/roms/pet/characters1.rom +0 -0
- data/lib/badline/roms/pet/characters2.rom +0 -0
- data/lib/badline/roms/pet/editor1.rom +0 -0
- data/lib/badline/roms/pet/editor2-b.rom +0 -0
- data/lib/badline/roms/pet/editor2-n.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-b.rom +0 -0
- data/lib/badline/roms/pet/editor4-n.rom +0 -0
- data/lib/badline/roms/pet/kernal1.rom +0 -0
- data/lib/badline/roms/pet/kernal2.rom +0 -0
- data/lib/badline/roms/pet/kernal4.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +21 -6
- data/lib/badline/snapshot/image.rb +14 -7
- data/lib/badline/snapshot/pet_setup.rb +71 -0
- data/lib/badline/snapshot/plus4_setup.rb +52 -10
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot.rb +9 -5
- data/lib/badline/storage/d80_image.rb +48 -0
- data/lib/badline/storage/d82_image.rb +21 -0
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/storage/tap.rb +16 -3
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/support/pet_checkpoint.rb +38 -0
- data/lib/badline/timing.rb +5 -4
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +7 -2
- data/lib/badline/vic20/bus/keyed_state.rb +5 -4
- data/lib/badline/vic20/bus.rb +5 -0
- data/lib/badline/vic20.rb +1 -1
- metadata +69 -1
|
@@ -8,6 +8,18 @@ module Badline
|
|
|
8
8
|
module Validation
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
def sid_card_for(value)
|
|
12
|
+
raise Error, "invalid argument: --sid-card #{value}" unless SID_MODELS.key?(value)
|
|
13
|
+
|
|
14
|
+
SID_MODELS[value]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sid_card_address_for(value)
|
|
18
|
+
raise Error, "invalid argument: --sid-card-address #{value}" unless SID_CARD_ADDRESSES.key?(value.downcase)
|
|
19
|
+
|
|
20
|
+
SID_CARD_ADDRESSES[value.downcase]
|
|
21
|
+
end
|
|
22
|
+
|
|
11
23
|
def sid_model_for(value)
|
|
12
24
|
return if value == "auto"
|
|
13
25
|
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
@@ -27,6 +39,24 @@ module Badline
|
|
|
27
39
|
PLUS4_RAM[value]
|
|
28
40
|
end
|
|
29
41
|
|
|
42
|
+
def pet_ram(value)
|
|
43
|
+
raise Error, "invalid argument: --ram #{value}" unless PET_RAM.key?(value)
|
|
44
|
+
|
|
45
|
+
PET_RAM[value]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def plus4_ram_expansion(value)
|
|
49
|
+
raise Error, "invalid argument: --ram-expansion #{value}" unless PLUS4_RAM_EXPANSIONS.key?(value)
|
|
50
|
+
|
|
51
|
+
PLUS4_RAM_EXPANSIONS[value]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def drive_type_for(value)
|
|
55
|
+
raise Error, "invalid argument: --drive-type #{value}" unless PLUS4_DRIVE_TYPES.include?(value)
|
|
56
|
+
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
30
60
|
def reu_size(value)
|
|
31
61
|
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
32
62
|
|
|
@@ -47,6 +77,8 @@ module Badline
|
|
|
47
77
|
|
|
48
78
|
def validate
|
|
49
79
|
validate_family
|
|
80
|
+
raise Error, "--drive-type needs plus4" unless @drive_type.nil? || @family == :plus4
|
|
81
|
+
|
|
50
82
|
validate_mode
|
|
51
83
|
validate_models
|
|
52
84
|
validate_numbers
|
|
@@ -56,6 +88,7 @@ module Badline
|
|
|
56
88
|
def validate_mode
|
|
57
89
|
if headless?
|
|
58
90
|
raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
|
|
91
|
+
raise Error, "#{@any_window_only.first} needs the window" unless player_window? || @any_window_only.empty?
|
|
59
92
|
raise Error, "no tune given" if tune_paths.empty? && !player_window?
|
|
60
93
|
|
|
61
94
|
tune_paths.each { |path| validate_tune(path) }
|
|
@@ -65,16 +98,10 @@ module Badline
|
|
|
65
98
|
end
|
|
66
99
|
|
|
67
100
|
def validate_family
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
if @family == :plus4
|
|
74
|
-
validate_plus4
|
|
75
|
-
elsif !@cartridges.empty?
|
|
76
|
-
raise Error, "--#{@cartridges.keys.first} needs plus4"
|
|
77
|
-
end
|
|
101
|
+
ram_option
|
|
102
|
+
validate_vic20 if @family == :vic20
|
|
103
|
+
@family == :plus4 ? validate_plus4 : refuse_plus4_options
|
|
104
|
+
@family == :pet ? validate_pet : refuse_pet_options
|
|
78
105
|
if @family == :c128
|
|
79
106
|
validate_c128
|
|
80
107
|
elsif @c64_mode
|
|
@@ -82,6 +109,19 @@ module Badline
|
|
|
82
109
|
end
|
|
83
110
|
end
|
|
84
111
|
|
|
112
|
+
# --ram names the VIC-20's RAM expansion, or the Plus/4's or the PET's
|
|
113
|
+
# RAM.
|
|
114
|
+
def ram_option
|
|
115
|
+
return if @ram_name.nil?
|
|
116
|
+
|
|
117
|
+
case @family
|
|
118
|
+
when :vic20 then @ram = ram_configuration(@ram_name)
|
|
119
|
+
when :plus4 then @ram_kb = plus4_ram(@ram_name)
|
|
120
|
+
when :pet then @ram_kb = pet_ram(@ram_name)
|
|
121
|
+
else raise Error, "--ram needs vic20, plus4 or pet"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
85
125
|
# The C128 runs in the window, without an REU, and its NTSC models
|
|
86
126
|
# have names of their own.
|
|
87
127
|
def validate_c128
|
|
@@ -100,19 +140,82 @@ module Badline
|
|
|
100
140
|
raise Error, "the VIC-20 needs the window" if headless?
|
|
101
141
|
end
|
|
102
142
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
143
|
+
# Only the Plus/4 has cartridge slots for raw ROM images and takes a
|
|
144
|
+
# SID card and a RAM expansion.
|
|
145
|
+
def refuse_plus4_options
|
|
146
|
+
raise Error, "--#{@cartridges.keys.first} needs plus4" unless @cartridges.empty?
|
|
147
|
+
raise Error, "--sid-card needs plus4" unless @sid_card.nil?
|
|
148
|
+
raise Error, "--sid-card-address needs plus4" unless @sid_card_address.nil?
|
|
149
|
+
raise Error, "--ram-expansion needs plus4" unless @ram_expansion.nil?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The Plus/4 runs in the window, without the C64's SID and REU but
|
|
153
|
+
# with a SID card if asked, its NTSC models have names of their own,
|
|
154
|
+
# and its cartridge slots take raw ROM images.
|
|
106
155
|
def validate_plus4
|
|
107
156
|
raise Error, "--ntsc needs the C64; the NTSC Plus/4 is --model plus4ntsc" if @models.include?("ntsc")
|
|
108
|
-
raise Error, "--sid needs the C64" unless @sid_model.nil?
|
|
157
|
+
raise Error, "--sid needs the C64; the Plus/4's is --sid-card" unless @sid_model.nil?
|
|
109
158
|
raise Error, "--reu needs the C64" unless @reu.nil?
|
|
110
159
|
raise Error, "the Plus/4 needs the window" if headless?
|
|
111
160
|
|
|
161
|
+
validate_plus4_expansions
|
|
112
162
|
image = @cartridges.values.find { |path| !File.extname(path).casecmp?(".bin") }
|
|
113
163
|
raise Error, "not a .bin ROM image: #{image}" unless image.nil?
|
|
114
164
|
end
|
|
115
165
|
|
|
166
|
+
# A RAM expansion goes over 64K, and a SID card's address needs a
|
|
167
|
+
# card.
|
|
168
|
+
def validate_plus4_expansions
|
|
169
|
+
raise Error, "--ram-expansion needs 64k" unless @ram_expansion.nil? || [nil, 64].include?(@ram_kb)
|
|
170
|
+
raise Error, "--sid-card-address needs --sid-card" unless @sid_card_address.nil? || @sid_card
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Only the PET has a frame rate to pick and BASIC and editor ROMs to
|
|
174
|
+
# fit.
|
|
175
|
+
def refuse_pet_options
|
|
176
|
+
raise Error, "--60hz needs pet" unless @refresh_hz.nil?
|
|
177
|
+
raise Error, "--basic needs pet" unless @basic.nil?
|
|
178
|
+
raise Error, "--editor needs pet" unless @editor.nil?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The PET runs in the window, without the C64's SID and REU or a true
|
|
182
|
+
# drive, at the frame rate its editor sets.
|
|
183
|
+
def validate_pet
|
|
184
|
+
raise Error, "--ntsc needs the C64; a CRTC PET's 60 Hz editor is --60hz" if @models.include?("ntsc")
|
|
185
|
+
raise Error, "--sid needs the C64" unless @sid_model.nil?
|
|
186
|
+
raise Error, "--reu needs the C64" unless @reu.nil?
|
|
187
|
+
raise Error, "the PET has no true drive yet" if @true_drive
|
|
188
|
+
raise Error, "the PET needs the window" if headless?
|
|
189
|
+
|
|
190
|
+
validate_pet_roms if PET_MODELS.include?(model)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# --basic and --editor name ROMs the model's board takes
|
|
194
|
+
# (PET::ROMSets), and an editor named sets the frame rate.
|
|
195
|
+
def validate_pet_roms
|
|
196
|
+
raise Error, "invalid argument: --basic #{@basic}" unless @basic.nil? || PET::ROMSets::BASICS.key?(@basic)
|
|
197
|
+
raise Error, "invalid argument: --editor #{@editor}" unless @editor.nil? || PET::ROMSets::EDITORS.key?(@editor)
|
|
198
|
+
|
|
199
|
+
basic = @basic || PET::ROMSets.stock_basic(model)
|
|
200
|
+
fitting = PET::ROMSets.editors(model, basic)
|
|
201
|
+
raise Error, "the #{model} takes no BASIC #{basic}" if fitting.empty?
|
|
202
|
+
return if @editor.nil?
|
|
203
|
+
raise Error, "the #{model} with BASIC #{basic} takes no editor #{@editor}" unless fitting.include?(@editor)
|
|
204
|
+
|
|
205
|
+
editor_rate(fitting)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The frame rate of the editor named, on a CRTC board, which has a 50
|
|
209
|
+
# Hz and a 60 Hz one.
|
|
210
|
+
def editor_rate(fitting)
|
|
211
|
+
return if fitting.size == 1
|
|
212
|
+
|
|
213
|
+
editor_hz = @editor == fitting.last ? 60 : 50
|
|
214
|
+
raise Error, "--60hz takes a 60 Hz editor, not #{@editor}" if @refresh_hz == 60 && editor_hz == 50
|
|
215
|
+
|
|
216
|
+
@refresh_hz = editor_hz
|
|
217
|
+
end
|
|
218
|
+
|
|
116
219
|
# `sid` takes directories as well as tunes.
|
|
117
220
|
def validate_tune(path)
|
|
118
221
|
return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
|
data/lib/badline/options.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/pet/rom_sets"
|
|
3
4
|
require "badline/options/option"
|
|
4
5
|
require "badline/options/event"
|
|
5
6
|
require "badline/options/table"
|
|
@@ -12,10 +13,12 @@ module Badline
|
|
|
12
13
|
# Either opens the window for any media, or with --headless or
|
|
13
14
|
# --audio-out plays or renders a .sid tune without one. `sid`, given
|
|
14
15
|
# first, plays .sid tunes and directories of them in the terminal.
|
|
15
|
-
# `c64`, `vic20`, `c128` or `
|
|
16
|
-
# family, and without one the family is the C64,
|
|
17
|
-
# that loads where C128 BASIC starts
|
|
18
|
-
#
|
|
16
|
+
# `c64`, `vic20`, `c128`, `plus4` or `pet`, given first, names the
|
|
17
|
+
# machine family, and without one the family is the C64, the C128 for a
|
|
18
|
+
# program that loads where C128 BASIC starts, or the PET for an 8050's
|
|
19
|
+
# or 8250's disk or a PET tape.
|
|
20
|
+
# The native build adds --no-sound and --version, and badline-ruby adds
|
|
21
|
+
# --disable-jit.
|
|
19
22
|
#
|
|
20
23
|
# TABLE lists the options, and both the parser and the help read it. It
|
|
21
24
|
# parses by hand, inside the subset of Ruby Spinel compiles. Values come
|
|
@@ -51,6 +54,15 @@ module Badline
|
|
|
51
54
|
# The Plus/4's RAM sizes as --ram names them, in K.
|
|
52
55
|
PLUS4_RAM = { "16k" => 16, "32k" => 32, "64k" => 64 }.freeze
|
|
53
56
|
|
|
57
|
+
# The PET's, below $8000.
|
|
58
|
+
PET_RAM = { "4k" => 4, "8k" => 8, "16k" => 16, "32k" => 32 }.freeze
|
|
59
|
+
|
|
60
|
+
# Where --sid-card-address puts the Plus/4's SID card.
|
|
61
|
+
SID_CARD_ADDRESSES = { "fd40" => 0xfd40, "fe80" => 0xfe80 }.freeze
|
|
62
|
+
|
|
63
|
+
# The Plus/4's RAM expansions as --ram-expansion names them.
|
|
64
|
+
PLUS4_RAM_EXPANSIONS = { "hannes256" => :hannes256, "hannes1m" => :hannes1m, "csory256" => :csory256 }.freeze
|
|
65
|
+
|
|
54
66
|
# The Plus/4's cartridge slots, by the options that fill them and
|
|
55
67
|
# xplus4's names (Media::Plus4Media::SLOTS).
|
|
56
68
|
CARTRIDGE_SLOTS = { "--c1lo" => "c1lo", "--c1hi" => "c1hi", "--c2lo" => "c2lo", "--c2hi" => "c2hi" }.freeze
|
|
@@ -64,22 +76,38 @@ module Badline
|
|
|
64
76
|
attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
|
|
65
77
|
:audio_out, :seconds, :songlengths, :filter_chunk, :timeline
|
|
66
78
|
|
|
67
|
-
# The machine family to build (Badline::Machine), :c64, :vic20, :c128
|
|
68
|
-
# or :
|
|
79
|
+
# The machine family to build (Badline::Machine), :c64, :vic20, :c128,
|
|
80
|
+
# :plus4 or :pet.
|
|
69
81
|
attr_reader :family
|
|
70
82
|
|
|
83
|
+
# The ROMs --basic and --editor fit a PET with, keys of
|
|
84
|
+
# PET::ROMSets::BASICS and PET::ROMSets::EDITORS, or nil for its own.
|
|
85
|
+
attr_reader :basic, :editor
|
|
86
|
+
|
|
71
87
|
# The VIC-20's RAM expansion --ram names, a key of
|
|
72
88
|
# Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
|
|
73
89
|
attr_reader :ram
|
|
74
90
|
|
|
75
|
-
# The Plus/4's RAM in K --ram names, one of PLUS4_RAM's
|
|
76
|
-
# --ram.
|
|
91
|
+
# The Plus/4's or the PET's RAM in K --ram names, one of PLUS4_RAM's or
|
|
92
|
+
# PET_RAM's, or nil without --ram.
|
|
77
93
|
attr_reader :ram_kb
|
|
78
94
|
|
|
79
95
|
# The Plus/4's cartridge ROM images --c1lo, --c1hi, --c2lo and --c2hi
|
|
80
96
|
# name, by slot: { "c1lo" => path }.
|
|
81
97
|
attr_reader :cartridges
|
|
82
98
|
|
|
99
|
+
# The SID model of the Plus/4's SID card --sid-card names, :mos6581 or
|
|
100
|
+
# :mos8580, or nil without one.
|
|
101
|
+
attr_reader :sid_card
|
|
102
|
+
|
|
103
|
+
# Where --sid-card-address puts the Plus/4's SID card, $FD40 without
|
|
104
|
+
# it.
|
|
105
|
+
attr_reader :sid_card_address
|
|
106
|
+
|
|
107
|
+
# The Plus/4's RAM expansion --ram-expansion names, a key of
|
|
108
|
+
# Plus4::RAMExpansion::KINDS, or nil without one.
|
|
109
|
+
attr_reader :ram_expansion
|
|
110
|
+
|
|
83
111
|
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
84
112
|
|
|
85
113
|
# The native build plays the SID in the window unless --no-sound says
|
|
@@ -96,10 +124,17 @@ module Badline
|
|
|
96
124
|
@tune_paths = []
|
|
97
125
|
@subtune = nil
|
|
98
126
|
@sid_model = nil
|
|
127
|
+
@sid_card = nil
|
|
128
|
+
@sid_card_address = nil
|
|
129
|
+
@ram_expansion = nil
|
|
99
130
|
@models = []
|
|
100
131
|
@reu = nil
|
|
101
132
|
@ram = nil
|
|
102
133
|
@ram_kb = nil
|
|
134
|
+
@ram_name = nil
|
|
135
|
+
@refresh_hz = nil
|
|
136
|
+
@basic = nil
|
|
137
|
+
@editor = nil
|
|
103
138
|
@cartridges = {}
|
|
104
139
|
@autostart = true
|
|
105
140
|
@writable = false
|
|
@@ -109,6 +144,7 @@ module Badline
|
|
|
109
144
|
@vsync = true
|
|
110
145
|
@verbose = false
|
|
111
146
|
@true_drive = false
|
|
147
|
+
@drive_type = nil
|
|
112
148
|
@screenshot = ""
|
|
113
149
|
@save_snapshot = ""
|
|
114
150
|
@vice = false
|
|
@@ -126,6 +162,7 @@ module Badline
|
|
|
126
162
|
@tui = true
|
|
127
163
|
@all_subtunes = false
|
|
128
164
|
@window_only = []
|
|
165
|
+
@any_window_only = []
|
|
129
166
|
@headless_only = []
|
|
130
167
|
end
|
|
131
168
|
|
|
@@ -187,7 +224,10 @@ module Badline
|
|
|
187
224
|
|
|
188
225
|
def verbose? = @verbose
|
|
189
226
|
|
|
190
|
-
def true_drive? = @true_drive
|
|
227
|
+
def true_drive? = @true_drive || !@drive_type.nil?
|
|
228
|
+
|
|
229
|
+
# The Plus/4's true drive --drive-type names, "1551" without it.
|
|
230
|
+
def drive_type = @drive_type || "1551"
|
|
191
231
|
|
|
192
232
|
# Whether --save-snapshot adds VICE's modules, so x64sc opens it.
|
|
193
233
|
def vice? = @vice
|
|
@@ -203,6 +243,10 @@ module Badline
|
|
|
203
243
|
# power-on.
|
|
204
244
|
def c64_mode? = @c64_mode
|
|
205
245
|
|
|
246
|
+
# The frame rate of a CRTC PET's editor: 60 with --60hz or a 60 Hz
|
|
247
|
+
# --editor, else 50.
|
|
248
|
+
def refresh_hz = @refresh_hz || 50
|
|
249
|
+
|
|
206
250
|
def help? = @help
|
|
207
251
|
|
|
208
252
|
def version? = @version
|
|
@@ -218,7 +262,7 @@ module Badline
|
|
|
218
262
|
# A .sid on its own plays in the SID player, as `sid` would, unless an
|
|
219
263
|
# option of the emulator's window asks for the machine.
|
|
220
264
|
def play_lone_tune
|
|
221
|
-
return if @sid_command || %i[vic20 plus4].include?(@family) || headless? || !@window_only.empty?
|
|
265
|
+
return if @sid_command || %i[vic20 plus4 pet].include?(@family) || headless? || !@window_only.empty?
|
|
222
266
|
return if @media_path.nil?
|
|
223
267
|
|
|
224
268
|
@sid_command = File.extname(@media_path).casecmp?(".sid")
|
|
@@ -254,9 +298,11 @@ module Badline
|
|
|
254
298
|
option
|
|
255
299
|
end
|
|
256
300
|
|
|
257
|
-
# Notes an option only the window
|
|
301
|
+
# Notes an option only the window, only a window of either kind, or
|
|
302
|
+
# only the headless player takes.
|
|
258
303
|
def restrict(option, flag)
|
|
259
304
|
@window_only << flag if option.needs == :window
|
|
305
|
+
@any_window_only << flag if option.needs == :any_window
|
|
260
306
|
@headless_only << flag if option.needs == :headless
|
|
261
307
|
end
|
|
262
308
|
|
|
@@ -270,9 +316,15 @@ module Badline
|
|
|
270
316
|
case name
|
|
271
317
|
when "--subtune" then @subtune = number(flag, value)
|
|
272
318
|
when "--sid" then @sid_model = sid_model_for(value)
|
|
319
|
+
when "--sid-card" then @sid_card = sid_card_for(value)
|
|
320
|
+
when "--sid-card-address" then @sid_card_address = sid_card_address_for(value)
|
|
321
|
+
when "--ram-expansion" then @ram_expansion = plus4_ram_expansion(value)
|
|
273
322
|
when "--model" then @models << value
|
|
274
323
|
when "--reu" then @reu = reu_size(value)
|
|
275
|
-
when "--ram" then
|
|
324
|
+
when "--ram" then @ram_name = value
|
|
325
|
+
when "--basic" then @basic = value
|
|
326
|
+
when "--editor" then @editor = value
|
|
327
|
+
when "--drive-type" then @drive_type = drive_type_for(value)
|
|
276
328
|
when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
|
|
277
329
|
when "--frames" then @frames = number(flag, value)
|
|
278
330
|
when "--screenshot" then @screenshot = value
|
|
@@ -286,15 +338,6 @@ module Badline
|
|
|
286
338
|
end
|
|
287
339
|
end
|
|
288
340
|
|
|
289
|
-
# --ram names the VIC-20's RAM expansion, or the Plus/4's RAM.
|
|
290
|
-
def ram_option(value)
|
|
291
|
-
if @family == :plus4
|
|
292
|
-
@ram_kb = plus4_ram(value)
|
|
293
|
-
else
|
|
294
|
-
@ram = ram_configuration(value)
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
341
|
def switch(name)
|
|
299
342
|
case name
|
|
300
343
|
when "--no-autostart" then @autostart = false
|
|
@@ -307,6 +350,7 @@ module Badline
|
|
|
307
350
|
when "--vice" then @vice = true
|
|
308
351
|
when "--ntsc" then @models << "ntsc"
|
|
309
352
|
when "--c64" then @c64_mode = true
|
|
353
|
+
when "--60hz" then @refresh_hz = 60
|
|
310
354
|
when "--unpaced" then @paced = false
|
|
311
355
|
when "--disable-jit" then @jit = false
|
|
312
356
|
when "--help" then @help = true
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class PET
|
|
5
|
+
class Bus
|
|
6
|
+
# The 8096's 64K board: four 16K banks, which a write-only control
|
|
7
|
+
# register at $FFF0 lays over $8000-$FFFF, two at a time:
|
|
8
|
+
#
|
|
9
|
+
# bit 7 the board on; when clear, a plain 8032 and the rest ignored
|
|
10
|
+
# bit 6 I/O shows through at $E800-$EFFF
|
|
11
|
+
# bit 5 the screen shows through at $8000-$8FFF
|
|
12
|
+
# bit 3 bank 3 at $C000-$FFFF, else bank 1
|
|
13
|
+
# bit 2 bank 2 at $8000-$BFFF, else bank 0
|
|
14
|
+
# bit 1 writes to $C000-$FFFF go nowhere
|
|
15
|
+
# bit 0 writes to $8000-$BFFF go nowhere
|
|
16
|
+
#
|
|
17
|
+
# A write to $FFF0 lands where $FFF0 was before it, too. The RES line
|
|
18
|
+
# clears the register, so the 8096 starts as an 8032.
|
|
19
|
+
class Expansion
|
|
20
|
+
# A 16K bank of the board, seen from +start+.
|
|
21
|
+
class Window
|
|
22
|
+
attr_accessor :read_only
|
|
23
|
+
|
|
24
|
+
def initialize(cells, offset, start)
|
|
25
|
+
@cells = cells
|
|
26
|
+
@offset = offset - start
|
|
27
|
+
@read_only = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def peek(addr) = @cells[addr + @offset]
|
|
31
|
+
|
|
32
|
+
def poke(addr, value)
|
|
33
|
+
@cells[addr + @offset] = value unless @read_only
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Page $FF's writes, which pass to what the page holds and set the
|
|
38
|
+
# control register at $FFF0.
|
|
39
|
+
class ControlPort
|
|
40
|
+
attr_accessor :under
|
|
41
|
+
|
|
42
|
+
def initialize(expansion, under)
|
|
43
|
+
@expansion = expansion
|
|
44
|
+
@under = under
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def poke(addr, value)
|
|
48
|
+
@under.poke(addr, value)
|
|
49
|
+
@expansion.control = value if addr == 0xfff0
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :cells, :control
|
|
54
|
+
|
|
55
|
+
def initialize(bus)
|
|
56
|
+
@bus = bus
|
|
57
|
+
@cells = Array.new(0x10000, 0)
|
|
58
|
+
@banks = [0x8000, 0xc000, 0x8000, 0xc000].each_with_index.map do |start, bank|
|
|
59
|
+
Window.new(@cells, bank * 0x4000, start)
|
|
60
|
+
end
|
|
61
|
+
@port = ControlPort.new(self, @banks[1])
|
|
62
|
+
@control = 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def control=(value)
|
|
66
|
+
@control = value
|
|
67
|
+
@bus.map_pages
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reset!
|
|
71
|
+
self.control = 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def power_on!
|
|
75
|
+
@cells.fill(0)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lays the banks the register picks over the pages, and the
|
|
79
|
+
# control register over page $FF's writes.
|
|
80
|
+
def map(reads, writes)
|
|
81
|
+
map_banks(reads, writes) if @control.anybits?(0x80)
|
|
82
|
+
@port.under = writes[0xff]
|
|
83
|
+
writes[0xff] = @port
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def map_banks(reads, writes)
|
|
89
|
+
low = @banks[@control.anybits?(0x04) ? 2 : 0]
|
|
90
|
+
high = @banks[@control.anybits?(0x08) ? 3 : 1]
|
|
91
|
+
low.read_only = @control.anybits?(0x01)
|
|
92
|
+
high.read_only = @control.anybits?(0x02)
|
|
93
|
+
first = @control.anybits?(0x20) ? 0x90 : 0x80
|
|
94
|
+
reads.fill(low, first, 0xc0 - first)
|
|
95
|
+
writes.fill(low, first, 0xc0 - first)
|
|
96
|
+
[[0xc0, 0x28], [0xe8, 0x08], [0xf0, 0x10]].each do |page, count|
|
|
97
|
+
next if page == 0xe8 && @control.anybits?(0x40)
|
|
98
|
+
|
|
99
|
+
reads.fill(high, page, count)
|
|
100
|
+
writes.fill(high, page, count)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/pet/bus/expansion"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class PET
|
|
7
|
+
# The CPU's bus:
|
|
8
|
+
#
|
|
9
|
+
# $0000-$7FFF - RAM, 4K, 8K, 16K or 32K of it from $0000, then nothing
|
|
10
|
+
# $8000-$8FFF - screen RAM, repeated (Model), then nothing
|
|
11
|
+
# $9000-$AFFF - two empty ROM sockets
|
|
12
|
+
# $B000-$DFFF - BASIC 4, or BASIC 1 or 2 from $C000
|
|
13
|
+
# $E000-$E7FF - the screen editor ROM
|
|
14
|
+
# $E800-$E8FF - I/O (IO)
|
|
15
|
+
# $E900-$EFFF - nothing
|
|
16
|
+
# $F000-$FFFF - the KERNAL ROM
|
|
17
|
+
#
|
|
18
|
+
# The data lines hold the last byte they carried, so a read from an
|
|
19
|
+
# empty spot finds it, mostly the high byte of the address just
|
|
20
|
+
# fetched, as PET/unconnected shows on a real 3016. Writes to ROM and
|
|
21
|
+
# to empty spots go nowhere. An 8096 banks its 64K board over
|
|
22
|
+
# $8000-$FFFF (Expansion).
|
|
23
|
+
class Bus
|
|
24
|
+
include Addressable
|
|
25
|
+
|
|
26
|
+
# The last byte the CPU moved, which an empty spot reads back.
|
|
27
|
+
class OpenBus
|
|
28
|
+
def initialize(bus)
|
|
29
|
+
@bus = bus
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def peek(_addr) = @bus.data
|
|
33
|
+
def poke(_addr, _value); end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Screen RAM, repeated over the pages it answers in.
|
|
37
|
+
class ScreenRAM
|
|
38
|
+
attr_reader :cells
|
|
39
|
+
|
|
40
|
+
def initialize(size)
|
|
41
|
+
@cells = Array.new(size, 0)
|
|
42
|
+
@mask = size - 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def peek(addr) = @cells[addr & @mask]
|
|
46
|
+
|
|
47
|
+
def poke(addr, value)
|
|
48
|
+
@cells[addr & @mask] = value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# $E800-$E8FF. Each chip answers on one address line, with its
|
|
53
|
+
# registers repeated: A4 selects PIA 1, A5 PIA 2, A6 the VIA and A7
|
|
54
|
+
# the CRTC, where there is one. Where more than one chip answers a
|
|
55
|
+
# read, each pulls the lines it drives low, so the CPU reads the AND
|
|
56
|
+
# of them, and a write reaches them all. Where none answers, as in
|
|
57
|
+
# $E800-$E80F, the CPU reads the last byte on the bus.
|
|
58
|
+
class IO
|
|
59
|
+
def initialize(bus, pia1:, pia2:, via:, crtc:, crtc_fitted:)
|
|
60
|
+
@bus = bus
|
|
61
|
+
@pia1 = pia1
|
|
62
|
+
@pia2 = pia2
|
|
63
|
+
@via = via
|
|
64
|
+
@crtc = crtc
|
|
65
|
+
@lines = crtc_fitted ? 0xf0 : 0x70
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def peek(addr)
|
|
69
|
+
return @bus.data if addr.nobits?(@lines)
|
|
70
|
+
|
|
71
|
+
value = 0xff
|
|
72
|
+
value &= @pia1.peek(addr) if addr.anybits?(0x10)
|
|
73
|
+
value &= @pia2.peek(addr) if addr.anybits?(0x20)
|
|
74
|
+
value &= @via.peek(addr) if addr.anybits?(0x40)
|
|
75
|
+
value &= @crtc.peek(addr) if addr.anybits?(@lines & 0x80)
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def poke(addr, value)
|
|
80
|
+
@pia1.poke(addr, value) if addr.anybits?(0x10)
|
|
81
|
+
@pia2.poke(addr, value) if addr.anybits?(0x20)
|
|
82
|
+
@via.poke(addr, value) if addr.anybits?(0x40)
|
|
83
|
+
@crtc.poke(addr, value) if addr.anybits?(@lines & 0x80)
|
|
84
|
+
@bus.io_written
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# VICE's debug cartridge register at $8BFF, which a write calls the
|
|
89
|
+
# handler with, the exit code the VICE testprogs report. It answers
|
|
90
|
+
# on every model, ahead of the 2001's screen RAM there and the
|
|
91
|
+
# 8096's banks, and every other write to its page passes to what
|
|
92
|
+
# the page holds.
|
|
93
|
+
class DebugRegister
|
|
94
|
+
ADDRESS = 0x8bff
|
|
95
|
+
|
|
96
|
+
attr_accessor :under
|
|
97
|
+
|
|
98
|
+
def initialize(&handler)
|
|
99
|
+
@handler = handler
|
|
100
|
+
@under = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def poke(addr, value)
|
|
104
|
+
return @handler.call(value) if addr == ADDRESS
|
|
105
|
+
|
|
106
|
+
@under.poke(addr, value)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
attr_reader :ram, :screen, :expansion
|
|
111
|
+
|
|
112
|
+
# The last byte the CPU read or wrote.
|
|
113
|
+
attr_reader :data
|
|
114
|
+
|
|
115
|
+
# +model+ is a Model::Profile, +ram_kb+ the RAM below $8000 and
|
|
116
|
+
# +editor+ the editor ROM's file. The CRTC answers only on a board
|
|
117
|
+
# the model fits one on.
|
|
118
|
+
def initialize(model:, ram_kb:, editor:, pia1:, pia2:, via:, crtc:)
|
|
119
|
+
addressable_at(0, length: 2**16)
|
|
120
|
+
@ram = Memory.new([], length: ram_kb * 1024, start: 0)
|
|
121
|
+
@screen = ScreenRAM.new(model.screen_bytes)
|
|
122
|
+
@screen_pages = model.screen_pages
|
|
123
|
+
basic = ROM.read(model.basic)
|
|
124
|
+
@basic_rom = ROM.new(basic, length: basic.length, start: 0xe000 - basic.length)
|
|
125
|
+
@editor_rom = ROM.load(editor, 0xe000)
|
|
126
|
+
@kernal_rom = ROM.load(model.kernal, 0xf000)
|
|
127
|
+
@io = IO.new(self, pia1:, pia2:, via:, crtc:, crtc_fitted: model.crtc?)
|
|
128
|
+
@open_bus = OpenBus.new(self)
|
|
129
|
+
@data = 0
|
|
130
|
+
@read_pages = Array.new(256, @open_bus)
|
|
131
|
+
@write_pages = Array.new(256, @open_bus)
|
|
132
|
+
@expansion = model.expansion ? Expansion.new(self) : nil
|
|
133
|
+
@debug_register = nil
|
|
134
|
+
@io_write_handler = nil
|
|
135
|
+
map_pages
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Calls the block after each write to the I/O chips, which may have
|
|
139
|
+
# moved the lines their ports drive.
|
|
140
|
+
def on_io_write(&handler)
|
|
141
|
+
@io_write_handler = handler
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def io_written
|
|
145
|
+
@io_write_handler&.call
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The RES line reaches the 8096's control register.
|
|
149
|
+
def reset!
|
|
150
|
+
@expansion&.reset!
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def power_on!
|
|
154
|
+
@ram.clear!
|
|
155
|
+
@screen.cells.fill(0)
|
|
156
|
+
@expansion&.power_on!
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def peek(addr)
|
|
160
|
+
@data = @read_pages[addr >> 8].peek(addr)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def poke(addr, value)
|
|
164
|
+
@data = value
|
|
165
|
+
@write_pages[addr >> 8].poke(addr, value)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Whether the CPU reads the KERNAL ROM at $F000-$FFFF, which an 8096's
|
|
169
|
+
# board covers while its control register turns it on.
|
|
170
|
+
def kernal_mapped? = @expansion.nil? || @expansion.control.nobits?(0x80)
|
|
171
|
+
|
|
172
|
+
# Calls the block with each byte written to $8BFF (DebugRegister).
|
|
173
|
+
def install_debug_register(&)
|
|
174
|
+
@debug_register = DebugRegister.new(&)
|
|
175
|
+
map_pages
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Lays the pages out again, for the 8096's control register.
|
|
179
|
+
def map_pages
|
|
180
|
+
map(@open_bus, 0x00, 0x100)
|
|
181
|
+
map(@ram, 0x00, @ram.length >> 8)
|
|
182
|
+
map(@screen, 0x80, @screen_pages)
|
|
183
|
+
@read_pages.fill(@basic_rom, @basic_rom.start >> 8, @basic_rom.length >> 8)
|
|
184
|
+
@read_pages.fill(@editor_rom, 0xe0, 0x08)
|
|
185
|
+
map(@io, 0xe8, 0x01)
|
|
186
|
+
@read_pages.fill(@kernal_rom, 0xf0, 0x10)
|
|
187
|
+
@expansion&.map(@read_pages, @write_pages)
|
|
188
|
+
map_debug_register if @debug_register
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def map_debug_register
|
|
194
|
+
@debug_register.under = @write_pages[0x8b]
|
|
195
|
+
@write_pages[0x8b] = @debug_register
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def map(chip, first, count)
|
|
199
|
+
@read_pages.fill(chip, first, count)
|
|
200
|
+
@write_pages.fill(chip, first, count)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|