/* 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 DOS 2.0S, we end up back at the DUP menu... but SpartaDOS 3.2d locks up with a black screen. So don't suggest using this... */ void readstring(void) { char *result; memset(stringbuf, 0, STRINGBUF_SIZE); result = fgets(stringbuf, STRINGBUF_SIZE - 1, stdin); if(result == NULL) 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 /* 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) 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; } 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 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; /* 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)) return 0; } }