badline 0.11.1 → 0.12.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/c128/bus.rb +4 -17
  10. data/lib/badline/c128/drives.rb +35 -22
  11. data/lib/badline/c128/fast_serial.rb +1 -0
  12. data/lib/badline/c128/io_pages.rb +35 -0
  13. data/lib/badline/c128/mmu.rb +9 -7
  14. data/lib/badline/c128/modes.rb +17 -4
  15. data/lib/badline/c128/saved_state.rb +22 -0
  16. data/lib/badline/c128/z80.rb +49 -0
  17. data/lib/badline/c128/z80_pages.rb +118 -0
  18. data/lib/badline/c128/z80_turns.rb +106 -0
  19. data/lib/badline/c128.rb +16 -19
  20. data/lib/badline/cartridge/action_replay.rb +2 -2
  21. data/lib/badline/cartridge/comal80.rb +1 -7
  22. data/lib/badline/cartridge/dinamic.rb +0 -8
  23. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  24. data/lib/badline/cartridge/fun_play.rb +0 -8
  25. data/lib/badline/cartridge/game_system.rb +0 -8
  26. data/lib/badline/cartridge/kcs_power.rb +10 -7
  27. data/lib/badline/cartridge/mach5.rb +2 -4
  28. data/lib/badline/cartridge/magic_desk.rb +0 -8
  29. data/lib/badline/cartridge/ocean.rb +0 -8
  30. data/lib/badline/cartridge/pagefox.rb +2 -2
  31. data/lib/badline/cartridge/retro_replay.rb +7 -6
  32. data/lib/badline/cartridge/rex_utility.rb +2 -6
  33. data/lib/badline/cartridge/rgcd.rb +2 -2
  34. data/lib/badline/cartridge/simons_basic.rb +0 -5
  35. data/lib/badline/cartridge/super_games.rb +4 -2
  36. data/lib/badline/cartridge/westermann.rb +2 -6
  37. data/lib/badline/cartridge.rb +37 -9
  38. data/lib/badline/cia/fast_forward.rb +21 -3
  39. data/lib/badline/cia.rb +8 -5
  40. data/lib/badline/computer/attachments.rb +4 -56
  41. data/lib/badline/computer/true_drives.rb +80 -0
  42. data/lib/badline/computer.rb +4 -0
  43. data/lib/badline/core.rb +2 -0
  44. data/lib/badline/drive/core.rb +8 -33
  45. data/lib/badline/drive/fast_serial.rb +73 -0
  46. data/lib/badline/drive/idle.rb +1 -11
  47. data/lib/badline/drive/orbit.rb +2 -5
  48. data/lib/badline/drive/sleep.rb +0 -2
  49. data/lib/badline/drive/vias.rb +84 -0
  50. data/lib/badline/drive.rb +2 -0
  51. data/lib/badline/drive1541.rb +1 -0
  52. data/lib/badline/drive1571/wd1770.rb +94 -8
  53. data/lib/badline/drive1571.rb +8 -58
  54. data/lib/badline/drive1581/bus.rb +137 -0
  55. data/lib/badline/drive1581/cpu.rb +11 -0
  56. data/lib/badline/drive1581/disk.rb +135 -0
  57. data/lib/badline/drive1581/disk_state.rb +87 -0
  58. data/lib/badline/drive1581/mechanism.rb +117 -0
  59. data/lib/badline/drive1581/serial_port.rb +52 -0
  60. data/lib/badline/drive1581/slot.rb +78 -0
  61. data/lib/badline/drive1581/track.rb +100 -0
  62. data/lib/badline/drive1581/track_bytes.rb +75 -0
  63. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  64. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  65. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  66. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  67. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  68. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  69. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  70. data/lib/badline/drive1581/wd1772.rb +216 -0
  71. data/lib/badline/drive1581.rb +233 -0
  72. data/lib/badline/frontend/app.rb +1 -3
  73. data/lib/badline/frontend/audio_device.rb +67 -0
  74. data/lib/badline/frontend/audio_sink.rb +16 -50
  75. data/lib/badline/frontend/boot.rb +18 -10
  76. data/lib/badline/frontend/buttons.rb +7 -7
  77. data/lib/badline/frontend/keys.rb +33 -8
  78. data/lib/badline/frontend/media_slots.rb +42 -0
  79. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  80. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  81. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  82. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  83. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  84. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  85. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  86. data/lib/badline/frontend/menu/theme.rb +18 -0
  87. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  88. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  89. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  90. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  91. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  92. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  93. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  94. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  95. data/lib/badline/frontend/player/theme.rb +18 -0
  96. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  97. data/lib/badline/frontend/sound.rb +19 -38
  98. data/lib/badline/frontend/timeline.rb +3 -14
  99. data/lib/badline/frontend.rb +24 -20
  100. data/lib/badline/iec_bus.rb +14 -2
  101. data/lib/badline/machine.rb +5 -4
  102. data/lib/badline/media/boot_disk.rb +50 -0
  103. data/lib/badline/media/not_disk.rb +8 -0
  104. data/lib/badline/media/true_drive.rb +35 -15
  105. data/lib/badline/media.rb +9 -3
  106. data/lib/badline/options/event.rb +2 -2
  107. data/lib/badline/options/family.rb +3 -9
  108. data/lib/badline/options/help.rb +2 -1
  109. data/lib/badline/roms/README +9 -0
  110. data/lib/badline/roms/dos1581.rom +0 -0
  111. data/lib/badline/sdl.rb +37 -20
  112. data/lib/badline/snapshot/state.rb +5 -4
  113. data/lib/badline/storage/p00.rb +8 -0
  114. data/lib/badline/time_of_day.rb +3 -3
  115. data/lib/badline/version.rb +1 -1
  116. data/lib/badline/vic20/attachments.rb +21 -7
  117. data/lib/badline/vic20/saved_state.rb +2 -0
  118. data/lib/badline/vic20.rb +3 -0
  119. data/lib/badline/z80/core.rb +2 -0
  120. data/lib/badline/z80/saved_state.rb +69 -0
  121. metadata +54 -21
  122. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  123. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  124. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  125. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A C128 boot disk. At power-on the C128 KERNAL reads track 1, sector 0
6
+ # of the disk in device 8 and runs it when it starts with "CBM", as boot
7
+ # games have it. The 1581's DOS first loads and runs a USR file named
8
+ # COPYRIGHT CBM 86 when the disk has one, which is how CP/M's 1581 disks
9
+ # hand the KERNAL their boot sector, so a .d81 with that file boots too.
10
+ #
11
+ # On a C128 in C128 mode a boot disk goes in a true drive, a 1581 for a
12
+ # .d81 and the C128D's 1571 otherwise, and nothing is typed, so the
13
+ # KERNAL boots it. Other disks, and other machines, keep the traps and
14
+ # the autostart.
15
+ module BootDisk
16
+ SIGNATURE = "CBM".bytes.freeze
17
+ AUTO_BOOT = "copyright cbm 86"
18
+ TYPES = %w[.d64 .d71 .d81].freeze
19
+
20
+ class << self
21
+ def takes?(computer, path)
22
+ computer.family == :c128 && computer.mode == :c128 && boot?(path)
23
+ end
24
+
25
+ # Puts a disk that boots on the computer in a true drive as device
26
+ # 8, write-protected with `disk: { read_only: true }`, for the KERNAL
27
+ # to boot at power-on, and says so. Nil for any other disk.
28
+ def attach(computer, path, disk: {}, **)
29
+ return unless takes?(computer, path)
30
+
31
+ read_only = disk.fetch(:read_only, false)
32
+ "#{TrueDrive.insert(computer, path, read_only:)}, to boot at power-on"
33
+ end
34
+
35
+ # Whether the disk image at path boots: its boot sector, or on a
36
+ # .d81 the 1581's auto-boot file.
37
+ def boot?(path)
38
+ type = File.extname(path).downcase
39
+ return false unless TYPES.include?(type) && File.file?(path)
40
+
41
+ image = MOUNT_TYPES.fetch(type).new(path, read_only: true)
42
+ block = image.read_block(1, 0)
43
+ return true if block && block[0, 3] == SIGNATURE
44
+
45
+ type == ".d81" && !image.read_file(AUTO_BOOT, type: :usr).nil?
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A file that goes in device 8 but isn't a disk image or a directory.
6
+ class NotDisk < ArgumentError; end
7
+ end
8
+ end
@@ -3,13 +3,15 @@
3
3
  module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
- # the C128 its 1571. Once a true drive is plugged in as device 8, disk
7
- # media go into it rather than into the traps, so a LOAD runs the DOS
8
- # and every byte crosses the serial bus. A .g64 holds the disk's raw
9
- # GCR, which only a true drive reads, so it plugs one in when there's
10
- # none yet. The 1541 reads only .d64 and .g64 images, and the 1571
11
- # .d71 and .g71 as well, so the traps' other disk media, a .d81 or
12
- # .t64 or a host directory, raise Error.
6
+ # the C128 its 1571, or a 1581. Once a true drive is plugged in as
7
+ # device 8, disk media go into it rather than into the traps, so a LOAD
8
+ # runs the DOS and every byte crosses the serial bus. A .g64 holds the
9
+ # disk's raw GCR, which only a true drive reads, so it plugs one in
10
+ # when there's none yet. The 1541 reads only .d64 and .g64 images, the
11
+ # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
12
+ # the drive on device 8 for a 1581 (Drive1581::Slot), and another disk
13
+ # image swaps a 1581 back for the machine's own drive. The traps' other
14
+ # disk media, a .t64 or a host directory, raise Error.
13
15
  module TrueDrive
14
16
  class Error < ArgumentError; end
15
17
 
@@ -35,22 +37,40 @@ module Badline
35
37
  !drive(computer).nil? && (File.directory?(path) || MOUNT_TYPES.key?(File.extname(path).downcase))
36
38
  end
37
39
 
38
- # Puts the image in the drive on device 8, plugging one in when
39
- # there's none, and takes out what was mounted through the traps,
40
- # so LOAD and SAVE reach the drive too. `read_only` puts it in
41
- # write-protected.
40
+ # Puts the image in the drive on device 8, plugging one in that
41
+ # reads it when the drive there doesn't, and takes out what was
42
+ # mounted through the traps, so LOAD and SAVE reach the drive too.
43
+ # `read_only` puts it in write-protected.
42
44
  def insert(computer, path, read_only: false)
43
- name = drive(computer)&.model_name || (computer.family == :c128 ? "1571" : "1541")
45
+ return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
46
+
47
+ drive = own_drive(computer)
48
+ name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
44
49
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
45
50
  unless types.include?(File.extname(path).downcase)
46
- raise Error, "#{path} is not a #{types[0..-2].join(', ')} or #{types.last} image, which the #{name} reads"
51
+ raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
47
52
  end
48
53
 
49
- disk = Drive1541::Disk.open(path, read_only:)
54
+ (drive || computer.plug_true_drive).insert_image(path, read_only:)
50
55
  computer.unmount
51
- plug(computer).insert(disk)
52
56
  "Inserted #{path} in the #{name} as device 8"
53
57
  end
58
+
59
+ private
60
+
61
+ # The drive on device 8 unless it's a 1581.
62
+ def own_drive(computer)
63
+ drive = drive(computer)
64
+ drive unless drive&.model_name == "1581"
65
+ end
66
+
67
+ def insert_d81(computer, path, read_only)
68
+ drive = drive(computer)
69
+ drive = computer.plug_drive1581 unless drive&.model_name == "1581"
70
+ drive.insert_image(path, read_only:)
71
+ computer.unmount
72
+ "Inserted #{path} in the 1581 as device 8"
73
+ end
54
74
  end
55
75
  end
56
76
  end
data/lib/badline/media.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/media/true_drive"
4
+ require "badline/media/boot_disk"
5
+ require "badline/media/not_disk"
4
6
  require "badline/media/queue"
5
7
  require "badline/media/disk_set"
6
8
  require "badline/media/disk_list"
@@ -32,6 +34,9 @@ module Badline
32
34
  # there, a .d64 goes into it instead of the KERNAL traps too. The
33
35
  # autostart then loads through it.
34
36
  #
37
+ # On a C128 in C128 mode, a boot disk goes in a true drive for the
38
+ # KERNAL to boot at power-on, with nothing typed (BootDisk).
39
+ #
35
40
  # An .m3u or .vfl list of disks attaches the first disk it lists
36
41
  # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
37
42
  # C128 in C64 mode a C64's.
@@ -39,7 +44,7 @@ module Badline
39
44
  path = DiskList.disk(path)
40
45
  return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
41
46
 
42
- attach_c64(computer, path, autostart:, subtune:, **)
47
+ BootDisk.attach(computer, path, **) || attach_c64(computer, path, autostart:, subtune:, **)
43
48
  end
44
49
 
45
50
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -50,11 +55,12 @@ module Badline
50
55
  # A .g64 goes in a true 1541, which is plugged in as device 8 when
51
56
  # there's none yet (TrueDrive). Once a true 1541 is device 8, a .d64
52
57
  # goes in its drive as well, and other disks, which a 1541 can't
53
- # read, raise TrueDrive::Error, an ArgumentError.
58
+ # read, raise TrueDrive::Error, an ArgumentError. Anything else
59
+ # raises NotDisk.
54
60
  def insert_disk(computer, path, read_only: false)
55
61
  return TrueDrive.insert(computer, path, read_only:) if TrueDrive.takes?(computer, path)
56
62
 
57
- raise ArgumentError, "#{path} is not a disk image or a directory" unless disk?(path)
63
+ raise NotDisk, "#{path} is not a disk image or a directory" unless disk?(path)
58
64
 
59
65
  computer.mount(open_storage(path, { read_only: }))
60
66
  "Inserted #{path} in device 8"
@@ -23,7 +23,7 @@ module Badline
23
23
 
24
24
  # What insert takes: disk images, tapes and cartridges, or an .m3u
25
25
  # or .vfl list of disks or a directory to mount as device 8.
26
- INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
26
+ INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt .m3u .vfl].freeze
27
27
 
28
28
  attr_reader :frame, :action, :argument
29
29
 
@@ -95,7 +95,7 @@ module Badline
95
95
  def self.insertable?(path)
96
96
  return true if File.directory?(path)
97
97
 
98
- File.exist?(path) && (INSERTABLE.include?(File.extname(path).downcase) || Media::DiskList.list?(path))
98
+ File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase)
99
99
  end
100
100
  end
101
101
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/storage/p00"
4
+
3
5
  module Badline
4
6
  class Options
5
7
  # The machine family: the subcommand that names it, or else the media,
@@ -37,15 +39,7 @@ module Badline
37
39
  return if @family_named || @sid_command || @media_path.nil?
38
40
  return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
39
41
 
40
- @family = :c128 if load_address(@media_path) == C128_BASIC_START
41
- end
42
-
43
- # The load address of a .prg, or of the program a .p00 wraps after its
44
- # 26-byte header.
45
- def load_address(path)
46
- offset = File.extname(path).casecmp?(".p00") ? 26 : 0
47
- bytes = File.binread(path).bytes
48
- bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
42
+ @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
49
43
  end
50
44
  end
51
45
  end
@@ -61,7 +61,8 @@ module Badline
61
61
  pause menu loads. Device 8 answers through traps on the KERNAL's
62
62
  disk routines, unless --true-drive puts an emulated 1541 there,
63
63
  which runs its own DOS and reads .d64 and .g64 images only, or on
64
- the C128 the C128D's 1571, which reads .d71 and .g71 too. A .sid
64
+ the C128 the C128D's 1571, which reads .d71 and .g71 too. A .d81
65
+ then goes in an emulated 1581 in its place. A .sid
65
66
  tune opens in the SID player, as `sid` plays it, unless an option
66
67
  of the emulator's window asks for the machine.
67
68
 
@@ -70,6 +70,15 @@ Downloaded from
70
70
  https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1571/1571-rom.310654-05.bin
71
71
  SHA-1 f1be619c106641a685f6609e4d43d6fc9eac1e70.
72
72
 
73
+ dos1581.rom
74
+ -----------
75
+ The 1581 disk drive's DOS, 318045-02, 32 KB for $8000-$FFFF.
76
+ Drive1581 loads it.
77
+
78
+ Downloaded from
79
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
80
+ SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
81
+
73
82
  Commodore VIC-20 ROMs
74
83
  =====================
75
84
 
Binary file
data/lib/badline/sdl.rb CHANGED
@@ -59,23 +59,6 @@ module Badline
59
59
  ffi_func :SDL_PushEvent, [:ptr], :int
60
60
  ffi_func :SDL_GetKeyName, [:int], :str
61
61
 
62
- ffi_buffer :event, 56
63
- ffi_read_u32 :event_type, 0
64
- ffi_read_u8 :event_repeat, 13
65
- ffi_read_i32 :event_scancode, 16
66
- ffi_read_i32 :event_sym, 20
67
- ffi_read_u16 :event_mod, 24
68
- # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
69
- # button.
70
- ffi_read_i32 :event_xrel, 28
71
- ffi_read_i32 :event_yrel, 32
72
- ffi_read_u8 :event_button, 16
73
- # Where the pointer is, in a motion or button event.
74
- ffi_read_i32 :event_x, 20
75
- ffi_read_i32 :event_y, 24
76
- # SDL_DropEvent's file, which the receiver frees.
77
- ffi_read_ptr :event_file, 8
78
-
79
62
  ffi_buffer :rect, 16
80
63
  ffi_write_i32 :rect_x, 0
81
64
  ffi_write_i32 :rect_y, 4
@@ -115,19 +98,53 @@ module Badline
115
98
  ffi_const :PIXELFORMAT_ARGB8888, 0x16362004
116
99
  ffi_const :BLENDMODE_BLEND, 0x01
117
100
  ffi_const :TEXTUREACCESS_STREAMING, 1
101
+ ffi_const :AUDIO_S16LSB, 0x8010
102
+ ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
103
+ end
104
+
105
+ # The events: the SDL_Event that SDL_PollEvent fills, the fields the
106
+ # front end reads from it, the event types, the key modifier and
107
+ # keycodes, and the dropped file's path.
108
+ module SDL
109
+ ffi_buffer :event, 56
110
+ ffi_read_u32 :event_type, 0
111
+ ffi_read_u8 :event_repeat, 13
112
+ ffi_read_i32 :event_scancode, 16
113
+ ffi_read_i32 :event_sym, 20
114
+ ffi_read_u16 :event_mod, 24
115
+ # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
116
+ # button.
117
+ ffi_read_i32 :event_xrel, 28
118
+ ffi_read_i32 :event_yrel, 32
119
+ ffi_read_u8 :event_button, 16
120
+ # Where the pointer is, in a motion or button event.
121
+ ffi_read_i32 :event_x, 20
122
+ ffi_read_i32 :event_y, 24
123
+ # SDL_DropEvent's file, which the receiver frees.
124
+ ffi_read_ptr :event_file, 8
125
+
118
126
  ffi_const :QUIT, 0x100
119
127
  ffi_const :KEYDOWN, 0x300
120
128
  ffi_const :KEYUP, 0x301
121
129
  ffi_const :MOUSEMOTION, 0x400
122
130
  ffi_const :MOUSEBUTTONDOWN, 0x401
123
131
  ffi_const :MOUSEBUTTONUP, 0x402
124
- ffi_const :KMOD_SHIFT, 0x0003
132
+ ffi_const :MOUSEWHEEL, 0x403
125
133
  ffi_const :CONTROLLERDEVICEADDED, 0x653
126
134
  ffi_const :CONTROLLERDEVICEREMOVED, 0x654
127
- ffi_const :AUDIO_S16LSB, 0x8010
128
- ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
135
+ ffi_const :DROPFILE, 0x1000
136
+ ffi_const :KMOD_SHIFT, 0x0003
129
137
  ffi_const :KEY_TAB, 0x09
130
138
  ffi_const :KEY_F10, 0x4000_0043
139
+
140
+ # The path of the file the DROPFILE event in SDL.event carries, as a
141
+ # String, freeing SDL's copy.
142
+ def self.dropped_file
143
+ file = SDL.event_file(SDL.event)
144
+ path = LibC.strstr(file, "")
145
+ LibC.free(file)
146
+ path
147
+ end
131
148
  end
132
149
 
133
150
  # The few C library calls the native front end makes.
@@ -15,10 +15,11 @@ module Badline
15
15
  # save_state writes something else, so a State in another layout
16
16
  # fails before anything is read into a machine. Layout 3, without the
17
17
  # KERNAL and datasette in the setup, layout 4, without the VIA shift
18
- # register's clock delay, and layout 5, without the board in the
19
- # setup, still read.
20
- SCHEMA = 6
21
- READABLE = [3, 4, 5, SCHEMA].freeze
18
+ # register's clock delay, layout 5, without the board in the setup,
19
+ # layout 6, without the C128's Z80, and layout 7, without a 1581 on the
20
+ # serial bus, still read.
21
+ SCHEMA = 8
22
+ READABLE = [3, 4, 5, 6, 7, SCHEMA].freeze
22
23
 
23
24
  attr_reader :values, :strings
24
25
 
@@ -18,6 +18,14 @@ module Badline
18
18
  def data(bytes)
19
19
  bytes[HEADER_SIZE..]
20
20
  end
21
+
22
+ # The load address of the program in a .prg file, or wrapped in a
23
+ # .p00 one, or -1 when the file is too short to have one.
24
+ def load_address(path)
25
+ bytes = File.binread(path).bytes
26
+ offset = wraps?(bytes) ? HEADER_SIZE : 0
27
+ bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -52,9 +52,9 @@ module Badline
52
52
  @latch = input.boolean? ? FIELDS.to_h { |field| [field, input.int] } : nil
53
53
  end
54
54
 
55
- # Whether the clock is stopped with no alarm to raise, so its cycles
56
- # move only the divider's phase.
57
- def quiet? = @stopped && !@alarm_pending
55
+ # Whether the clock is stopped, or gets no pulses on its pin, with no
56
+ # alarm to raise, so its cycles move only the divider's phase.
57
+ def quiet? = (@stopped || @mains_hz.zero?) && !@alarm_pending
58
58
 
59
59
  # Runs +cycles+ quiet cycles at once.
60
60
  def fast_forward(cycles)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.11.1"
4
+ VERSION = "0.12.0"
5
5
  end
@@ -4,8 +4,10 @@ module Badline
4
4
  class Vic20
5
5
  # What plugs into the VIC-20 besides its chips: cartridge ROM in the
6
6
  # expansion blocks, the disk device 8 serves through the KERNAL traps,
7
- # and a true 1541 on the serial bus.
7
+ # and a true 1541 or 1581 on the serial bus.
8
8
  module Attachments
9
+ include Drive1581::Slot
10
+
9
11
  # The cycle by which a 1541 switched on with the machine has run its
10
12
  # DOS's reset, the RAM test and the ROM checksum, and waits on the
11
13
  # bus, with a margin: about 1.1 million of the VIC-20's cycles.
@@ -48,7 +50,7 @@ module Badline
48
50
 
49
51
  cpu.remove_trap(KernalTrap::VIC20_LAYOUT.load)
50
52
  cpu.remove_trap(KernalTrap::VIC20_LAYOUT.save)
51
- @serial_trap.device = nil
53
+ @serial_trap.uninstall
52
54
  @serial_trap = nil
53
55
  @save_trap = nil
54
56
  @drive = nil
@@ -75,11 +77,16 @@ module Badline
75
77
  @serial_trap&.device = serial_trap_device
76
78
  end
77
79
 
78
- # The true drive on the serial bus: the Drive1541, or nil.
79
- def true_drive = @drive1541
80
+ # The true drive on the serial bus: the Drive1581 ahead of the
81
+ # Drive1541, or nil.
82
+ def true_drive = @drive1581 || @drive1541
80
83
 
81
- # Plugs a Drive1541 in as device 8, and returns it.
82
- def plug_true_drive = Drive1541.new.tap { |drive| attach_drive1541(drive) }
84
+ # Plugs a Drive1541 in as device 8, in place of a 1581 there, and
85
+ # returns it.
86
+ def plug_true_drive
87
+ detach_drive1581 if drive1581_on_device8?
88
+ Drive1541.new.tap { |drive| attach_drive1541(drive) }
89
+ end
83
90
 
84
91
  # Unplugs the Drive1541, leaving the serial bus with nothing on it.
85
92
  def detach_drive1541
@@ -102,7 +109,14 @@ module Badline
102
109
  # The device number the serial traps answer: device 8, unless a true
103
110
  # drive is on the bus as device 8.
104
111
  def serial_trap_device
105
- @drive1541&.device == KernalTrap::Routine::DEVICE ? nil : KernalTrap::Routine::DEVICE
112
+ device = KernalTrap::Routine::DEVICE
113
+ @drive1541&.device == device || drive1581_on_device8? ? nil : device
114
+ end
115
+
116
+ # A 1581 plugged in, as a 1541 does, holds the machine's on_init
117
+ # handlers back until its DOS has booted.
118
+ def drive1581_attached
119
+ @init_threshold = [@init_threshold, Drive1581::BOOT_CYCLES].max if @cycles < @init_threshold
106
120
  end
107
121
  end
108
122
  end
@@ -78,6 +78,7 @@ module Badline
78
78
  @datasette.save_state(out)
79
79
  save_trap_drive(out)
80
80
  save_drive1541(out)
81
+ save_drive1581(out)
81
82
  end
82
83
 
83
84
  def load_state(input)
@@ -99,6 +100,7 @@ module Badline
99
100
  @datasette.load_state(input)
100
101
  load_trap_drive(input)
101
102
  load_drive1541(input)
103
+ load_drive1581(input)
102
104
  end
103
105
 
104
106
  private
data/lib/badline/vic20.rb CHANGED
@@ -75,6 +75,7 @@ module Badline
75
75
  @save_trap = nil
76
76
  @capture_output = nil
77
77
  @drive1541 = nil
78
+ @drive1581 = nil
78
79
  @init_threshold = boot_cycles
79
80
  wire_ports
80
81
  end
@@ -94,6 +95,7 @@ module Badline
94
95
  drive_nmi
95
96
  @cpu.cycle!
96
97
  @drive1541&.host_cycle!
98
+ @drive1581&.host_cycle!
97
99
 
98
100
  @cycles += 1
99
101
  end
@@ -176,6 +178,7 @@ module Badline
176
178
  via_written
177
179
  @drive&.reset!
178
180
  @drive1541&.reset!
181
+ @drive1581&.reset!
179
182
  @nmi_asserted = false
180
183
  @cpu.reset!
181
184
  end
@@ -14,6 +14,7 @@ require "badline/z80/extended_page"
14
14
  require "badline/z80/block_transfers"
15
15
  require "badline/z80/block_io"
16
16
  require "badline/z80/interrupt_acceptance"
17
+ require "badline/z80/saved_state"
17
18
 
18
19
  module Badline
19
20
  class Z80
@@ -42,6 +43,7 @@ module Badline
42
43
  include BlockTransfers
43
44
  include BlockIO
44
45
  include InterruptAcceptance
46
+ include SavedState
45
47
 
46
48
  attr_reader :bus, :cycles, :nmi
47
49
  attr_accessor :int
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Z80
5
+ # The registers, the T-state count and the interrupt latches for a
6
+ # snapshot, taken between instructions.
7
+ module SavedState
8
+ def save_state(out)
9
+ out.marker("Z80")
10
+ out.int(@a).int(@f).int(@b).int(@c).int(@d).int(@e).int(@h).int(@l)
11
+ out.int(@af_alt).int(@bc_alt).int(@de_alt).int(@hl_alt)
12
+ out.int(@i).int(@r).int(@pc).int(@sp).int(@ix).int(@iy).int(@wz).int(@q).int(@last_q).int(@im)
13
+ out.int(@cycles)
14
+ out.boolean(@iff1).boolean(@iff2).boolean(@halted).boolean(@after_ei).boolean(@after_ld_a_ir)
15
+ out.boolean(@int).boolean(@nmi).boolean(@nmi_pending)
16
+ end
17
+
18
+ def load_state(input)
19
+ input.marker("Z80")
20
+ load_main_registers(input)
21
+ @af_alt = input.int
22
+ @bc_alt = input.int
23
+ @de_alt = input.int
24
+ @hl_alt = input.int
25
+ load_special_registers(input)
26
+ @cycles = input.int
27
+ load_latches(input)
28
+ @prefix = 0
29
+ end
30
+
31
+ private
32
+
33
+ def load_main_registers(input)
34
+ @a = input.int
35
+ @f = input.int
36
+ @b = input.int
37
+ @c = input.int
38
+ @d = input.int
39
+ @e = input.int
40
+ @h = input.int
41
+ @l = input.int
42
+ end
43
+
44
+ def load_special_registers(input)
45
+ @i = input.int
46
+ @r = input.int
47
+ @pc = input.int
48
+ @sp = input.int
49
+ @ix = input.int
50
+ @iy = input.int
51
+ @wz = input.int
52
+ @q = input.int
53
+ @last_q = input.int
54
+ @im = input.int
55
+ end
56
+
57
+ def load_latches(input)
58
+ @iff1 = input.boolean?
59
+ @iff2 = input.boolean?
60
+ @halted = input.boolean?
61
+ @after_ei = input.boolean?
62
+ @after_ld_a_ir = input.boolean?
63
+ @int = input.boolean?
64
+ @nmi = input.boolean?
65
+ @nmi_pending = input.boolean?
66
+ end
67
+ end
68
+ end
69
+ end