aboutsummaryrefslogtreecommitdiff
path: root/cart2xex.c
diff options
context:
space:
mode:
Diffstat (limited to 'cart2xex.c')
-rw-r--r--cart2xex.c583
1 files changed, 583 insertions, 0 deletions
diff --git a/cart2xex.c b/cart2xex.c
new file mode 100644
index 0000000..4a8acb2
--- /dev/null
+++ b/cart2xex.c
@@ -0,0 +1,583 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+
+#include "cart.h"
+#include "get_address.h"
+#include "loadscreen_bin.h"
+
+/*
+ cart2xex 20061018 bkw
+ updated 20070524 bkw
+ added atari800 CART format 20071227 bkw
+
+ Quick & dirty utility to convert Atari 8-bit cartridge images
+ to Atari DOS binary load files (suitable for use with DOS the 2.0S
+ "L" command, or your favorite 3-sector loader, etc).
+
+ Works on non-bankswitched 8K or 16K cart images. Output is:
+ - Segment containing code that prints LOADING (suppress with -n)
+ - 6-byte Atari binary load header
+ - The entire input
+ - 4-byte header, followed by the init address (total 6 bytes)
+ - 4-byte header, followed by the run address (total 6 bytes)
+
+ Without the LOADING screen, the resulting binary is 18 bytes
+ larger than the input.
+
+ If the cart image isn't exactly 8192 or 16384 bytes long, an error
+ is printed, and the program exits with status 1. Otherwise, exit
+ status is 0 (success).
+
+ Future options (stuff I might add, if there's any interest):
+
+ - ATR image output support (image would consist of a 3-sector
+ loader plus the binary)... not really necessary: can use "makeatr"
+ to make a k-file if you really want.
+
+ - Simple algorithm to detect STA/STX/STY instructions that write
+ to the ROM address space. If I were going to copy-protect a cart,
+ to keep people from dumping it to a bin, I'd write to what's supposed
+ to be ROM, then read it back. If I got back what I wrote there, it
+ means the "ROM" is actually RAM, so I'd refuse to play the game.
+ Parker Bros. Frogger does this.
+ The trouble is, a *simple* algorithm will probably fail. Several
+ self-destructing carts I've dealt with use STA ($00),Y to write to
+ ROM... meaning I basically need a complete emulator. Atari++ has a
+ nice debugger with conditional breakpoints, maybe there's a way to
+ hook into that?
+
+ - Documentation for all this crap, if I really add it :)
+*/
+
+#ifndef VERSION
+#define VERSION "???"
+#endif
+
+#define SELF "cart2xex"
+
+#define BANNER \
+ SELF " v" VERSION " - by B. Watson (WTFPL)\n"
+
+#define MAX_CART_SIZE (16384 + 16)
+
+char *usage =
+ "\nUsage: " SELF " [-cdhilnprR] [-t title] infile.rom [outfile.xex]\n"
+ " -c Check only; do not create xex file\n"
+ " -d Don't include code to reboot the Atari when RESET is pressed\n"
+ " -h Print this help\n"
+ " -i addr Force init address to addr (0 = no init address)\n"
+ " -r addr Force run address to addr (0 = no run address)\n"
+ " -l addr Force load address to addr\n"
+ " -R ROM is right cartridge image (same as -l0x8000)\n"
+ " -p pages Reserve # of pages below RAMTOP\n"
+ " -n Do not include code for LOADING screen\n"
+ " -t title Set title for LOADING screen (default: outfile)\n";
+
+int loadscreen = 1; /* true if we're going to prepend a title screen */
+int checkonly = 0; /* true if we're not writing an output file */
+int reserve_pages = 0; /* number of extra pages of RAM to reserve */
+int reboot_reset = 1; /* true if RESET key causes a reboot */
+
+/* buffer[] will contain the cart image after it's loaded, which may
+ or may not include a 16-byte CART header. rom will point to the start
+ of the actual ROM data (buffer or buffer+16) */
+unsigned char buffer[MAX_CART_SIZE];
+unsigned char *rom;
+
+/* warn() and die() are like the Perl functions of the same names, kind of */
+void warn(char *msg) {
+ if(msg) fprintf(stderr, "%s\n", msg);
+}
+
+void die(char *msg) {
+ warn(msg);
+ exit(1);
+}
+
+/* cart_image_valid() looks at an atari800-style CART header and returns
+ true if the cartridge type is 8K or 16K non-bankswitched */
+int cart_image_valid(unsigned char *cart) {
+ int cart_type;
+
+ cart_dump_header(cart, 0);
+
+ cart_type = get_cart_type(cart);
+
+ /* magic numbers: types 1 and 2 are "Standard 8K" and "Standard 16K",
+ type 21 is "Standard 8K right slot", which are the only types we
+ can support. */
+ if(cart_type != 1 && cart_type != 2 && cart_type != 21)
+ return 0;
+
+ /* bad cksum is not a fatal error */
+ if(cart_checksum_ok(cart))
+ fprintf(stderr, "CART checksum: 0x%08x (OK)\n", get_cart_checksum(cart));
+ else
+ warn("Warning: CART checksum invalid, file may be corrupt");
+
+ return 1;
+}
+
+/* For now, all the work is done in main() */
+int main(int argc, char **argv) {
+ int maxres, c, size = 0;
+ unsigned char init_lo, init_hi, run_lo, run_hi, option, present, ramtop_adj;
+ int runadr, initadr;
+ int force_init = -1, force_run = -1, force_load = -1;
+ char *infile = "-", outfile[4096] = "-";
+ FILE *in = stdin, *out = stdout;
+ char *title = NULL;
+
+ puts(BANNER);
+
+ argc--; argv++;
+
+ while(argc && *argv[0] == '-') {
+ switch(argv[0][1]) {
+ case 't':
+ if(argv[0][2]) {
+ title = &argv[0][2];
+ } else {
+ argc--; argv++;
+ if(argc) {
+ title = &argv[0][0];
+ } else {
+ die(usage);
+ }
+ }
+ break;
+
+ case 'p':
+ if(argv[0][2]) {
+ reserve_pages = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ reserve_pages = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(reserve_pages < 0)
+ exit(1);
+ break;
+
+ case 'R':
+ force_load = 0x8000;
+ break;
+
+ case 'l':
+ if(argv[0][2]) {
+ force_load = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_load = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_load < 0)
+ exit(1);
+ break;
+
+ case 'i':
+ if(argv[0][2]) {
+ force_init = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_init = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_init < 0)
+ exit(1);
+ break;
+
+ case 'r':
+ if(argv[0][2]) {
+ force_run = get_address(NULL, &argv[0][2]);
+ } else {
+ argc--; argv++;
+ if(argc) {
+ force_run = get_address(NULL, argv[0]);
+ } else {
+ die(usage);
+ }
+ }
+ if(force_run < 0)
+ exit(1);
+ break;
+
+ case 'c':
+ checkonly = 1;
+ break;
+
+ case 'n':
+ loadscreen = 0;
+ break;
+
+ case 'd':
+ reboot_reset = 0;
+ break;
+
+ case 'h':
+ printf(usage);
+ exit(0);
+ break;
+
+ default:
+ fprintf(stderr, "Unrecognized option '-%c'\n", argv[0][1]);
+ die(usage);
+ break;
+ }
+
+ argc--; argv++;
+ }
+
+ if(argc) {
+ infile = *argv;
+ argc--; argv++;
+ }
+
+ if(argc) {
+ strcpy(outfile, *argv);
+ argc--; argv++;
+ } else if(!checkonly && strcmp(infile, "-") != 0) {
+ char *p;
+
+ strcpy(outfile, infile);
+ p = strrchr(outfile, '.');
+ if(!p) p = outfile + strlen(outfile);
+ *p = '\0';
+ strcat(outfile, ".xex");
+ fprintf(stderr, "- Output is '%s'\n", outfile);
+
+ if(strcmp(infile, outfile) == 0)
+ die("Input and output filenames are the same, aborting!\n");
+ }
+
+ if(argc)
+ die(usage);
+
+ if(strcmp(infile, "-") != 0) {
+ if( !(in = fopen(infile, "rb")) ) {
+ perror(infile);
+ exit(1);
+ }
+ }
+
+ while( (c = getc(in)) != EOF && size < MAX_CART_SIZE) {
+ buffer[size++] = c;
+ }
+
+ if(has_cart_signature(buffer)) {
+ int header_size;
+
+ warn("Image type: Atari800 CART format");
+ if(!cart_image_valid(buffer))
+ die("Unsupported/invalid CART image\n"
+ "(only 8K/16K non-banked images are supported)");
+
+ if(get_cart_type(buffer) == 21) {
+ /* right cartridge */
+ force_load = 0x8000;
+ warn("Forcing load address to $8000 for right-slot cartridge");
+ }
+
+ header_size = get_cart_size(buffer);
+ rom = buffer + 16;
+ size -= 16;
+
+ if(size != header_size)
+ die("CART header says image should be %d bytes, but only %d bytes "
+ "are in the image! Corrupt/damaged CART image?");
+ } else {
+ warn("Image type: Raw dump");
+ rom = buffer;
+ }
+
+ if( !(c == EOF && (size == 8192 || size == 16384)) ) {
+ if(rom[0] == 0xff && rom[1] == 0xff) {
+ warn("This looks like an Atari binary load file!");
+ } else if(rom[0] == 0x00 && rom[1] == 0x00) {
+ warn("This looks like an Atari BASIC program!");
+ }
+
+ die("Cartridge size must be 8192 or 16384 bytes\n"
+ "(Are you sure this is a valid Atari 8-bit cartridge image?)");
+ }
+
+ /* All this info comes from Mapping the Atari, originally by
+ Ian Chadwick, now available for free at
+ http://www.atariarchives.org/mapping/
+ */
+
+ /* Assume we're using a Left Cartridge. Not many Right Cartridges
+ were ever made... */
+ run_lo = rom[size-6]; /* $BFFA */
+ run_hi = rom[size-5]; /* $BFFB */
+ present = rom[size-4]; /* $BFFC */
+ option = rom[size-3]; /* $BFFD */
+ init_lo = rom[size-2]; /* $BFFE */
+ init_hi = rom[size-1]; /* $BFFF */
+
+ /* Some helpful messages... decode the cart options */
+ if(present) { /* $BFFC */
+ fprintf(stderr,
+ "Hmm, this cart image has $%02X in the \"cartridge present\" "
+ "byte at $BFFC\n"
+ "(should be $00). "
+ "Are you sure it's really a valid image?\n",
+ present);
+ }
+
+ fprintf(stderr, "Image size: %dK (addresses $%04X - $%04X)\n",
+ size/1024, 49152-size, 49152-1);
+ fprintf(stderr, "Run address: $%02X%02X\n", run_hi, run_lo);
+ fprintf(stderr, "Init address: $%02X%02X\n", init_hi, init_lo);
+ fprintf(stderr, "Option byte: $%02X\n", option);
+
+ /* $BFFD, bits 0, 2, 7 */
+ fprintf(stderr, "- Cartridge does %sallow disk boot\n",
+ (option & 1) ? "" : "NOT ");
+
+ fprintf(stderr, "- Cartridge gets initialized, %s\n",
+ (option & 4) ?
+ "then started normally" :
+ "but NOT started (weird)");
+
+ fprintf(stderr, "Cartridge type: %s\n",
+ (option & 128) ?
+ "diagnostic (output binary may not run correctly!)" :
+ "normal (non-diagnostic)");
+
+ if(checkonly)
+ exit(0);
+
+ if(force_load >= 0) {
+ fprintf(stderr, "-l option: Load address forced to $%04X\n", force_load);
+ }
+
+ if(force_init >= 0) {
+ init_lo = rom[size-2] = force_init & 0xff;
+ init_hi = rom[size-1] = (force_init >> 8) & 0xff;
+ fprintf(stderr, "-i option: Init address forced to $%04X\n", force_init);
+ }
+
+ if(force_run >= 0) {
+ run_lo = rom[size-6] = force_run & 0xff;
+ run_hi = rom[size-5] = (force_run >> 8) & 0xff;
+ fprintf(stderr, "-r option: Run address forced to $%04X\n", force_run);
+ }
+
+ runadr = run_lo + (run_hi << 8);
+ if(runadr && (runadr < 49152-size || runadr >= 49152))
+ warn("Warning: Run address not in cartridge address range");
+
+ initadr = init_lo + (init_hi << 8);
+ if(initadr && (initadr < 49152-size || initadr >= 49152))
+ warn("Warning: Init address not in cartridge address range");
+
+ if(size == 8192 && force_load < 0) {
+ if(runadr >= 0x8000 && runadr < 0xa000)
+ warn("This may be a \"right cartridge\" ROM, try -R?");
+ }
+
+ /* Whew! Finally all checks are done, we can open the output file */
+ if(strcmp(outfile, "-") != 0) {
+ if( !(out = fopen(outfile, "wb")) ) {
+ perror(outfile);
+ fclose(in);
+ exit(1);
+ }
+
+ if(!title) {
+ char *p;
+
+ title = outfile;
+
+ /* remove directory if present */
+ p = strrchr(title, '/');
+ if(p) title = p + 1;
+
+ /* remove extension if present */
+ p = strrchr(title, '.');
+ if(p) *p = '\0';
+ }
+
+ }
+
+ if(isatty(fileno(out))) {
+ warn("\nStandard output is a terminal, not writing binary data.\n"
+ "Either redirect to a file or set the output filename.");
+ die(usage);
+ }
+
+ maxres = ((0xc000 - size) - 0x7000) >> 8;
+ if(reserve_pages > maxres) {
+ fprintf(stderr, "Can't reserve %d pages below RAMTOP. Maximum "
+ "reserved pages is %d for a %dK ROM.\n",
+ reserve_pages, maxres, size/1024);
+ exit(1);
+ }
+
+ if(reserve_pages) {
+ fprintf(stderr, "Reserving %d pages below RAMTOP\n", reserve_pages);
+ }
+
+ ramtop_adj = (size >> 8) + reserve_pages;
+
+ /* Write the loading screen object code, unless suppressed */
+ if(loadscreen) {
+ int i;
+ char c, d;
+
+ /* loadscreen.bin adjusts RAMTOP to make room for the cart image,
+ so it needs to know the size. */
+ loadscreen_bin[6] = ramtop_adj;
+
+ if(title) {
+ /* WARNING: magic number here! */
+ /* Look for "Title offset: loadscreen_bin_len - $xx"
+ in dasm output. */
+ int offset = loadscreen_bin_len - 27;
+ int len = strlen(title);
+
+ if(len > 20) {
+ fprintf(stderr, "Title > 20 characters, truncating\n");
+ len = 20;
+ }
+ offset += 10-len/2;
+
+ fprintf(stderr, "- Setting title to \"");
+ for(i=0; i<len; i++) {
+ c = title[i];
+
+ c = tolower(c);
+ d = c;
+
+ if(c >= 33 && c <= 63) {
+ c -= 32;
+ } else if(c >= 64 && c <= 95) {
+ c += 32;
+ } else if(!isalnum(c)) {
+ d = c = 32;
+ }
+ fputc(d, stderr);
+
+ loadscreen_bin[offset+i] = c;
+ }
+
+ fputc('"', stderr);
+ fputc('\n', stderr);
+ } else {
+ fprintf(stderr,
+ "No -t and no output filename, not setting load screen title\n");
+ }
+
+ /* loadscreen_bin already includes $FF, $FF header */
+ for(i=0; i<loadscreen_bin_len; i++)
+ putc(loadscreen_bin[i], out);
+ } else {
+ /* $FF, $FF binary load header. All Atari executables must have this. */
+ putc(0xff, out);
+ putc(0xff, out);
+
+ /* Without the title screen, we still need to set RAMTOP. Do it
+ by loading a 1-byte segment at $6A. */
+ putc(0x6a, out);
+ putc(0x00, out);
+ putc(0x6a, out);
+ putc(0x00, out);
+ putc(0xc0 - ramtop_adj, out); /* assume RAMTOP is 48K */
+ }
+
+ if(reboot_reset) {
+ /* make the Atari reboot when user presses RESET (POKE 580,1) */
+ putc(0x44, out); /* start addr LSB */
+ putc(0x02, out); /* start addr MSB */
+ putc(0x44, out); /* end addr LSB */
+ putc(0x02, out); /* end addr MSB */
+ putc(0x01, out); /* one byte of data */
+ }
+
+ /* Now write the Atari binary load file... Remember, the 6502 expects
+ addresses to be little-endian (LSB, then MSB) */
+
+ /* First segment: the code. */
+ /* Load address (LSB first). $8000 for a 16K cart, or $A000 for 8K,
+ or force_load if user used it */
+ if(force_load >= 0) {
+ putc(force_load & 0xff, out);
+ putc(force_load >> 8, out);
+ force_load += (size - 1);
+ putc(force_load & 0xff, out);
+ putc(force_load >> 8, out);
+ } else {
+ putc(0, out);
+ putc(size == 16384 ? 0x80 : 0xa0, out);
+ /* End address (LSB first). Always $BFFF */
+ putc(0xff, out);
+ putc(0xbf, out);
+ }
+
+ /* Image data */
+ for(c=0; c<size; c++)
+ putc(rom[c], out);
+
+ /* Only write init address if it's not 0 */
+ if(init_hi || init_lo) {
+ /* Next segment loads at INITAD ($2E2), contains the cart's init addr */
+ putc(0xe2, out);
+ putc(0x02, out);
+
+ /* Segment length is 2 bytes (ends at $2E3) */
+ putc(0xe3, out);
+ putc(0x02, out);
+
+ /* Init address will get loaded at $2E2 and $2E3 */
+ putc(init_lo, out);
+ putc(init_hi, out);
+ }
+
+ /* Only write run address if it's not 0 */
+ /* Next segment loads at RUNAD ($2E0), contains the cart's run address */
+ if(run_hi || run_lo) {
+ putc(0xe0, out);
+ putc(0x02, out);
+
+ /* Segment length is 2 bytes (ends at $2E1) */
+ putc(0xe1, out);
+ putc(0x02, out);
+
+ /* Run address will get loaded at $2E0 and $2E1 */
+ putc(run_lo, out);
+ putc(run_hi, out);
+ }
+
+ /* Check for I/O errors before closing files */
+ c = 0;
+ if(ferror(in)) {
+ fprintf(stderr, "%s: %s\n", infile, strerror(errno));
+ c = 1;
+ }
+
+ if(ferror(out)) {
+ fprintf(stderr, "%s: %s\n", outfile, strerror(errno));
+ c = 1;
+ }
+
+ /* Close 'em, we're done */
+ fclose(in);
+ fclose(out);
+
+ /* That's all, folks! */
+ exit(c);
+}