badline 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +40 -0
- data/exe/badline +55 -0
- data/lib/badline/address_bus.rb +206 -0
- data/lib/badline/addressable.rb +61 -0
- data/lib/badline/cartridge/magic_desk.rb +23 -0
- data/lib/badline/cartridge/ocean.rb +33 -0
- data/lib/badline/cartridge/standard.rb +31 -0
- data/lib/badline/cartridge.rb +75 -0
- data/lib/badline/chrout_trap.rb +39 -0
- data/lib/badline/cia/timer.rb +122 -0
- data/lib/badline/cia.rb +189 -0
- data/lib/badline/color_memory.rb +16 -0
- data/lib/badline/computer.rb +100 -0
- data/lib/badline/control_ports.rb +24 -0
- data/lib/badline/cpu.rb +239 -0
- data/lib/badline/cycleable.rb +35 -0
- data/lib/badline/gui/application.rb +94 -0
- data/lib/badline/gui/joy_map.rb +19 -0
- data/lib/badline/gui/key_map.rb +34 -0
- data/lib/badline/gui/palette.rb +35 -0
- data/lib/badline/gui/pane.rb +35 -0
- data/lib/badline/gui/screen_pane.rb +50 -0
- data/lib/badline/gui/window.rb +46 -0
- data/lib/badline/gui.rb +11 -0
- data/lib/badline/instruction.rb +334 -0
- data/lib/badline/instruction_set/arithmetic.rb +119 -0
- data/lib/badline/instruction_set/bitwise.rb +131 -0
- data/lib/badline/instruction_set/branch.rb +78 -0
- data/lib/badline/instruction_set/flag.rb +63 -0
- data/lib/badline/instruction_set/illegal.rb +278 -0
- data/lib/badline/instruction_set/inc_dec.rb +71 -0
- data/lib/badline/instruction_set/stack.rb +104 -0
- data/lib/badline/instruction_set/transfer.rb +137 -0
- data/lib/badline/instruction_set.rb +77 -0
- data/lib/badline/integer_helper.rb +39 -0
- data/lib/badline/joystick.rb +25 -0
- data/lib/badline/kernal_trap/file.rb +54 -0
- data/lib/badline/kernal_trap/load.rb +63 -0
- data/lib/badline/kernal_trap/save.rb +42 -0
- data/lib/badline/kernal_trap.rb +5 -0
- data/lib/badline/keyboard.rb +58 -0
- data/lib/badline/keyboard_buffer.rb +33 -0
- data/lib/badline/media.rb +59 -0
- data/lib/badline/memory.rb +43 -0
- data/lib/badline/rom.rb +23 -0
- data/lib/badline/roms/README +18 -0
- data/lib/badline/roms/basic.rom +0 -0
- data/lib/badline/roms/character.rom +0 -0
- data/lib/badline/roms/kernal.rom +0 -0
- data/lib/badline/sid.rb +25 -0
- data/lib/badline/status.rb +56 -0
- data/lib/badline/storage/crt_file.rb +53 -0
- data/lib/badline/storage/d64_image.rb +21 -0
- data/lib/badline/storage/d71_image.rb +13 -0
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image.rb +71 -0
- data/lib/badline/storage/host_directory.rb +49 -0
- data/lib/badline/storage/p00.rb +24 -0
- data/lib/badline/storage.rb +28 -0
- data/lib/badline/time_of_day.rb +101 -0
- data/lib/badline/traps.rb +15 -0
- data/lib/badline/version.rb +5 -0
- data/lib/badline/vic/bank.rb +65 -0
- data/lib/badline/vic/display_state.rb +78 -0
- data/lib/badline/vic/graphics_mode.rb +139 -0
- data/lib/badline/vic/registers.rb +170 -0
- data/lib/badline/vic/sequencer.rb +237 -0
- data/lib/badline/vic/sprite.rb +121 -0
- data/lib/badline/vic/sprites.rb +112 -0
- data/lib/badline/vic.rb +192 -0
- data/lib/badline.rb +29 -0
- metadata +131 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KeyboardBuffer
|
|
5
|
+
ADDRESS = 0x0277
|
|
6
|
+
COUNT = 0xc6
|
|
7
|
+
CAPACITY = 10
|
|
8
|
+
|
|
9
|
+
def type_text(text)
|
|
10
|
+
codes = text.bytes.map { |b| ascii_to_petscii(b) }
|
|
11
|
+
on_init { @pending_keys = codes }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def feed_keyboard
|
|
17
|
+
return unless ram.peek(COUNT).zero?
|
|
18
|
+
|
|
19
|
+
chunk = @pending_keys.shift(CAPACITY)
|
|
20
|
+
ram.write(ADDRESS, chunk)
|
|
21
|
+
ram.poke(COUNT, chunk.length)
|
|
22
|
+
@pending_keys = nil if @pending_keys.empty?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ascii_to_petscii(byte)
|
|
26
|
+
case byte
|
|
27
|
+
when 0x61..0x7a then byte - 0x20
|
|
28
|
+
when 0x41..0x5a then byte + 0x80
|
|
29
|
+
else byte
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
AUTOSTART = %(lO"*",8,1\rrun\r)
|
|
6
|
+
BASIC_START = 0x0801
|
|
7
|
+
|
|
8
|
+
DISK_TYPES = {
|
|
9
|
+
".d64" => Storage::D64Image,
|
|
10
|
+
".d71" => Storage::D71Image,
|
|
11
|
+
".d81" => Storage::D81Image
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def attach(computer, path, autostart: true)
|
|
16
|
+
if File.directory?(path)
|
|
17
|
+
computer.mount(Storage::HostDirectory.new(path))
|
|
18
|
+
"Mounted #{path} as device 8"
|
|
19
|
+
elsif File.extname(path).downcase == ".crt"
|
|
20
|
+
attach_cartridge(computer, path)
|
|
21
|
+
elsif (image = DISK_TYPES[File.extname(path).downcase])
|
|
22
|
+
attach_disk(computer, image.new(path), path, autostart:)
|
|
23
|
+
else
|
|
24
|
+
attach_prg(computer, path, autostart:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def attach_cartridge(computer, path)
|
|
31
|
+
computer.attach_cartridge(Cartridge.from_file(path))
|
|
32
|
+
"Attached cartridge #{path}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attach_disk(computer, image, path, autostart:)
|
|
36
|
+
computer.mount(image)
|
|
37
|
+
computer.type_text(AUTOSTART) if autostart
|
|
38
|
+
"Mounted #{path} as device 8"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attach_prg(computer, path, autostart:)
|
|
42
|
+
bytes = File.binread(path).bytes
|
|
43
|
+
bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
|
|
44
|
+
computer.on_init { start_prg(computer, bytes, autostart:) }
|
|
45
|
+
"Loading #{path}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def start_prg(computer, data, autostart:)
|
|
49
|
+
load_addr = computer.load_prg(data)
|
|
50
|
+
# Run only makes sense for programs at BASIC start.
|
|
51
|
+
return unless autostart && load_addr == BASIC_START
|
|
52
|
+
|
|
53
|
+
end_addr = load_addr + data.length - 2
|
|
54
|
+
computer.ram.write(0x2d, [end_addr & 0xff, end_addr >> 8])
|
|
55
|
+
computer.type_text("run\r")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Memory
|
|
5
|
+
include Addressable
|
|
6
|
+
|
|
7
|
+
def initialize(initial = [], length: 2**16, start: 0)
|
|
8
|
+
addressable_at(start, length:)
|
|
9
|
+
@storage = zero_fill(initial)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def peek(addr)
|
|
13
|
+
@storage[index(addr)]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def poke(addr, value)
|
|
17
|
+
@storage[index(addr)] = value
|
|
18
|
+
value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read(addr, length)
|
|
22
|
+
(addr...(addr + length)).to_a.map { |a| peek(a) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write(addr, bytes)
|
|
26
|
+
Array(bytes).each_with_index { |b, i| poke(addr + i, b) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def blank_value
|
|
32
|
+
0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def zero_fill(initial)
|
|
36
|
+
array = initial.dup
|
|
37
|
+
0.upto(length - 1) do |i|
|
|
38
|
+
array[i] ||= blank_value
|
|
39
|
+
end
|
|
40
|
+
array
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/badline/rom.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class ROM < Memory
|
|
5
|
+
class << self
|
|
6
|
+
def load(filename, start = 0x0)
|
|
7
|
+
data = File.read(file_path(filename)).bytes
|
|
8
|
+
new(data, length: data.length, start: start)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def file_path(filename)
|
|
14
|
+
File.join(File.dirname(__FILE__), "roms", filename)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poke(_addr, _value)
|
|
19
|
+
raise ReadOnlyMemoryError
|
|
20
|
+
end
|
|
21
|
+
alias []= poke
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Commodore 64 ROMs
|
|
2
|
+
=================
|
|
3
|
+
|
|
4
|
+
Downloaded from http://www.unusedino.de/ec64/technical2.html
|
|
5
|
+
Credited in turn to ftp://ftp.funet.fi/pub/cbm
|
|
6
|
+
|
|
7
|
+
basic.rom
|
|
8
|
+
---------
|
|
9
|
+
Commodore 64 BASIC V2. The first and only revision.
|
|
10
|
+
|
|
11
|
+
kernal.rom
|
|
12
|
+
----------
|
|
13
|
+
Commodore 64 KERNAL ROM Revision 3. The last revision, also used in
|
|
14
|
+
the C128's C64 mode.
|
|
15
|
+
|
|
16
|
+
character.rom
|
|
17
|
+
-------------
|
|
18
|
+
The character generator ROM.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/badline/sid.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
include Addressable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
addressable_at(0xd400, length: 2**10)
|
|
9
|
+
@registers = Memory.new(length: 2**5)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def peek(addr)
|
|
13
|
+
i = index(addr) % (2**5)
|
|
14
|
+
case i
|
|
15
|
+
when 0x1d..0x1f then 0xff # Unused memory
|
|
16
|
+
else @registers.peek(i)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def poke(addr, value)
|
|
21
|
+
i = index(addr) % (2**5)
|
|
22
|
+
@registers.poke(i, value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Status
|
|
5
|
+
attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
|
|
6
|
+
|
|
7
|
+
def initialize(flags = [], value: 0x0)
|
|
8
|
+
@flags = flags
|
|
9
|
+
|
|
10
|
+
@bitmask = create_mask { |f| f.is_a?(Symbol) }
|
|
11
|
+
@low_mask = create_mask { |f| f.is_a?(Integer) && f.zero? }
|
|
12
|
+
@high_mask = create_mask { |f| f.is_a?(Integer) && f == 1 }
|
|
13
|
+
|
|
14
|
+
self.value = value
|
|
15
|
+
|
|
16
|
+
define_accessors!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value=(new_value)
|
|
20
|
+
@value = (new_value | high_mask) & ~low_mask
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def create_mask(&predicate)
|
|
26
|
+
flags.each.with_index.inject(0) do |mask, (flag, i)|
|
|
27
|
+
mask + (predicate.call(flag) ? 1 << i : 0)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def define_accessors!
|
|
32
|
+
flags.each_with_index do |name, i|
|
|
33
|
+
next unless name.is_a?(Symbol)
|
|
34
|
+
|
|
35
|
+
mask = 1 << i
|
|
36
|
+
define_singleton_method("#{name}=") do |enabled|
|
|
37
|
+
update(mask, enabled)
|
|
38
|
+
end
|
|
39
|
+
define_singleton_method("#{name}?") do
|
|
40
|
+
!@value.nobits?(mask)
|
|
41
|
+
end
|
|
42
|
+
define_singleton_method(name) do
|
|
43
|
+
@value.nobits?(mask) ? 0 : 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def update(mask, enabled)
|
|
49
|
+
self.value = if enabled && enabled != 0
|
|
50
|
+
value | mask
|
|
51
|
+
else
|
|
52
|
+
value & ~mask
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class CRTFile
|
|
6
|
+
class FormatError < StandardError; end
|
|
7
|
+
|
|
8
|
+
SIGNATURE = "C64 CARTRIDGE ".b
|
|
9
|
+
CHIP_SIGNATURE = "CHIP".b
|
|
10
|
+
CHIP_HEADER_SIZE = 0x10
|
|
11
|
+
RAM_CHIP = 1
|
|
12
|
+
|
|
13
|
+
Chip = Data.define(:chip_type, :bank, :address, :data)
|
|
14
|
+
|
|
15
|
+
attr_reader :hardware_type, :exrom, :game, :name, :chips
|
|
16
|
+
|
|
17
|
+
def initialize(path)
|
|
18
|
+
parse(File.binread(path))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def parse(bytes)
|
|
24
|
+
raise FormatError, "Missing CRT signature" unless bytes.start_with?(SIGNATURE)
|
|
25
|
+
|
|
26
|
+
@hardware_type = bytes[0x16, 2].unpack1("n")
|
|
27
|
+
@exrom = bytes.getbyte(0x18)
|
|
28
|
+
@game = bytes.getbyte(0x19)
|
|
29
|
+
@name = bytes[0x20, 32].unpack1("Z*")
|
|
30
|
+
@chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_chips(bytes, offset)
|
|
34
|
+
chips = []
|
|
35
|
+
while offset < bytes.length
|
|
36
|
+
chip, offset = parse_chip(bytes, offset)
|
|
37
|
+
chips << chip
|
|
38
|
+
end
|
|
39
|
+
chips
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_chip(bytes, offset)
|
|
43
|
+
raise FormatError, "Bad CHIP packet at offset #{offset}" unless bytes[offset, 4] == CHIP_SIGNATURE
|
|
44
|
+
|
|
45
|
+
packet_length = bytes[offset + 4, 4].unpack1("N")
|
|
46
|
+
chip_type, bank, address, size = bytes[offset + 8, 8].unpack("n4")
|
|
47
|
+
data = chip_type == RAM_CHIP ? [] : bytes[offset + CHIP_HEADER_SIZE, size].bytes
|
|
48
|
+
[Chip.new(chip_type:, bank:, address:, data:),
|
|
49
|
+
offset + [packet_length, CHIP_HEADER_SIZE + data.length].max]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class D64Image < DiskImage
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def directory_track = 18
|
|
9
|
+
def directory_sector = 1
|
|
10
|
+
|
|
11
|
+
def sectors_in(track)
|
|
12
|
+
case track
|
|
13
|
+
when 1..17 then 21
|
|
14
|
+
when 18..24 then 19
|
|
15
|
+
when 25..30 then 18
|
|
16
|
+
else 17
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
SECTOR_SIZE = 256
|
|
7
|
+
ENTRY_SIZE = 32
|
|
8
|
+
ENTRIES_PER_SECTOR = 8
|
|
9
|
+
FILETYPE_PRG = 0x02
|
|
10
|
+
NAME_PADDING = 0xa0
|
|
11
|
+
|
|
12
|
+
def initialize(path)
|
|
13
|
+
@bytes = File.binread(path).bytes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def read_file(name)
|
|
17
|
+
pattern = Storage.matcher(name)
|
|
18
|
+
entry = entries.find { |e| pattern.match?(e[:name]) }
|
|
19
|
+
read_chain(entry[:track], entry[:sector]) if entry
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def entries
|
|
25
|
+
@entries ||= each_sector(directory_track, directory_sector)
|
|
26
|
+
.flat_map { |data| parse_entries(data) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse_entries(data)
|
|
30
|
+
(0...ENTRIES_PER_SECTOR).filter_map do |i|
|
|
31
|
+
entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
|
|
32
|
+
next unless (entry[2] & 0x07) == FILETYPE_PRG
|
|
33
|
+
|
|
34
|
+
{ name: decode_name(entry[5, 16]),
|
|
35
|
+
track: entry[3],
|
|
36
|
+
sector: entry[4] }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def decode_name(bytes)
|
|
41
|
+
Storage.ascii(bytes.take_while { |b| b != NAME_PADDING }).downcase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_chain(track, sector)
|
|
45
|
+
each_sector(track, sector).flat_map do |data|
|
|
46
|
+
data[0].zero? ? data[2..data[1]] : data[2..]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def each_sector(track, sector)
|
|
51
|
+
return to_enum(:each_sector, track, sector) unless block_given?
|
|
52
|
+
|
|
53
|
+
visited = {}
|
|
54
|
+
while track != 0 && !visited[[track, sector]]
|
|
55
|
+
visited[[track, sector]] = true
|
|
56
|
+
data = sector_at(track, sector)
|
|
57
|
+
yield data
|
|
58
|
+
track, sector = data[0, 2]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sector_at(track, sector)
|
|
63
|
+
@bytes[sector_offset(track, sector), SECTOR_SIZE]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def sector_offset(track, sector)
|
|
67
|
+
((1...track).sum { |t| sectors_in(t) } + sector) * SECTOR_SIZE
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class HostDirectory
|
|
6
|
+
def initialize(path)
|
|
7
|
+
@path = path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def read_file(name)
|
|
11
|
+
entry = find(name)
|
|
12
|
+
return unless entry
|
|
13
|
+
|
|
14
|
+
bytes = File.binread(File.join(@path, entry[:file])).bytes
|
|
15
|
+
P00.wraps?(bytes) ? P00.data(bytes) : bytes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write_file(name, bytes)
|
|
19
|
+
host_name = "#{name.downcase.tr('/', '_')}.prg"
|
|
20
|
+
File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def find(name)
|
|
26
|
+
pattern = Storage.matcher(name)
|
|
27
|
+
entries.find { |e| pattern.match?(e[:name]) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entries
|
|
31
|
+
Dir.children(@path).sort.filter_map { |f| entry_for(f) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def entry_for(file)
|
|
35
|
+
case File.extname(file).downcase
|
|
36
|
+
when ".prg"
|
|
37
|
+
{ name: File.basename(file, ".*").downcase, file: }
|
|
38
|
+
when ".p00"
|
|
39
|
+
p00_entry(file)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def p00_entry(file)
|
|
44
|
+
header = File.binread(File.join(@path, file), P00::HEADER_SIZE).bytes
|
|
45
|
+
{ name: P00.name(header), file: } if P00.wraps?(header)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
module P00
|
|
6
|
+
MAGIC = "C64File\x00".bytes.freeze
|
|
7
|
+
HEADER_SIZE = 26
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def wraps?(bytes)
|
|
11
|
+
bytes[0, 8] == MAGIC
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name(bytes)
|
|
15
|
+
Storage.ascii(bytes[8, 16].take_while(&:positive?)).downcase
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def data(bytes)
|
|
19
|
+
bytes[HEADER_SIZE..]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/storage/p00"
|
|
4
|
+
require "badline/storage/host_directory"
|
|
5
|
+
require "badline/storage/disk_image"
|
|
6
|
+
require "badline/storage/d64_image"
|
|
7
|
+
require "badline/storage/d71_image"
|
|
8
|
+
require "badline/storage/d81_image"
|
|
9
|
+
require "badline/storage/crt_file"
|
|
10
|
+
|
|
11
|
+
module Badline
|
|
12
|
+
module Storage
|
|
13
|
+
class << self
|
|
14
|
+
# Folds shifted PETSCII letters to their ASCII equivalents.
|
|
15
|
+
def ascii(bytes)
|
|
16
|
+
bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
|
|
20
|
+
def matcher(name)
|
|
21
|
+
escaped = Regexp.escape(name.downcase)
|
|
22
|
+
.gsub('\*', ".*")
|
|
23
|
+
.gsub('\?', ".")
|
|
24
|
+
Regexp.new("\\A#{escaped}\\z")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class TimeOfDay
|
|
5
|
+
include IntegerHelper
|
|
6
|
+
|
|
7
|
+
CLOCK_HZ = 985_248 # PAL only for now.
|
|
8
|
+
|
|
9
|
+
def initialize(clock_hz: CLOCK_HZ)
|
|
10
|
+
# The accumulator advances 10 per cycle, so a tenth of a second has
|
|
11
|
+
# passed when it reaches clock_hz. Integer math keeps it exact.
|
|
12
|
+
@cycles_per_tenth = clock_hz
|
|
13
|
+
@accumulator = 0
|
|
14
|
+
@clock = { tenths: 0, seconds: 0, minutes: 0, hours: 12, pm: false }
|
|
15
|
+
@alarm = { tenths: 0, seconds: 0, minutes: 0, hours: 0, pm: false }
|
|
16
|
+
@latch = nil
|
|
17
|
+
@stopped = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cycle!
|
|
21
|
+
return if @stopped
|
|
22
|
+
|
|
23
|
+
@accumulator += 10
|
|
24
|
+
return if @accumulator < @cycles_per_tenth
|
|
25
|
+
|
|
26
|
+
@accumulator -= @cycles_per_tenth
|
|
27
|
+
advance
|
|
28
|
+
yield if block_given? && alarm?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def tenths
|
|
32
|
+
value = (@latch || @clock)[:tenths]
|
|
33
|
+
@latch = nil
|
|
34
|
+
bcd(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def seconds
|
|
38
|
+
bcd((@latch || @clock)[:seconds])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def minutes
|
|
42
|
+
bcd((@latch || @clock)[:minutes])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hours
|
|
46
|
+
@latch ||= @clock.dup
|
|
47
|
+
bcd(@latch[:hours]) | (@latch[:pm] ? 0x80 : 0)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write(field, value, alarm:)
|
|
51
|
+
(alarm ? @alarm : @clock)[field] = bcd_to_i(value)
|
|
52
|
+
# Start the clock again when writing tenths.
|
|
53
|
+
resume if field == :tenths && !alarm
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 12-hour BCD value, bit 7 is the AM/PM flag.
|
|
57
|
+
def write_hours(value, alarm:)
|
|
58
|
+
target = alarm ? @alarm : @clock
|
|
59
|
+
target[:hours] = bcd_to_i(value & 0x7f)
|
|
60
|
+
target[:pm] = value.anybits?(0x80)
|
|
61
|
+
# Halt the clock when writing hours, so that it doesn't
|
|
62
|
+
# advance mid-update.
|
|
63
|
+
@stopped = true unless alarm
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def resume
|
|
69
|
+
@stopped = false
|
|
70
|
+
@accumulator = 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def advance
|
|
74
|
+
@clock[:tenths] += 1
|
|
75
|
+
return if @clock[:tenths] < 10
|
|
76
|
+
|
|
77
|
+
@clock[:tenths] = 0
|
|
78
|
+
@clock[:seconds] += 1
|
|
79
|
+
return if @clock[:seconds] < 60
|
|
80
|
+
|
|
81
|
+
@clock[:seconds] = 0
|
|
82
|
+
@clock[:minutes] += 1
|
|
83
|
+
return if @clock[:minutes] < 60
|
|
84
|
+
|
|
85
|
+
@clock[:minutes] = 0
|
|
86
|
+
advance_hour
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def advance_hour
|
|
90
|
+
@clock[:hours] += 1
|
|
91
|
+
@clock[:pm] = !@clock[:pm] if @clock[:hours] == 12
|
|
92
|
+
@clock[:hours] = 1 if @clock[:hours] > 12
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def alarm?
|
|
96
|
+
%i[tenths seconds minutes hours pm].all? do |field|
|
|
97
|
+
@clock[field] == @alarm[field]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|