2016-06-02 22:09:34 +00:00
|
|
|
from collections import defaultdict
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
2018-06-24 19:10:59 +00:00
|
|
|
from . import errors
|
2017-05-07 20:28:15 +00:00
|
|
|
from .segments import SegmentData, EmptySegment, ObjSegment
|
|
|
|
from .diskimages import DiskImageBase
|
|
|
|
from .utils import to_numpy
|
2016-06-02 22:09:34 +00:00
|
|
|
|
2016-06-02 23:59:59 +00:00
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
# From atari800 source
|
|
|
|
known_cart_types = [
|
|
|
|
# (note: all size units in KB)
|
|
|
|
# atari800 index number
|
|
|
|
# name
|
|
|
|
# total size
|
|
|
|
# static size
|
|
|
|
# static offset
|
|
|
|
# static address
|
|
|
|
# banked size
|
|
|
|
# banked offset (for bank zero)
|
|
|
|
# banked address
|
2016-06-02 23:59:59 +00:00
|
|
|
(0, "", 0,),
|
2016-06-03 00:18:06 +00:00
|
|
|
(57, "Standard 2 KB", 2, 2, 0, 0xb800),
|
|
|
|
(58, "Standard 4 KB", 4, 4, 0, 0xb000),
|
2016-06-03 20:05:16 +00:00
|
|
|
(59, "Right slot 4 KB", 4, 4, 0, 0, 0x9000),
|
2016-06-02 23:59:59 +00:00
|
|
|
(1, "Standard 8 KB", 8, 8, 0, 0xa000),
|
2016-06-03 05:58:11 +00:00
|
|
|
(21, "Right slot 8 KB", 8,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(2, "Standard 16 KB", 16, 16, 0, 0x8000),
|
2016-06-03 05:58:11 +00:00
|
|
|
(44, "OSS 8 KB", 8,),
|
2016-06-03 00:18:06 +00:00
|
|
|
(15, "OSS one chip 16 KB", 16,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(3, "OSS two chip (034M) 16 KB", 16, 4, 12, 0xb000, 4, 0, 0xa000),
|
2016-06-03 00:18:06 +00:00
|
|
|
(45, "OSS two chip (043M) 16 KB", 16, 4, 12, 0xb000, 4, 0, 0xa000),
|
2016-06-02 23:59:59 +00:00
|
|
|
(12, "XEGS 32 KB", 32, 8, 24, 0xa000, 8, 0, 0x8000),
|
|
|
|
(13, "XEGS (banks 0-7) 64 KB", 64, 8, 56, 0xa000, 8, 0, 0x8000),
|
2016-06-03 00:18:06 +00:00
|
|
|
(67, "XEGS (banks 8-15) 64 KB", 64, 8, 56, 0xa000, 8, 0, 0x8000),
|
2016-06-02 23:59:59 +00:00
|
|
|
(14, "XEGS 128 KB", 128, 8, 120, 0xa000, 8, 0, 0x8000),
|
|
|
|
(23, "XEGS 256 KB", 256, 8, 248, 0xa000, 8, 0, 0x8000),
|
|
|
|
(24, "XEGS 512 KB", 512, 8, 504, 0xa000, 8, 0, 0x8000),
|
|
|
|
(25, "XEGS 1 MB", 1024, 8, 1016, 0xa000, 8, 0, 0x8000 ),
|
2016-06-03 05:58:11 +00:00
|
|
|
(33, "Switchable XEGS 32 KB", 32, 8, 24, 0xa000, 8, 0, 0x8000),
|
|
|
|
(34, "Switchable XEGS 64 KB", 64, 8, 56, 0xa000, 8, 0, 0x8000),
|
|
|
|
(35, "Switchable XEGS 128 KB", 128, 8, 120, 0xa000, 8, 0, 0x8000),
|
|
|
|
(36, "Switchable XEGS 256 KB", 256, 8, 248, 0xa000, 8, 0, 0x8000),
|
|
|
|
(37, "Switchable XEGS 512 KB", 512, 8, 504, 0xa000, 8, 0, 0x8000),
|
|
|
|
(38, "Switchable XEGS 1 MB", 1024, 8, 1016, 0xa000, 8, 0, 0x8000 ),
|
|
|
|
(22, "Williams 32 KB", 32,),
|
|
|
|
(8, "Williams 64 KB", 64,),
|
|
|
|
(9, "Express 64 KB", 64,),
|
|
|
|
(10, "Diamond 64 KB", 64,),
|
|
|
|
(11, "SpartaDOS X 64 KB", 64,),
|
|
|
|
(43, "SpartaDOS X 128 KB", 128,),
|
|
|
|
(17, "Atrax 128 KB", 128,),
|
|
|
|
(18, "Bounty Bob 40 KB", 40,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(26, "MegaCart 16 KB", 16,),
|
|
|
|
(27, "MegaCart 32 KB", 32,),
|
|
|
|
(28, "MegaCart 64 KB", 64,),
|
|
|
|
(29, "MegaCart 128 KB", 128,),
|
|
|
|
(30, "MegaCart 256 KB", 256,),
|
|
|
|
(31, "MegaCart 512 KB", 512,),
|
|
|
|
(32, "MegaCart 1 MB", 1024,),
|
|
|
|
(39, "Phoenix 8 KB", 8,),
|
2016-06-03 05:58:11 +00:00
|
|
|
(46, "Blizzard 4 KB", 4,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(40, "Blizzard 16 KB", 16, 16, 0, 0x8000),
|
2016-06-03 05:58:11 +00:00
|
|
|
(60, "Blizzard 32 KB", 32,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(41, "Atarimax 128 KB Flash", 128,),
|
|
|
|
(42, "Atarimax 1 MB Flash", 1024,),
|
|
|
|
(47, "AST 32 KB", 32,),
|
|
|
|
(48, "Atrax SDX 64 KB", 64,),
|
|
|
|
(49, "Atrax SDX 128 KB", 128,),
|
|
|
|
(50, "Turbosoft 64 KB", 64,),
|
|
|
|
(51, "Turbosoft 128 KB", 128,),
|
|
|
|
(52, "Ultracart 32 KB", 32,),
|
|
|
|
(53, "Low bank 8 KB", 8, 8, 0, 0x8000),
|
2016-06-03 05:58:11 +00:00
|
|
|
(5, "DB 32 KB", 32,),
|
2016-06-02 23:59:59 +00:00
|
|
|
(54, "SIC! 128 KB", 128,),
|
|
|
|
(55, "SIC! 256 KB", 256,),
|
|
|
|
(56, "SIC! 512 KB", 512,),
|
|
|
|
(61, "MegaMax 2 MB", 2048,),
|
|
|
|
(62, "The!Cart 128 MB", 128*1024,),
|
|
|
|
(63, "Flash MegaCart 4 MB", 4096,),
|
|
|
|
(64, "MegaCart 2 MB", 2048,),
|
|
|
|
(65, "The!Cart 32 MB", 32*1024,),
|
|
|
|
(66, "The!Cart 64 MB", 64*1024,),
|
2016-06-03 00:18:06 +00:00
|
|
|
(20, "Standard 4 KB 5200", 4, 4, 0, 0x8000),
|
|
|
|
(19, "Standard 8 KB 5200", 8, 8, 0, 0x8000),
|
|
|
|
(4, "Standard 32 KB 5200", 32, 32, 0, 0x4000),
|
|
|
|
(16, "One chip 16 KB 5200", 16,),
|
|
|
|
(6, "Two chip 16 KB 5200", 16,),
|
|
|
|
(7, "Bounty Bob 40 KB 5200", 40,),
|
2016-06-02 22:09:34 +00:00
|
|
|
]
|
|
|
|
|
2016-06-03 00:18:06 +00:00
|
|
|
known_cart_type_map = {c[0]:i for i, c in enumerate(known_cart_types)}
|
|
|
|
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def get_known_carts():
|
|
|
|
grouped = defaultdict(list)
|
2016-06-03 00:18:06 +00:00
|
|
|
for c in known_cart_types[1:]:
|
2016-06-02 22:09:34 +00:00
|
|
|
size = c[2]
|
2016-06-03 00:18:06 +00:00
|
|
|
grouped[size].append(c)
|
2016-06-02 22:09:34 +00:00
|
|
|
return grouped
|
|
|
|
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-03 00:18:06 +00:00
|
|
|
def get_cart(cart_type):
|
2016-06-03 20:05:16 +00:00
|
|
|
try:
|
|
|
|
return known_cart_types[known_cart_type_map[cart_type]]
|
|
|
|
except KeyError:
|
2018-06-24 19:10:59 +00:00
|
|
|
raise errors.InvalidDiskImage("Unsupported cart type %d" % cart_type)
|
2016-06-03 00:18:06 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
|
2018-06-25 00:20:32 +00:00
|
|
|
class A8CartHeader:
|
2016-06-02 22:09:34 +00:00
|
|
|
# Atari Cart format described by https://sourceforge.net/p/atari800/source/ci/master/tree/DOC/cart.txt NOTE: Big endian!
|
|
|
|
format = np.dtype([
|
|
|
|
('magic', '|S4'),
|
|
|
|
('format', '>u4'),
|
|
|
|
('checksum', '>u4'),
|
|
|
|
('unused','>u4')
|
|
|
|
])
|
2018-10-26 19:14:56 +00:00
|
|
|
nominal_length = format.itemsize
|
2016-06-02 22:09:34 +00:00
|
|
|
file_format = "Cart"
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def __init__(self, bytes=None, create=False):
|
|
|
|
self.image_size = 0
|
|
|
|
self.cart_type = -1
|
|
|
|
self.cart_name = ""
|
|
|
|
self.cart_size = 0
|
|
|
|
self.crc = 0
|
|
|
|
self.unused = 0
|
|
|
|
self.header_offset = 0
|
|
|
|
self.num_banks = 0
|
|
|
|
self.banks = []
|
|
|
|
self.bank_size = 0
|
|
|
|
self.bank_origin = 0
|
|
|
|
self.main_size = 0
|
|
|
|
self.main_offset = 0
|
|
|
|
self.main_origin = 0
|
|
|
|
self.possible_types = set()
|
|
|
|
if create:
|
2018-10-26 19:14:56 +00:00
|
|
|
self.header_offset = self.nominal_length
|
2016-06-02 22:09:34 +00:00
|
|
|
self.check_size(0)
|
|
|
|
if bytes is None:
|
|
|
|
return
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
if len(bytes) == 16:
|
|
|
|
values = bytes.view(dtype=self.format)[0]
|
2017-05-07 21:03:25 +00:00
|
|
|
if values[0] != b'CART':
|
2018-06-24 19:10:59 +00:00
|
|
|
raise errors.InvalidCartHeader
|
2016-06-02 22:09:34 +00:00
|
|
|
self.cart_type = int(values[1])
|
|
|
|
self.crc = int(values[2])
|
2018-10-26 19:14:56 +00:00
|
|
|
self.header_offset = self.nominal_length
|
2016-06-02 22:09:34 +00:00
|
|
|
self.set_type(self.cart_type)
|
|
|
|
else:
|
2018-06-24 19:10:59 +00:00
|
|
|
raise errors.InvalidCartHeader
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def __str__(self):
|
|
|
|
return "%s Cartridge (atari800 type=%d size=%d, %d banks, crc=%d)" % (self.cart_name, self.cart_type, self.cart_size, self.bank_size, self.crc)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def __len__(self):
|
|
|
|
return self.header_offset
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2018-10-26 19:14:56 +00:00
|
|
|
@property
|
|
|
|
def valid(self):
|
|
|
|
return self.cart_type != -1
|
|
|
|
|
|
|
|
def calc_crc_from_data(self, data):
|
|
|
|
self.crc = 0
|
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def to_array(self):
|
2018-10-26 19:14:56 +00:00
|
|
|
raw = np.zeros([self.nominal_length], dtype=np.uint8)
|
2016-06-02 22:09:34 +00:00
|
|
|
values = raw.view(dtype=self.format)[0]
|
2017-05-09 05:26:48 +00:00
|
|
|
values[0] = b'CART'
|
2016-06-02 22:09:34 +00:00
|
|
|
values[1] = self.cart_type
|
|
|
|
values[2] = self.crc
|
|
|
|
values[3] = 0
|
|
|
|
return raw
|
|
|
|
|
|
|
|
def set_type(self, cart_type):
|
|
|
|
self.cart_type = cart_type
|
2016-06-03 00:18:06 +00:00
|
|
|
c = get_cart(cart_type)
|
2016-06-02 22:09:34 +00:00
|
|
|
self.cart_name = c[1]
|
|
|
|
self.cart_size = c[2]
|
2016-06-02 23:59:59 +00:00
|
|
|
self.main_size = self.cart_size
|
2016-06-02 22:09:34 +00:00
|
|
|
if len(c) >= 6:
|
|
|
|
self.main_size, self.main_offset, self.main_origin = c[3:6]
|
|
|
|
if len(c) >= 9:
|
|
|
|
self.banks = []
|
|
|
|
self.bank_size, offset, self.bank_origin = c[6:9]
|
|
|
|
s = self.cart_size - self.main_size
|
|
|
|
while s > 0:
|
|
|
|
self.banks.append(offset)
|
|
|
|
offset += self.bank_size
|
|
|
|
s -= self.bank_size
|
|
|
|
|
|
|
|
def check_size(self, size):
|
|
|
|
self.possible_types = set()
|
|
|
|
k, r = divmod(size, 1024)
|
2018-10-26 19:14:56 +00:00
|
|
|
if r == 0 or r == self.nominal_length:
|
2016-06-02 22:09:34 +00:00
|
|
|
for i, t in enumerate(known_cart_types):
|
|
|
|
valid_size = t[0]
|
|
|
|
if k == valid_size:
|
|
|
|
self.possible_types.add(i)
|
|
|
|
|
|
|
|
|
2018-10-26 02:51:33 +00:00
|
|
|
class BaseAtariCartImage(DiskImageBase):
|
2016-06-02 22:09:34 +00:00
|
|
|
def __str__(self):
|
|
|
|
return str(self.header)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def read_header(self):
|
2017-05-07 21:03:25 +00:00
|
|
|
data = self.bytes[0:16]
|
2016-06-02 22:09:34 +00:00
|
|
|
try:
|
2017-05-07 21:03:25 +00:00
|
|
|
self.header = A8CartHeader(data)
|
2018-06-24 19:10:59 +00:00
|
|
|
except errors.InvalidCartHeader:
|
2018-10-26 19:14:56 +00:00
|
|
|
self.header = A8CartHeader()
|
2016-06-02 23:59:59 +00:00
|
|
|
|
|
|
|
def strict_check(self):
|
2018-10-26 02:51:33 +00:00
|
|
|
raise NotImplementedError
|
2016-06-02 23:59:59 +00:00
|
|
|
|
|
|
|
def relaxed_check(self):
|
|
|
|
if self.header.cart_type != self.cart_type:
|
|
|
|
# force the header to be the specified cart type
|
|
|
|
self.header = A8CartHeader()
|
|
|
|
self.header.set_type(self.cart_type)
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def check_size(self):
|
2018-10-26 19:14:56 +00:00
|
|
|
if not self.header.valid:
|
2016-06-02 22:09:34 +00:00
|
|
|
return
|
|
|
|
k, rem = divmod((len(self) - len(self.header)), 1024)
|
2018-10-26 19:14:56 +00:00
|
|
|
c = get_cart(self.header.cart_type)
|
2016-06-03 20:05:16 +00:00
|
|
|
log.debug("checking type=%d, k=%d, rem=%d for %s, %s" % (self.cart_type, k, rem, c[1], c[2]))
|
2016-06-02 22:09:34 +00:00
|
|
|
if rem > 0:
|
2018-06-24 19:10:59 +00:00
|
|
|
raise errors.InvalidDiskImage("Cart not multiple of 1K")
|
2016-06-02 22:09:34 +00:00
|
|
|
if k != c[2]:
|
2018-06-24 19:10:59 +00:00
|
|
|
raise errors.InvalidDiskImage("Image size %d doesn't match cart type %d size %d" % (k, self.cart_type, c[2]))
|
2017-03-23 17:06:37 +00:00
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def parse_segments(self):
|
|
|
|
r = self.rawdata
|
|
|
|
i = self.header.header_offset
|
|
|
|
if i > 0:
|
|
|
|
self.segments.append(ObjSegment(r[0:i], 0, 0, 0, i, name="Cart Header"))
|
|
|
|
self.segments.extend(self.get_main_segment())
|
|
|
|
self.segments.extend(self.get_banked_segments())
|
|
|
|
|
|
|
|
def get_main_segment(self):
|
|
|
|
r = self.rawdata
|
|
|
|
start = self.header.header_offset + self.header.main_offset * 1024
|
|
|
|
end = start + (self.header.main_size * 1024)
|
|
|
|
s = ObjSegment(r[start:end], 0, 0, self.header.main_origin, name="Main Bank")
|
|
|
|
return [s]
|
|
|
|
|
|
|
|
def get_banked_segments(self):
|
|
|
|
segments = []
|
|
|
|
r = self.rawdata
|
|
|
|
for i, offset in enumerate(self.header.banks):
|
|
|
|
start = self.header.header_offset + offset * 1024
|
|
|
|
end = start + (self.header.bank_size * 1024)
|
|
|
|
s = ObjSegment(r[start:end], 0, 0, self.header.bank_origin, name="Bank #%d" % (i + 1))
|
|
|
|
segments.append(s)
|
|
|
|
return segments
|
|
|
|
|
2018-10-26 19:14:56 +00:00
|
|
|
def create_emulator_boot_segment(self):
|
|
|
|
print(self.segments)
|
|
|
|
h = self.header
|
|
|
|
k, rem = divmod(len(self), 1024)
|
|
|
|
if rem == 0:
|
|
|
|
h.calc_crc_from_data(self.bytes)
|
|
|
|
data_with_header = np.empty(len(self) + h.nominal_length, dtype=np.uint8)
|
|
|
|
data_with_header[0:h.nominal_length] = h.to_array()
|
|
|
|
data_with_header[h.nominal_length:] = self.bytes
|
|
|
|
r = SegmentData(data_with_header)
|
|
|
|
else:
|
|
|
|
r = self.rawdata
|
|
|
|
s = ObjSegment(r, 0, 0, self.header.main_origin, name="Cart image")
|
|
|
|
return s
|
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
|
2018-10-26 02:51:33 +00:00
|
|
|
class AtariCartImage(BaseAtariCartImage):
|
|
|
|
def __init__(self, rawdata, cart_type, filename=""):
|
2018-10-26 19:14:56 +00:00
|
|
|
c = get_cart(cart_type)
|
2018-10-26 02:51:33 +00:00
|
|
|
self.cart_type = cart_type
|
|
|
|
DiskImageBase.__init__(self, rawdata, filename)
|
|
|
|
|
|
|
|
def strict_check(self):
|
2018-10-26 19:14:56 +00:00
|
|
|
if not self.header.valid:
|
|
|
|
raise errors.InvalidDiskImage("Missing cart header")
|
2018-10-26 02:51:33 +00:00
|
|
|
if self.header.cart_type != self.cart_type:
|
|
|
|
raise errors.InvalidDiskImage("Cart type doesn't match type defined in header")
|
|
|
|
|
|
|
|
|
|
|
|
class Atari8bitCartImage(BaseAtariCartImage):
|
|
|
|
def strict_check(self):
|
|
|
|
if "5200" in self.header.cart_name:
|
|
|
|
raise errors.InvalidDiskImage("5200 Carts don't work in the home computers.")
|
2018-10-26 19:14:56 +00:00
|
|
|
AtariCartImage.strict_check(self)
|
2018-10-26 02:51:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Atari5200CartImage(BaseAtariCartImage):
|
|
|
|
def strict_check(self):
|
|
|
|
if "5200" not in self.header.cart_name:
|
|
|
|
raise errors.InvalidDiskImage("Home computer carts don't work in the 5200.")
|
2018-10-26 19:14:56 +00:00
|
|
|
AtariCartImage.strict_check(self)
|
2018-10-26 02:51:33 +00:00
|
|
|
|
|
|
|
|
2016-06-02 22:09:34 +00:00
|
|
|
def add_cart_header(bytes):
|
|
|
|
header = A8CartHeader(create=True)
|
|
|
|
header.check_size(len(bytes))
|
|
|
|
hlen = len(header)
|
|
|
|
data = np.empty([hlen + len(bytes)], dtype=np.uint8)
|
|
|
|
data[0:hlen] = header.to_array()
|
|
|
|
data[hlen:] = bytes
|
|
|
|
return data
|
2018-10-26 02:51:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class RomImage(DiskImageBase):
|
|
|
|
def __str__(self):
|
|
|
|
return f"{len(self.rawdata) // 1024}k ROM image"
|
|
|
|
|
|
|
|
def read_header(self):
|
|
|
|
self.header = A8CartHeader()
|
|
|
|
|
|
|
|
def strict_check(self):
|
|
|
|
self.check_size()
|
|
|
|
|
|
|
|
def check_size(self):
|
|
|
|
size = len(self)
|
|
|
|
if (size & (size - 1)) != 0:
|
|
|
|
raise errors.InvalidDiskImage("ROM image not a power of 2")
|
|
|
|
|
|
|
|
def parse_segments(self):
|
|
|
|
r = self.rawdata
|
|
|
|
s = ObjSegment(r, 0, 0, self.header.main_origin, name="Main Bank")
|
2018-10-26 05:38:39 +00:00
|
|
|
self.segments = [s]
|
|
|
|
|
|
|
|
def create_emulator_boot_segment(self):
|
|
|
|
print(self.segments)
|
|
|
|
s = self.segments[0]
|
|
|
|
if s.origin == 0:
|
|
|
|
return None
|
|
|
|
return s
|