#!/usr/bin/env python # -*- coding: utf-8 -*- # Magic Desk Compatible Cartridge Generator (c) 2013-2019 Žarko Živanov # Cartridge schematics and PCB design (c) 2013-2014 Marko Šolajić # E-mails: zzarko and msolajic at gmail # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import division VERSION="3.0" import copy import array import os import glob import sys import re if sys.version_info >= (3, 0): import configparser else: import ConfigParser as configparser #commodore 64 screen codes SCR_CODES="""@abcdefghijklmnopqrstuvwxyz[|]|| !"#$%&'()*+,-./0123456789:;<=>?|ABCDEFGHIJKLMNOPQRSTUVWXYZ""" SCR_DICT={SCR_CODES[pos]:pos for pos in range(len(SCR_CODES))} #commodore 64 petascii codes PETASCII_CODES="""0123456789|||||||abcdefghijklmnopqrstuvwxyz||||<|ABCDEFGHIJKLMNOPQRSTUVWXYZ""" PETASCII_DICT={PETASCII_CODES[pos]:pos+48 for pos in range(len(PETASCII_CODES))} MAX_MENU_ITEMS = 10+26*2 MAX_MENUS = 8 #cartridge size (64/128/256/512k/1024K, 0 - autosize) cart_sizek = 0 cart_size = cart_sizek*1024 #directory for prg files prg_directory = "prg" #cartridge menu program cart_menu_file = "menu" #finished cartridge bin file cart_file_name = "compilation.bin" #order of programs in CFG file cart_order = [] #border color after program select color_border = 14 #background color after program select color_background = 6 #character color after program select color_character = 14 #default menu help text cart_help = "(Shift)CRSR: Scroll, Fn/Ret: Menu select" #should menu wave cart_wave = 1 #should menu have ROSS cartridge sound cart_sound = 0 #returns array of screen codes def to_screen(s): return [SCR_DICT[x] for x in s] #returns the size of an open file def flen(f): return os.fstat(f.fileno()).st_size #adds one byte to array def append_byte(arr,byte): arr.append(byte & 0xff) #adds two bytes to array (lo/hi) def append_word(arr,word): arr.append(word & 0xff) arr.append(word >> 0x08) def append_text(bytearray, text, zero=False): text = to_screen(text) for c in text: append_byte(bytearray, c) if zero: append_byte(bytearray, 0) def center_text(text, width=40): l = len(text) pos = (40 - l) // 2 text = " "*pos + text + " "*(width-pos-len(text)) return text def error(s, usg=False): print("\nERROR!! {0}\n".format(s)) if (usg): usage(1) else: exit(1) def genitem(name, prg="", run=0, load=0, length=0, data=0): return {'name':name[:31], 'prg':prg, 'run':run, 'load':load, 'len': length, 'data':data, 'link': None} def genmenu(title, order=None, spacing=0, width=0, height=0, x=0, y=0): return {'title':center_text(title), 'order':order, 'spacing':spacing, 'items':[], 'width':width, 'height':height, 'x':x, 'y':y, 'offset':0} def printmenus(): print("\nMENUS:") for menuid in sorted(menus): menu = menus[menuid] print("MENU",menuid,menu['title'],"LEN",len(menu['items']),"W",menu['width'],"H",menu['height']) for i,item in enumerate(menu['items']): print(" ITEM",i,item['name'],"LINK",item['link']) def usage(code=0): print("""Usage: python {0} - scans the prg directory and places all prg files on cartridge python {0} (with or without extension) - reads cartridge configuration from CFG file All programs for cartridge must be in prg format! Create prg directory if it doesn't exist and place all prg files for cartridge inside. To make specific program order, you can add NNN_ prefix to prg files (N - 0-9). This prefix will be excluded from name in menu. Or, you can use CFG file to define cartridge. Sample CFG file is provided with installation. Generated bin file can be burned to Magic Desk compatibile eprom/cartridge, or converted to crt with VICE's cartconv: cartconv -t md -i -o """.format(sys.argv[0])) exit(code) print(u"\nMagic Desk Cartridge Generator v%s\n(c) 2013-2019 Žarko Živanov" % VERSION) print(u"\nMagic Desk Compatible Cartridge \n(c) 2013-2019 Marko Šolajić\n") menus = {} programs = {} prgs_found = 0 if len(sys.argv) > 2: usage() if len(sys.argv) > 1: #read configuration from cfg file cfgfile = sys.argv[1] if cfgfile[-4:] != ".cfg" and cfgfile[-4:] != ".CFG": cfgfile += ".cfg" if os.access(cfgfile, os.F_OK): print("\nReading configuration file %s ..." % cfgfile) if sys.version_info >= (3, 0): cfg = configparser.ConfigParser() else: cfg = configparser.SafeConfigParser() cfg.read(cfgfile) programs = cfg.sections() if cfg.has_section("cartridge"): print("Reading cartridge section ...") if cfg.has_option("cartridge","bin"): cart_file_name = cfg.get("cartridge","bin") if cart_file_name[-4:] != ".bin" and cart_file_name[-4:] != ".BIN": cart_file_name += ".bin" if cfg.has_option("cartridge","size"): cart_sizek = cfg.getint("cartridge","size") if cart_sizek not in [64,128,256,512,1024]: error("Wrong cartridge size, must be 64,128,256,512 or 1024") if cfg.has_option("cartridge","menu"): cart_menu_file = cfg.get("cartridge","menu") if cart_menu_file[-4:] == ".prg" or cart_menu_file[-4:] == ".PRG": cart_menu_file = cart_menu_file[:-4] if cfg.has_option("cartridge","border"): color_border = cfg.getint("cartridge","border") if color_border not in range(16): error("Border color must be 0-15") if cfg.has_option("cartridge","background"): color_background = cfg.getint("cartridge","background") if color_background not in range(16): error("Background color must be 0-15") if cfg.has_option("cartridge","character"): color_character = cfg.getint("cartridge","character") if color_character not in range(16): error("Character color must be 0-15") if cfg.has_option("cartridge","help"): cart_help = cfg.get("cartridge","help") if len(cart_help) > 40: error("Cartridge help text must be 40 characters or less") if cfg.has_option("cartridge","wave"): cart_wave = int(cfg.get("cartridge","wave")) if cfg.has_option("cartridge","sound"): cart_sound = int(cfg.get("cartridge","sound")) programs.remove("cartridge") for m in range(1,MAX_MENUS+1): menu = "menu%d" % m if cfg.has_section(menu): print("Reading menu %d section ..." % m) title = "" order = ["{:03d}".format(i) for i in range(100*m,100*m+100)] spacing = width = height = x = y = 0 if cfg.has_option(menu,"order"): order = cfg.get(menu,"order") order = order.split(",") order = [ "%d%s" % (m, x) for x in order] if cfg.has_option(menu,"title"): title = cfg.get(menu,"title") if len(title) > 40: error("Menu %d title must be 40 characters or less" % m) if cfg.has_option(menu,"spacing"): spacing = int(cfg.get(menu,"spacing")) if cfg.has_option(menu,"width"): width = int(cfg.get(menu,"width")) if cfg.has_option(menu,"height"): height = int(cfg.get(menu,"height")) if cfg.has_option(menu,"x"): x = int(cfg.get(menu,"x")) if cfg.has_option(menu,"y"): y = int(cfg.get(menu,"y")) menus["%d" % m] = genmenu(title, order, spacing, width, height, x, y) programs.remove(menu) if len(menus) == 0: order = ["{:03d}".format(i) for i in range(100,200)] menus['1'] = genmenu("",order,0) for menuid in sorted(menus): menu = menus[menuid] print("Menu %s programs:" % menuid) for prgno in menu['order']: prgsec = "prg"+prgno if not cfg.has_section(prgsec): continue prgname = cfg.get(prgsec,"file") if prgname[-4:] == ".prg" or prgname[-4:] == ".PRG": prgname = prgname[:-4] if cfg.has_option(prgsec,"name"): name = cfg.get(prgsec,"name") else: name = prgname[:-4].replace("_"," ") if cfg.has_option(prgsec,"run"): runaddr = cfg.get(prgsec,"run") if runaddr[0] == '$': runaddr = runaddr.replace("$","0x") runaddr = int(runaddr,0) else: runaddr = 0 menu['items'].append(genitem(name,prgname,runaddr)) print(" %s" % name) programs.remove(prgsec) prgs_found += 1 else: error("CFG file '{0}' not found.".format(cfgfile), True) if len(menus) == 0 or prgs_found == 0: #collect all prg files in prg directory print("\nReading prg files from prg directory ...") prgList = glob.glob( os.path.join(prg_directory, '*.[pP][rR][gG]') ) if len(prgList) == 0: error("No prg files found. Make sure to place them in prg directory.", True) prefix = re.compile("\A([0-9])([0-9]?[0-9]?)_(.*)") suffix = re.compile("(.*)_([0-9]+|0[xX][0-9a-fA-F]+)\Z") prgList.sort() print("Found prg files:") for prg in prgList: prgname = os.path.basename(prg)[:-4] regmatch = prefix.match(prgname) if regmatch: name = regmatch.group(3) menuid = regmatch.group(1) else: name = prgname menuid = '1' regmatch = suffix.match(name) if regmatch: name = regmatch.group(1) run = regmatch.group(2) if run[:2] == "0x": run = int(run, 16) else: run = int(run) else: run = 0 print(" Menu %s: %s" % (menuid,name)) name = name.replace("_"," ") if not menuid in menus: menus[menuid] = genmenu("") menus[menuid]['items'].append(genitem(name,prgname,run)) if len(programs) > 0: print("\nFollowing program sections were unused in cartridge:") for prgsec in sorted(programs): print(" [%s] %s" % (prgsec, cfg.get(prgsec,"file")) ) #list of prg names, used to detect repeated programs load_list = [] #printmenus() #load all programs names_len = 0 for menuid in sorted(menus): menu = menus[menuid] menu['items'].append(genitem("Basic","",0xfce2)) items_no = 0 for i,item in enumerate(menu['items']): if item['prg'] == "": continue #check if the same file was already loaded found = None for ii,ll in enumerate(load_list): if item['prg'] == ll: found = ii break item['link'] = found prgfile=os.path.join(prg_directory,item['prg']) if os.access(prgfile+".PRG", os.F_OK): prgfile += ".PRG" else: prgfile += ".prg" prg = open(prgfile,"rb") temp = array.array('B') temp.fromfile(prg,flen(prg)) addr = temp.pop(0) + 256*temp.pop(0) item['load'] = addr if item['run'] == 0: # if there is no run address item['run'] = addr item['len'] = temp.buffer_info()[1] item['data'] = temp items_no += 1 load_list.append(item['prg']) if items_no > MAX_MENU_ITEMS: error("Cartridge menu %s can have max %d programs, but %d supplied." % (menuid, MAX_MENU_ITEMS, items_no)) width = menu['width'] if width == 0: width = min( max( [ len(x['name'])+3 for x in menu['items'] ] ), 34 ) else: width = min(menu['width'],34) menu['width'] = width height = menu['height'] cheight = min( 20 if menu['spacing'] == 0 else 10, len(menu['items']) ) if height == 0: height = cheight else: height = min(menu['height'],20 if menu['spacing'] == 0 else 10, cheight) menu['height'] = height x = menu['x'] if x == 0: x = (40 - width - 3 - 3) // 2 + 1 #3 for borders, 3 for key display else: x = max(menu['x'],1) menu['x'] = x y = menu['y'] if y == 0: y = (25 - height - menu['spacing']*(height-1) - 3) // 2 #3 for borders else: y = max(menu['y'],1) menu['y'] = y menu['offset'] = x + 40*y #length of all menu names nameslen = sum(len(x['name'])+1 for x in menu['items']) #+1 for null character menu['itemsoffset'] = names_len names_len += nameslen #printmenus() print("\nAssembling cartridge file ...") #open cart menu file if os.access(cart_menu_file+".PRG", os.F_OK): cart_menu_file += ".PRG" else: if os.access(cart_menu_file+".prg", os.F_OK): cart_menu_file += ".prg" else: error("cartridge menu program '%s' not found." % cart_menu_file) cart_prg = open(cart_menu_file,"rb") #skip start address cart_prg.seek(2) cart_file = array.array('B') cart_file.fromfile(cart_prg,flen(cart_prg)-2) cart_prg.close() #number of menus menus_no = len(menus) menunamesoffset = 2+5+8*(1+2+1+1+1+2+2)+40 menuitemsoffset = menunamesoffset + menus_no*40 #from ProgramTable to last menu item text menudatasize = menuitemsoffset + names_len #9 bytes for prgtable per item tblsize = 9*sum( [ len(menus[menuid]['items']) for menuid in menus ] ) #calculate table addresses table_data = array.array('B') bank = 0 menuprglen = cart_file.buffer_info()[1] #address of first program inside cartridge memory, starting at 0 crtaddress = menuprglen + menudatasize + tblsize #address of first program inside C64 memory, starting at 0x8000 address = crtaddress + 0x8000 #start address of program table inside C64 memory tbladdress = menuprglen + menudatasize + 0x8000 menunamesaddress = menuprglen + menunamesoffset + 0x8000 menuitemsaddress = menunamesaddress + menus_no*40 if crtaddress > 0x2000: error("Program data has %d bytes, %d is the maximum.\nShorten program names or number of programs." % (crtaddress, 0x2000) ) #list of program table entries, used for repeated programs tbl_list = [] print("\nCartridge memory map:\n%31s located at $%06x" % (cart_menu_file, 0)) print("%31s located at $%06x" % ("menu data",menuprglen)) print("%31s located at $%06x\n" % ("program table",tbladdress-0x8000)) for menuid in sorted(menus): menu = menus[menuid] for i,item in enumerate(menu['items']): prg_data = array.array('B') if item['link'] != None: print("%31s linked to previous instance" % item['name']) prg_data = tbl_list[item['link']] table_data.extend(prg_data) tbl_list.append(prg_data) else: append_byte(prg_data,bank) #bank, 1 byte append_word(prg_data,address) #address in bank, 2 bytes if item['prg'] != "": #length, 2 bytes print("%31s located at $%06x, run address:" % (item['name'],crtaddress), end=" ") length = item['data'].buffer_info()[1] else: length = 0 append_word(prg_data,length) append_word(prg_data,item['load']) #load address, 2 bytes if item['run'] == 2049: #run address, 2 bytes (0 - BASIC RUN) append_word(prg_data,0) print("BASIC RUN") else: append_word(prg_data,item['run']) if item['prg'] != "": print("$%04x" % item['run']) table_data.extend(prg_data) tbl_list.append(prg_data) address += length #next program address crtaddress += length while address > 0x9fff: address -= 0x2000 bank += 1 #assemble cartridge #for details about various fields in here, check C64 assembler source append_word(cart_file,tbladdress) #program table address append_byte(cart_file,color_border) #border color append_byte(cart_file,color_background) #background color append_byte(cart_file,color_character) #character color append_byte(cart_file,cart_wave) #menu waving append_byte(cart_file,cart_sound) #menu sound for menuid in sorted(menus): #menu_items_no append_byte(cart_file,len(menus[menuid]['items'])) for i in range(8-len(menus)): append_byte(cart_file,0) for menuid in sorted(menus): #menu_offset append_word(cart_file,menus[menuid]['offset']) for i in range(8-len(menus)): append_word(cart_file,0) for menuid in sorted(menus): #menu_width append_byte(cart_file,menus[menuid]['width']) for i in range(8-len(menus)): append_byte(cart_file,0) for menuid in sorted(menus): #menu_height append_byte(cart_file,menus[menuid]['height']) for i in range(8-len(menus)): append_byte(cart_file,0) for menuid in sorted(menus): #menu_spacing append_byte(cart_file,menus[menuid]['spacing']) for i in range(8-len(menus)): append_byte(cart_file,0) for idx, menuid in enumerate(sorted(menus)): #menu_names append_word(cart_file,menunamesaddress + 40*idx) for i in range(8-len(menus)): append_word(cart_file,0) for menuid in sorted(menus): #menu_items append_word(cart_file,menuitemsaddress+menus[menuid]['itemsoffset']) for i in range(8-len(menus)): append_word(cart_file,0) append_text(cart_file, center_text(cart_help)) for menuid in sorted(menus): append_text(cart_file, menus[menuid]['title']) for menuid in sorted(menus): for item in menus[menuid]['items']: append_text(cart_file, item['name'], True) cart_file.extend(table_data) for menuid in sorted(menus): for item in menus[menuid]['items']: if (item['prg'] == "") or (item['link']): continue cart_file.extend(item['data']) length = cart_file.buffer_info()[1] #calculate size if set to 0 if cart_sizek == 0: cart_sizek = 64 while (cart_sizek < 1024) and (length > cart_sizek*1024): cart_sizek *= 2 cart_size = cart_sizek*1024 if length > cart_size: larger = length - cart_size error("\nCartridge is %d bytes (%d blocks) larger than it should be." % (larger, larger // 254)) #pad to cartridge size print("\nCartridge size %dk" % cart_sizek, end=" ") if length < cart_size: unused = cart_size - length temp = array.array('B',[0xff]*unused) cart_file.extend(temp) print(", unused %d bytes / %d block(s)" % (unused , unused // 254)) else: print("") #write cartridge to file cartridge = open(cart_file_name,"wb") cart_file.tofile(cartridge) cartridge.close() print("\nDone! Cartridge saved as '{0}'".format(cart_file_name)) print("\nIf needed, you can convert it to Magic Desk crt with cartconv from VICE:") print(" cartconv -t md -i %s -o %s.crt\n" % (cart_file_name,cart_file_name[:-4]) )