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
@@ -2,29 +2,17 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # PC trap on the KERNAL serial LOAD routine ($F4A5, the default ILOAD
6
- # vector target). Reads device 8 requests from the virtual drive's
7
- # channel 0, then hands over to the ROM's own tail so it prints
8
- # SEARCHING FOR and LOADING (or VERIFYING) in direct mode, reports
9
- # errors and returns with the routine's register/zeropage contract;
10
- # other devices fall through to the ROM. So does a load that reaches
5
+ # PC trap on the KERNAL serial LOAD routine (the default ILOAD vector
6
+ # target, $F4A5 on the C64, $F549 on the VIC-20). Reads device 8
7
+ # requests from the virtual drive's channel 0, then hands over to the
8
+ # ROM's own tail so it prints SEARCHING FOR and LOADING (or VERIFYING)
9
+ # in direct mode, reports errors and returns with the routine's
10
+ # register/zeropage contract; other devices fall through to the ROM.
11
+ # So does a load that reaches
11
12
  # below $0334, into the zero page, the stack or the KERNAL vectors: the
12
13
  # ROM's byte loop then loads it through the serial traps, and a loader
13
14
  # that overwrites ISTOP takes over mid-load as it does on a real drive.
14
15
  class Load < File
15
- ADDRESS = 0xf4a5
16
-
17
- # ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
18
- # the byte loop that retries a timed-out byte until RUN/STOP, the
19
- # successful return (CLC, LDX $AE, LDY $AF, RTS) and the
20
- # FILE NOT FOUND and MISSING FILE NAME error exits
21
- SEARCHING_MESSAGE = 0xf5af
22
- LOADING_MESSAGE = 0xf5d2
23
- BYTE_LOOP = 0xf4f3
24
- LOAD_DONE = 0xf5a9
25
- FILE_NOT_FOUND_EXIT = 0xf704
26
- MISSING_FILE_NAME_EXIT = 0xf710
27
-
28
16
  # ST bits at $90
29
17
  EOI = 0x40
30
18
  VERIFY_MISMATCH = 0x10
@@ -35,8 +23,8 @@ module Badline
35
23
  # address even when the first one relocated.
36
24
  LOAD_SECONDARY = 0x60
37
25
 
38
- def initialize(cpu:, bus:, drive:)
39
- super(cpu:, bus:)
26
+ def initialize(cpu:, bus:, layout:, drive:)
27
+ super(cpu:, bus:, layout:)
40
28
  @drive = drive
41
29
  end
42
30
 
@@ -46,7 +34,7 @@ module Badline
46
34
  @bus.poke(0x93, @cpu.a)
47
35
  @bus.poke(0x90, 0x00)
48
36
  name = filename
49
- return @cpu.program_counter = MISSING_FILE_NAME_EXIT if Storage.parse_name(name).first.empty?
37
+ return @cpu.program_counter = @layout.missing_file_name_exit if Storage.parse_name(name).first.empty?
50
38
 
51
39
  data, complete = receive(name)
52
40
  if low_memory?(data)
@@ -80,22 +68,23 @@ module Badline
80
68
  if data.empty?
81
69
  @bus.poke(0x90, EOI | READ_TIMEOUT)
82
70
  leave_bus(ACPTR_TIMEOUT)
83
- continue_with(SEARCHING_MESSAGE, FILE_NOT_FOUND_EXIT)
71
+ continue_with(@layout.searching_message, @layout.file_not_found_exit)
84
72
  else
85
73
  deliver(data)
86
74
  @bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
87
75
  leave_bus(ISOUR_TIMEOUT)
88
- continue_with(SEARCHING_MESSAGE, LOADING_MESSAGE, complete ? LOAD_DONE : BYTE_LOOP)
76
+ continue_with(@layout.searching_message, @layout.loading_message,
77
+ complete ? @layout.load_done : @layout.load_byte_loop)
89
78
  end
90
79
  @bus.poke(0xb9, LOAD_SECONDARY)
91
80
  end
92
81
 
93
82
  # A load ends with the UNLISTEN that closes the file, a byte sent,
94
83
  # and a missing file with the ACPTR that timed out. Either way it
95
- # releases ATN, the clock and the data line on CIA 2's port A.
84
+ # releases ATN, the clock and the data line.
96
85
  def leave_bus(timer_high)
97
86
  time_serial_byte(timer_high)
98
- release_serial_lines
87
+ @layout.release_serial_lines(@bus)
99
88
  end
100
89
 
101
90
  def low_memory?(data)
@@ -112,7 +101,7 @@ module Badline
112
101
  payload = data[2..] || []
113
102
  payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
114
103
  if load?
115
- @bus.ram.write(addr, payload)
104
+ @layout.store_file(@bus, addr, payload)
116
105
  @bus.poke(0x90, EOI)
117
106
  else
118
107
  verify(addr, payload)
@@ -3,50 +3,34 @@
3
3
  module Badline
4
4
  module KernalTrap
5
5
  # Base class for the PC traps on KERNAL routines. Holds the device
6
- # number they answer for and the return that stands in for the trapped
7
- # routine's RTS.
6
+ # number they answer for, the layout of the KERNAL they trap, and the
7
+ # return that stands in for the trapped routine's RTS.
8
8
  class Routine
9
9
  include IntegerHelper
10
10
 
11
11
  DEVICE = 8
12
12
 
13
- # Timer B's latch high byte as ISOUR and ACPTR set it, and the
14
- # control value both start it with: force load, one-shot, start.
13
+ # The serial timer's high byte as ISOUR and ACPTR set it.
15
14
  ISOUR_TIMEOUT = 0x04
16
15
  ACPTR_TIMEOUT = 0x01
17
- TIMER_ONE_SHOT_START = 0x19
18
16
 
19
- def initialize(cpu:, bus:)
17
+ def initialize(cpu:, bus:, layout:)
20
18
  @cpu = cpu
21
19
  @bus = bus
20
+ @layout = layout
22
21
  end
23
22
 
24
23
  private
25
24
 
26
- def kernal?
27
- @bus.io_port.kernal?
25
+ def kernal_mapped?
26
+ @layout.kernal_mapped?(@bus)
28
27
  end
29
28
 
30
- # The ROM times each byte on the serial bus with CIA 1's timer B,
31
- # started one-shot with the high byte of its latch set: ISOUR_TIMEOUT
32
- # for a byte sent, ACPTR_TIMEOUT for one received.
29
+ # The ROM times each byte on the serial bus with a one-shot timer
30
+ # (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
31
+ # ACPTR_TIMEOUT for one received.
33
32
  def time_serial_byte(timer_high)
34
- @bus.poke(0xdc07, timer_high)
35
- @bus.poke(0xdc0f, TIMER_ONE_SHOT_START)
36
- end
37
-
38
- # The ROM's release of the serial bus ($EE03): ATN ($EDBE), then the
39
- # clock ($EE85), then the data line ($EE97), each a read of CIA 2's
40
- # port A, a mask and a write back, so the input bits written back
41
- # follow the lines as each one is let go. Returns the last value
42
- # written, which the ROM leaves in A.
43
- def release_serial_lines
44
- value = 0
45
- [0xf7, 0xef, 0xdf].each do |mask|
46
- value = @bus.peek(0xdd00) & mask
47
- @bus.poke(0xdd00, value)
48
- end
49
- value
33
+ @layout.time_serial_byte(@bus, timer_high)
50
34
  end
51
35
 
52
36
  def return_to_caller
@@ -2,37 +2,27 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # PC trap on the KERNAL serial SAVE routine ($F5ED, the default ISAVE
6
- # vector target). Hands device 8 saves to the virtual drive as a PRG
7
- # (load address followed by the memory range); other devices fall
8
- # through to the ROM, and so do saves to a disk that doesn't take them
9
- # whole. The ROM prints SAVING in direct mode and returns
10
- # into the trap, which then writes the file and leaves through the
11
- # ROM's own tail with the registers its UNLISTEN and return leave. A
5
+ # PC trap on the KERNAL serial SAVE routine (the default ISAVE vector
6
+ # target, $F5ED on the C64, $F685 on the VIC-20). Hands device 8 saves
7
+ # to the virtual drive as a PRG (load address followed by the memory
8
+ # range); other devices fall through to the ROM, and so do saves to a
9
+ # disk that doesn't take them whole. The ROM prints SAVING in direct
10
+ # mode and returns into the trap, which then writes the file and leaves
11
+ # through the ROM's own tail with the registers its UNLISTEN and return
12
+ # leave. A
12
13
  # write that fails, to a full disk, a name already on it or a host file
13
14
  # that can't be written, ends the way a 1541 ends a SAVE it can't
14
15
  # write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
15
16
  # and the ROM returns without an error. A "@" before the drive prefix
16
17
  # writes over a file of the same name.
17
18
  class Save < File
18
- ADDRESS = 0xf5ed
19
-
20
- # ROM entry points: the SAVING message, the two line releases that
21
- # end UNLISTEN (clock, then data, leaving A as read from $DD00), the
22
- # successful return (CLC, RTS) and the MISSING FILE NAME error exit
23
- SAVING_MESSAGE = 0xf68f
24
- CLOCK_RELEASE = 0xee85
25
- DATA_RELEASE = 0xee97
26
- SAVE_DONE = 0xf657
27
- MISSING_FILE_NAME_EXIT = 0xf710
28
-
29
19
  SECONDARY = 0x61
30
20
 
31
21
  # ST bit at $90
32
22
  DEVICE_NOT_PRESENT = 0x80
33
23
 
34
- def initialize(cpu:, bus:, drive:)
35
- super(cpu:, bus:)
24
+ def initialize(cpu:, bus:, layout:, drive:)
25
+ super(cpu:, bus:, layout:)
36
26
  @drive = drive
37
27
  @saving = false
38
28
  end
@@ -51,11 +41,11 @@ module Badline
51
41
  return unless @drive.saves?
52
42
 
53
43
  @bus.poke(0xb9, SECONDARY)
54
- return @cpu.program_counter = MISSING_FILE_NAME_EXIT if name.empty?
44
+ return @cpu.program_counter = @layout.missing_file_name_exit if name.empty?
55
45
 
56
46
  @bus.poke(0x90, 0x00)
57
47
  @saving = true
58
- continue_with(SAVING_MESSAGE, ADDRESS)
48
+ continue_with(@layout.saving_message, @layout.save)
59
49
  end
60
50
 
61
51
  private
@@ -72,7 +62,7 @@ module Badline
72
62
  @bus.poke(0xad, @bus.peek(0xaf))
73
63
  @cpu.y = 0
74
64
  @cpu.status.overflow = true
75
- continue_with(CLOCK_RELEASE, DATA_RELEASE, SAVE_DONE)
65
+ continue_with(@layout.clock_release, @layout.data_release, @layout.save_done)
76
66
  end
77
67
 
78
68
  # Start address at $C1/$C2 (STAL), end address (exclusive) at
@@ -12,17 +12,6 @@ module Badline
12
12
  class Serial < Routine
13
13
  attr_accessor :device
14
14
 
15
- ROUTINES = {
16
- 0xed09 => :talk,
17
- 0xed0c => :listen,
18
- 0xedb9 => :second,
19
- 0xedc7 => :tksa,
20
- 0xeddd => :ciout,
21
- 0xedef => :untalk,
22
- 0xedfe => :unlisten,
23
- 0xee13 => :acptr
24
- }.freeze
25
-
26
15
  # Frame type in the high nibble of the secondary address
27
16
  OPEN = 0xf0
28
17
  CLOSE = 0xe0
@@ -34,8 +23,8 @@ module Badline
34
23
 
35
24
  NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
36
25
 
37
- def initialize(cpu:, bus:, drive:, device: DEVICE)
38
- super(cpu:, bus:)
26
+ def initialize(cpu:, bus:, layout:, drive:, device: DEVICE)
27
+ super(cpu:, bus:, layout:)
39
28
  @drive = drive
40
29
  @device = device
41
30
  @listening = false
@@ -63,14 +52,27 @@ module Badline
63
52
  end
64
53
 
65
54
  def install
66
- ROUTINES.each do |address, routine|
67
- @cpu.install_trap(address) { call_routine(routine) if kernal? }
55
+ routines.each do |address, routine|
56
+ @cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
68
57
  end
69
58
  self
70
59
  end
71
60
 
72
61
  private
73
62
 
63
+ def routines
64
+ {
65
+ @layout.talk => :talk,
66
+ @layout.listen => :listen,
67
+ @layout.second => :second,
68
+ @layout.tksa => :tksa,
69
+ @layout.ciout => :ciout,
70
+ @layout.untalk => :untalk,
71
+ @layout.unlisten => :unlisten,
72
+ @layout.acptr => :acptr
73
+ }
74
+ end
75
+
74
76
  def call_routine(routine)
75
77
  case routine
76
78
  when :talk then talk
@@ -86,7 +88,7 @@ module Badline
86
88
 
87
89
  # Addressing the bus starts a new frame. The channel number arrives
88
90
  # 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
91
+ # and each byte of a frame, times its handshake with the serial timer
90
92
  # as ISOUR does.
91
93
  def talk
92
94
  @talk_channel = nil
@@ -157,10 +159,10 @@ module Badline
157
159
  release_bus
158
160
  end
159
161
 
160
- # UNLSN and UNTLK end by releasing ATN, the clock and the data line on
161
- # CIA 2's port A, and leave the port's last read in the accumulator.
162
+ # UNLSN and UNTLK end by releasing ATN, the clock and the data line,
163
+ # and leave the port's last read in the accumulator.
162
164
  def release_bus
163
- @cpu.a = release_serial_lines
165
+ @cpu.a = @layout.release_serial_lines(@bus)
164
166
  @cpu.status.negative = @cpu.a.anybits?(0x80)
165
167
  @cpu.status.zero = @cpu.a.zero?
166
168
  return_to_caller
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/kernal_trap/layout"
3
4
  require "badline/kernal_trap/routine"
4
5
  require "badline/kernal_trap/channel"
5
6
  require "badline/kernal_trap/drive"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- # The 8x8 key matrix spanning CIA 1's two ports: rows on port A, columns on
5
- # port B.
4
+ # The key matrix spanning CIA 1's two ports: rows on port A, columns on
5
+ # port B. Row lines beyond the eighth sit above port A's bits in +rows+.
6
6
  #
7
7
  # A pressed key shorts its row line to its column line, so whichever side is
8
8
  # pulled low drags the other down with it. Scanning works in either
@@ -19,22 +19,25 @@ module Badline
19
19
  cursor_left: %i[lshift cursor_h]
20
20
  }.freeze
21
21
 
22
+ # The C64's wiring: one row of keys per port A line, in port B column
23
+ # order.
24
+ C64_MATRIX = [
25
+ %i[delete return cursor_h f7 f1 f3 f5 cursor_v],
26
+ %i[3 w a 4 z s e lshift],
27
+ %i[5 r d 6 c f t x],
28
+ %i[7 y g 8 b h u v],
29
+ %i[9 i j 0 m k o n],
30
+ %i[+ p l - . : @ ,],
31
+ %i[£ * ; clr_home rshift = up /],
32
+ %i[1 left control 2 space cbm q run_stop]
33
+ ].freeze
34
+
22
35
  attr_reader :keys, :matrix
23
36
 
24
- def initialize
37
+ def initialize(matrix: C64_MATRIX)
25
38
  @keys = []
26
39
  @row_masks = nil
27
-
28
- @matrix = [
29
- %i[delete return cursor_h f7 f1 f3 f5 cursor_v],
30
- %i[3 w a 4 z s e lshift],
31
- %i[5 r d 6 c f t x],
32
- %i[7 y g 8 b h u v],
33
- %i[9 i j 0 m k o n],
34
- %i[+ p l - . : @ ,],
35
- %i[£ * ; clr_home rshift = up /],
36
- %i[1 left control 2 space cbm q run_stop]
37
- ]
40
+ @matrix = matrix
38
41
  end
39
42
 
40
43
  def press(key)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # = Machine
5
+ #
6
+ # The machines badline builds by family, as the command line's
7
+ # subcommand names them: the C64, Computer, and the VIC-20, Vic20.
8
+ #
9
+ # A machine answers what the front end, Media, the runners and the
10
+ # snapshots call on it:
11
+ #
12
+ # - Identity: +family+, the family it was built as.
13
+ # - Clocking: +run_cycles(n)+, +run_until(limit) { ... }+, +cycles+,
14
+ # +on_init+ and +init_threshold+, the cycle its handlers run at.
15
+ # - Lifecycle: +reset!+ and +power_cycle!+.
16
+ # - Video: +video+, the chip the screen reads (+display+, +width+,
17
+ # +dirty_lines+, +clear_dirty_lines!+ and +palette+), and +timing+, a
18
+ # Timing.
19
+ # - Sound: +sound_source+, which answers +record(rate:)+,
20
+ # +drain_samples+ and +model+.
21
+ # - Input: +keyboard+, +joystick1+, +joystick2+, +control_ports+,
22
+ # +press_restore+ and +release_restore+.
23
+ # - Media: +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
24
+ # +mounted?+, +datasette+, +attach_cartridge+,
25
+ # +press_cartridge_button+, +release_cartridge_button+,
26
+ # +attach_drive1541+, +drive1541+, and +address_bus+ for its
27
+ # +cartridge+ and +detach_cartridge+.
28
+ # - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
29
+ # +install_debug_register+ and +capture_output+.
30
+ #
31
+ # The VIC-20 answers the identity, clocking, lifecycle, video, sound and
32
+ # input parts, with its one joystick as both +joystick1+ and +joystick2+
33
+ # and nil for +control_ports+, and of the rest +ram+, +load_prg+,
34
+ # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
35
+ # +attach_drive1541+, +drive1541+, +install_debug_register+ and
36
+ # +capture_output+.
37
+ module Machine
38
+ # A new machine of `family`, :c64 or :vic20, the model of it `model`
39
+ # names. A C64's model is one of Model::ALL, with its SID `sid_model`
40
+ # when not nil and an REU of `reu` K when not nil. A VIC-20's is "pal",
41
+ # with the RAM expansion `ram` names (Vic20::Bus::RAM_CONFIGURATIONS),
42
+ # unexpanded when nil.
43
+ def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
44
+ return vic20(model, ram) if family == :vic20
45
+ raise ArgumentError, "no machine family named #{family}" unless family == :c64
46
+
47
+ profile = Model.named(model)
48
+ Computer.new(vic_model: profile.vic_model, cia_model: profile.cia_model,
49
+ sid_model: sid_model || profile.sid_model, region: profile.region, reu:,
50
+ kernal: profile.kernal, datasette: profile.datasette)
51
+ end
52
+
53
+ def self.vic20(model, ram)
54
+ raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
55
+
56
+ Vic20.new(ram: ram || :unexpanded)
57
+ end
58
+ private_class_method :vic20
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A program loading into a VIC-20 once it has booted (Vic20Media).
6
+ module Vic20Basic
7
+ # Where BASIC starts with each RAM expansion it can start in, as
8
+ # TXTTAB holds it.
9
+ BASIC_STARTS = { 0x1001 => :unexpanded, 0x0401 => :"3k", 0x1201 => :"8k" }.freeze
10
+
11
+ class << self
12
+ # A BASIC program, one that loads at a start of BASIC or a byte
13
+ # ahead of it, goes to this machine's start of BASIC. Anything else
14
+ # goes where it says, and doesn't RUN.
15
+ def load_program(machine, data, autostart:)
16
+ load_addr = data[0] | (data[1] << 8)
17
+ basic_start = machine.basic_start
18
+ ahead = [0, 1].find { |offset| BASIC_STARTS.key?(load_addr + offset) || load_addr + offset == basic_start }
19
+ return machine.load_prg(data) unless ahead
20
+
21
+ load_basic(machine, data, basic_start - ahead, load_addr)
22
+ machine.type_text("run\r") if autostart
23
+ end
24
+
25
+ private
26
+
27
+ # Puts a BASIC program at `address`, relinking its lines when that
28
+ # isn't where it was saved from, as BASIC's LOAD does, and sets the
29
+ # end of the program, where BASIC's variables start, and the end
30
+ # address the KERNAL's LOAD leaves.
31
+ def load_basic(machine, data, address, saved_at)
32
+ ram = machine.ram
33
+ ram.write(address, data[2..])
34
+ end_addr = address + data.length - 2
35
+ relink(ram, machine.basic_start, end_addr) unless address == saved_at
36
+ [0x2d, 0xae].each { |pointer| ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
37
+ end
38
+
39
+ # Points each line's link at the line after it, as BASIC's LINKPRG
40
+ # does: past the zero that ends the line's text. A link with a zero
41
+ # high byte ends the program.
42
+ def relink(ram, line, end_addr)
43
+ while line + 4 < end_addr && ram.peek(line + 1).positive?
44
+ following = line + 4
45
+ following += 1 while following < end_addr && ram.peek(following).positive?
46
+ following += 1
47
+ ram.write(line, [following & 0xff, following >> 8])
48
+ line = following
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A VIC-20 cartridge kept as programs, one per ROM chip, each loading
6
+ # at the block the chip goes in. The chip in BLK5 at $A000 starts the
7
+ # cartridge, and a bigger cartridge has more in BLK1 to BLK3. The files
8
+ # of one cartridge share a name, with each block's address in its
9
+ # extension (game.a0, game.60) or its name (game-a000.prg,
10
+ # game-6000.prg).
11
+ module Vic20Cartridge
12
+ BLOCKS = [0xa000, 0x2000, 0x4000, 0x6000].freeze
13
+
14
+ class << self
15
+ # The ROM chips, as CRTFile::Chips, of the cartridge `data`, the
16
+ # program at `path`, is a part of, or nil when it isn't one: a
17
+ # program loading at $A000, or one loading at the start of BLK1 to
18
+ # BLK3 with an $A000 part beside it.
19
+ def chips(path, data)
20
+ own = load_address(data)
21
+ return unless BLOCKS.include?(own)
22
+
23
+ parts = BLOCKS.filter_map { |address| address == own ? data : part(path, own, address) }
24
+ return unless parts.any? { |part| load_address(part) == 0xa000 }
25
+
26
+ parts.map { |part| rom_chip(part) }
27
+ end
28
+
29
+ # A program's bytes, unwrapped from a .p00.
30
+ def program_bytes(path)
31
+ bytes = File.binread(path).bytes
32
+ Storage::P00.wraps?(bytes) ? Storage::P00.data(bytes) : bytes
33
+ end
34
+
35
+ private
36
+
37
+ def load_address(data)
38
+ data[0] | (data[1] << 8) if data.length > 2
39
+ end
40
+
41
+ def part(path, own, address)
42
+ name = part_names(path, own, address).find { |candidate| File.file?(candidate) }
43
+ return unless name
44
+
45
+ part = program_bytes(name)
46
+ part if load_address(part) == address
47
+ end
48
+
49
+ # `path` with the block at `own` in its extension or its name
50
+ # swapped for the one at `address`, its letters in either case.
51
+ def part_names(path, own, address)
52
+ extension = File.extname(path)
53
+ base = path[0, path.length - extension.length]
54
+ own_tag = own.to_s(16)
55
+ tags = [address.to_s(16), address.to_s(16).upcase].uniq
56
+ return tags.map { |tag| "#{base}.#{tag[0, 2]}" } if extension.downcase == ".#{own_tag[0, 2]}"
57
+
58
+ name = File.basename(base)
59
+ at = name.downcase.index(own_tag)
60
+ return [] unless at
61
+
62
+ tags.map { |tag| File.join(File.dirname(base), name[0, at] + tag + name[(at + 4)..]) + extension }
63
+ end
64
+
65
+ # A ROM chip of whole pages, the last padded with $FF.
66
+ def rom_chip(part)
67
+ data = part[2..]
68
+ data += [0xff] * (-data.length % 0x100)
69
+ Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address: load_address(part), data:)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end