/* dla2csv.c - convert dla.xex save files to CSV. Rather bloated and slow by Atari standards. */ #include #include #include #include #include #include #include #include #include "dlaver.h" #define HEIGHT 170 #define WIDTH 256 /* cc65 doesn't fold constants, it won't let us do this: #define INBUF_SIZE (WIDTH * HEIGHT) ...it has to be a *constant*. */ #define INBUF_SIZE 5440 #define STRINGBUF_SIZE 256 /* cc65 doesn't "localize" \b to the Atari backspace character, so: */ #ifdef __ATARI__ #define BS '\x7e' #else #define BS '\b' #endif char inbuf[INBUF_SIZE]; char stringbuf[STRINGBUF_SIZE]; FILE *inf, *outf; /* We can't use string literal syntax, cc65 "helpfully" turns "\x0a" into an Atari 0x9b EOL character. Numeric constants are left alone. */ char a8eol[] = { 0x9b, 0x00, 0x00 }; char uxeol[] = { 0x0a, 0x00, 0x00 }; char mseol[] = { 0x0d, 0x0a, 0x00 }; char *eoltypes[][2] = { { "Atari ($9B)", a8eol }, { "Unix (\\n)", uxeol }, { "MS (\\r\\n)", mseol }, { NULL, NULL } }; /* On the Atari, we should show Atari-style error messages, including the familiar error number. Another reason to do this: on the Atari, perror() says "Bad file number" instead of "No space left on device", if the disk fills up due to fprintf(). On modern platforms, just use perror(). */ #ifdef __ATARI__ #define PERROR(x) atari_perror(x) void atari_perror(char *msg) { printf("%s: Error %d: %s\n", msg, _oserror, _stroserror(_oserror)); } #else #define PERROR(x) perror(x) #endif /* cc65 doesn't play nice with the Break key on the Atari. It works as expected during disk I/O, but if you press it while printing to stdout or reading from stdin, the program freezes. so call disable_break() at startup, then wrap disk I/O in enable_break() and disable_break(). */ #ifdef __ATARI__ char get_pokmsk(void) { return *((char *)0x10); } void set_pokmsk(char value) { *((char *)0x10) = *((char *)0xd20e) = value; } void enable_break() { set_pokmsk(get_pokmsk() | 0x80); } void disable_break() { set_pokmsk(get_pokmsk() & 0x7f); } #else void enable_break() { } /* no-op */ void disable_break() { } /* no-op */ #endif /* Read a string from stdin (E: on the Atari). Exit on EOF (or Break key on the Atari, except we disable it). On Linux, EOF is the ^D character, and this works. On the Atari, EOF is ^3... On DOS 2.0S, we end up back at the DUP menu... but SpartaDOS 3.2d locks up with a black screen. So don't document ctrl-3 as a way to exit the program (too bad), and just hope the user never presses it. */ void readstring(void) { char *result; memset(stringbuf, 0, STRINGBUF_SIZE); result = fgets(stringbuf, STRINGBUF_SIZE - 1, stdin); if(result == NULL) { enable_break(); exit(1); } } #ifdef __ATARI__ /* Show disk directory, in a 3-column layout. drive is an ASCII digit, e.g. 0x31 is drive 1. */ void show_dir(char drive) { static char dirspec[16]; char device = 'D'; DIR *dir; struct dirent *ent; int column = 0; if(drive == '0') { drive++; device = 'H'; } sprintf(dirspec, "%c%c:*.*", device, drive); if(!(dir = opendir(dirspec))) { dirspec[2] = '\0'; PERROR(dirspec); return; } while((ent = readdir(dir))) { printf("%-13s", ent->d_name); if(++column == 3) { column = 0; putchar('\n'); } } closedir(dir); if(column) putchar('\n'); } #endif int prompt_yn(char *prompt, int default_y) { char *yn = "y/N"; if(default_y) yn = "Y/n"; printf("%s[%s]? ", prompt, yn); fflush(stdout); readstring(); switch(stringbuf[0]) { case 'y': case 'Y': return 1; break; case 'n': case 'N': return 0; break; default: break; } return default_y; } /* Prompt for a filename, try to open it. If there's an error, show error message and retry. Will not return until it opens the file. */ FILE *prompt_filename(const char *name, const char *mode) { FILE *f = NULL; putchar('\n'); while(f == NULL) { printf("%s file: ", name); fflush(stdout); readstring(); stringbuf[strlen(stringbuf) - 1] = '\0'; /* kill trailing \n */ if(strlen(stringbuf) == 0) { if(prompt_yn("Exit program", 0)) { enable_break(); exit(0); } continue; } #ifdef __ATARI__ /* if the user enters a single digit, show directory of that drive. */ if(isdigit(stringbuf[0])) { show_dir(stringbuf[0]); continue; } /* if there's no device spec (D: or D1: etc), prepend D: */ if(!strchr(stringbuf, ':')) { memmove(stringbuf+2, stringbuf, strlen(stringbuf) + 1); stringbuf[0] = 'D'; stringbuf[1] = ':'; } #endif enable_break(); f = fopen(stringbuf, mode); disable_break(); if(!f) PERROR(stringbuf); } return f; } /* Prompt for and read EOL type, retry if needed. Will not return until a valid number was entered. */ char *prompt_eol(void) { int i; putchar('\n'); for(i = 0; eoltypes[i][0] != NULL; i++) { printf("%d:%s ", i + 1, eoltypes[i][0]); } putchar('\n'); i = -1; while(i == -1) { printf("Line ending type[1]? "); fflush(stdout); readstring(); if(stringbuf[0] == '\n') { i = 0; } else { i = stringbuf[0] - 49; /* ASCII 1-3 => 0-2 */ if(i < 0 || i > 2) i = -1; } } return eoltypes[i][1]; } void backspace3(void) { putchar(BS); putchar(BS); putchar(BS); } int main(int argc, char **argv) { char *inp, *eol; int i, bytes = 0, x, y, xmask, err; #ifdef __ATARI__ /* cc65's startup code turns off caps lock. turn it back on, since we're typing DOS filenames. in BASIC this would be: POKE 702,64 */ *((char *)0x02be) = 0x40; /* also, cc65 sets APPMHI to $bc1f (last byte before the GR.0 display list). which causes the atari to lock up when Reset is pressed. I can't believe this is useful behaviour... */ *((int *)0x0e) = 0; /* clear the screen */ putchar(0x7d); #endif printf("DLA to CSV converter v" VERSION ".\n"); #ifdef __ATARI__ printf("\n" "At any filename prompt, you may enter\n" "a number, to get a directory of that\n" "drive."); /* be nice to emulator users. 0x31a is HATABS. */ for(i = 0x31a; i < 0x340; i += 3) { if(*((char *)i) == 'H') { printf(" Use 0 for \"H:\"."); break; } } printf("\n\n"); #endif while(1) { disable_break(); err = 0; /* in case of short read on 2nd or later conversion: */ memset(inbuf, 0, INBUF_SIZE); /* read whole input file into memory */ inf = prompt_filename("Input DLA", "rb"); printf("Reading..."); fflush(stdout); enable_break(); bytes = fread(inbuf, 1, INBUF_SIZE, inf); disable_break(); if(bytes <= 0) { PERROR(stringbuf); continue; } printf("Read %d bytes.\n", bytes); /* warn if the file size is wrong... */ if(bytes < INBUF_SIZE) { printf("Warning: File too short!\n"); err = 1; } else if(fgetc(inf) != EOF) { printf("Warning: File too long!\n"); err = 1; } else { printf("File size is OK.\n"); } fclose(inf); /* a DLA file never has non-zero bytes at the beginning. */ for(i = 0; i < 32; i++) { if(inbuf[i]) { printf("Warning: File doesn't look like a DLA!\n"); err = 1; break; } } /* if anything looks wrong, we shouldn't proceed... but the user is boss, so give him the choice to shoot himself in the foot. */ if(err) { if(!prompt_yn("Try to convert anyway", 0)) continue; } eol = prompt_eol(); /* convert in a loop. in case of write error, this allows retry without re-reading the input file. */ i = err = 0; while(!i) { outf = prompt_filename("Output CSV", "wb"); #ifdef __ATARI__ printf("Press Ctrl-C to abort conversion.\n"); #endif printf("\nConverting... "); fflush(stdout); /* CSV file header row (column names) */ fprintf(outf, "x,y%s", eol); /* write output file one line at a time */ inp = inbuf; xmask = 0x80; bytes = 0; for(y = 0; y < HEIGHT; y++) { #ifdef __ATARI__ /* check for ^C */ if( *((char *)0x2fc) == 0x92 ) { printf("User abort!\n"); fclose(outf); remove(stringbuf); err = 1; *((char *)0x2fc) = 0xff; break; } #endif backspace3(); printf("%02d%%", y * 100 / HEIGHT); /* percentage */ fflush(stdout); for(x = 0; x < WIDTH; x++) { if(*inp & xmask) { if(fprintf(outf, "%d,%d%s", x, y, eol) < 0) { putchar('\n'); PERROR(stringbuf); err = 1; break; } bytes++; } xmask >>= 1; if(!xmask) { xmask = 0x80; inp++; } } if(err) break; } fclose(outf); if(err) { if(!prompt_yn("Conversion failed, try again", 1)) i = 1; } else { i = 1; backspace3(); printf("100%%\n%d particles.\n", bytes); } } if(!prompt_yn("\nConvert another file", 1)) break; } enable_break(); exit(0); }