diff options
author | B. Watson <urchlay@slackware.uk> | 2022-08-29 16:11:13 -0400 |
---|---|---|
committer | B. Watson <urchlay@slackware.uk> | 2022-08-29 16:11:13 -0400 |
commit | e2ba8458a5cfdfacfaf103e7ba97d610afa6c970 (patch) | |
tree | cd665e602e6e2b636578a7d3d7894380605dafcc /cart2xex.c | |
download | bw-atari8-tools-e2ba8458a5cfdfacfaf103e7ba97d610afa6c970.tar.gz |
initial commit
Diffstat (limited to 'cart2xex.c')
-rw-r--r-- | cart2xex.c | 583 |
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); +} |