/* dla2csv.c - convert dla.xex save files to CSV. Rather bloated and slow by Atari standards. */ #include #include #include #include #include #include #include #include #ifdef __CC65__ /* This will error out, if someone tries to compile for e.g. Apple or Commodore with cc65: */ #include #endif #include "dlaver.h" #define HEIGHT 170 #define WIDTH 176 #define BYTEWIDTH 22 /* 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 3740 #define STRINGBUF_SIZE 256 void print_id(void) { printf("DLA to CSV converter v" VERSION ".\n"); } 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 } }; char *eol; /* gets assigned one of the eoltypes */ /* using a table of masks instead of calculating them saves maybe 2s on the Atari. */ unsigned char masks[] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; #ifdef __ATARI__ /* Uncomment this to see elapsed time in convert() */ #define PROFILE /* cc65 doesn't "localize" \b to the Atari backspace character, like it does for \n. If we needed it: #define BS CH_DEL */ /* I like these colors. Also they match dla.xex. */ #define TEXT_COLOR 0x0e #define TEXT_BG_COLOR 0x90 /* 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(). */ #define PERROR(x) atari_perror(x) void atari_perror(char *msg) { printf("%s: Error %d: %s\n", msg, _oserror, _stroserror(_oserror)); } /* 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(). */ void enable_break() { OS.pokmsk = POKEY_WRITE.irqen = (OS.pokmsk | 0x80); } void disable_break() { OS.pokmsk = POKEY_WRITE.irqen = (OS.pokmsk & 0x7f); } char old_color1, old_color2; void restore_colors(void) { OS.color1 = old_color1; OS.color2 = old_color2; } void init_console(void) { /* 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 */ OS.shflok = 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... */ OS.appmhi = 0; /* clear the screen */ putchar(CH_CLR); /* save the old text & background colors */ old_color1 = OS.color1; old_color2 = OS.color2; /* Use my glorious and eye-catching color scheme :) */ OS.color1 = TEXT_COLOR; OS.color2 = TEXT_BG_COLOR; /* Put things back the way they were, when we exit. */ /* Nope. Using atexit() here makes MyDOS crash. Used atari_exit() and EXIT() macro instead. atexit(restore_colors); atexit(enable_break); */ } void print_banner(void) { unsigned char i; print_id(); printf("\n" "At any filename prompt, you may:\n" ); printf( "- Press Return, to exit this program.\n"); printf( "- Enter a number, to get a directory of\n" " that drive."); /* be nice to emulator users. */ for(i = 0; i < 12; i++) { if(OS.hatabs[i].id == 'H') { printf(" Use 0 for \"H:\"."); break; } } printf("\n"); } /* if the user enters a single digit, show directory of that drive. in a 3-column layout. this is a no-op on non-Atari. returns true if the user asked for a directory, false if not. */ char show_dir(void) { static char dirspec[16]; DIR *dir; struct dirent *ent; char device = 'D', drive = stringbuf[0]; unsigned char column = 0; if(!isdigit(drive)) return 0; if(drive == '0') { drive++; device = 'H'; } sprintf(dirspec, "%c%c:*.*", device, drive); if(!(dir = opendir(dirspec))) { dirspec[2] = '\0'; PERROR(dirspec); return 1; } errno = 0; while((ent = readdir(dir))) { printf("%-13s", ent->d_name); if(++column == 3) { column = 0; putchar('\n'); } } if(errno) PERROR(dirspec); closedir(dir); if(column) putchar('\n'); return 1; } void atari_exit(int status) { restore_colors(); enable_break(); exit(0); } #define EXIT(x) atari_exit(x) /* On the Atari, EOF is Ctrl-3. *Terrible* things happen if we ever get EOF on stdin on the Atari! The screen shows gibberish (display list gets mangled). Even calling exit() doesn't help. There's not really a way to avoid this (other than not using stdin, or hoping your users never press Ctrl-3), but we can avoid crashing the Atari by jumping directly to DOS (do not call the exit stuff from cc65's runtime, do not pass Go, do not collect $200). This seems to work reliably, but I hesistate to document "Press Ctrl-3 to exit"... ...and it turns out that on SpartaDOS X and other command-line DOSes, the display list doesn't get fixed: jmp (DOSVEC) just gives you the prompt, which expects the screen to still be usable... Sigh. Treat it as the Reset key instead. */ void exit_eof_stdin(void) { if(_is_cmdline_dos()) __asm__("jmp $e474"); /* cc65 doesn't define WARMSV for C */ else (*(OS.dosvec))(); } void backspace2(void) { OS.colcrs -= 2; } void print_converting(void) { printf("Press Ctrl-C to abort conversion.\n"); OS.crsinh = 1; printf("\nConverting... %%"); putchar(0xa0); backspace2(); } #define enable_cursor() OS.crsinh = 0 char user_abort(void) { return OS.ch == (KEY_C | KEY_CTRL); } void clear_keystroke(void) { OS.ch = KEY_NONE; } #define REGISTER register /* using utoa() rather than fprintf() saves ~12 sec on the Atari. */ void writepoint(unsigned char x, unsigned char y) { static char ubuf[10]; static char fbuf[16]; utoa(x, fbuf, 10); strcat(fbuf, ","); strcat(fbuf, utoa(y, ubuf, 10)); strcat(fbuf, eol); fputs(fbuf, outf); } /* if user entered a filename with no device spec (D: or D1: etc), prepend D1:. this is a no-op on non-Atari. */ void fix_filename(void) { if(!strchr(stringbuf, ':')) { memmove(stringbuf+3, stringbuf, strlen(stringbuf) + 1); stringbuf[0] = 'D'; stringbuf[1] = '1'; stringbuf[2] = ':'; } } #ifdef PROFILE #define start_profile_clock() OS.rtclok[0] = OS.rtclok[1] = OS.rtclok[2] = 0 #define print_profile_clock() \ printf("\nElapsed time: %ds\n", ((OS.rtclok[1] << 8) | OS.rtclok[2]) / 60) #else #define start_profile_clock() #define print_profile_clock() #endif #else /* non-Atari is assumed to be POSIX (and not something like Commodore or Apple II). */ #define BS '\b' void backspace2(void) { putchar(BS); putchar(BS); } #define PERROR(x) perror(x) #define print_banner() print_id() #define noop() #define enable_break() noop() #define disable_break() noop() #define enable_cursor() noop() #define init_console() noop() #define clear_keystroke() noop() #define start_profile_clock() noop() #define print_profile_clock() noop() #define fix_filename() noop() #define user_abort() 0 #define show_dir() 0 #define EXIT(x) exit(1) #define exit_eof_stdin() exit(1) #define print_converting() printf("\nConverting... %%") #define REGISTER /* since utoa() only exists on cc65, can't use it on non-Atari. */ void writepoint(unsigned char x, unsigned char y) { fprintf(outf, "%d,%d%s", x, y, eol); } #endif /* Read a string from stdin (E: on the Atari). Exit if we get EOF on stdin (e.g. ^D on Linux, ^3 on Atari). See exit_eof_stdin() comments above. */ void readstring(void) { memset(stringbuf, 0, STRINGBUF_SIZE); if(fgets(stringbuf, STRINGBUF_SIZE - 1, stdin) == NULL) exit_eof_stdin(); } 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)) { EXIT(0); } continue; } if(show_dir()) continue; fix_filename(); 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) { static int default_eoltype = 0; 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[%d]? ", default_eoltype + 1); fflush(stdout); readstring(); if(stringbuf[0] == '\n') { i = default_eoltype; } else { i = stringbuf[0] - 49; /* ASCII 1-3 => 0-2 */ if(i < 0 || i > 2) i = -1; } } default_eoltype = i; return eoltypes[i][1]; } /* check_dla() returns 1 if all is well, 0 if there's a problem. */ int check_dla(int bytes) { int i, ok = 1; /* warn if the file size is wrong... */ if(bytes < INBUF_SIZE) { printf("Warning: File too short!\n"); ok = 0; } else if(fgetc(inf) != EOF) { printf("Warning: File too long!\n"); ok = 0; } else { printf("File size is OK.\n"); } fclose(inf); /* a DLA file never has non-zero pixels in the first 3 columns */ for(i = 0; i < INBUF_SIZE; i += (WIDTH / 8)) { if(inbuf[i] & 0xe0) { printf("Warning: File doesn't look like a DLA!\n"); ok = 0; break; } } return ok; } int read_file(void) { int bytes; /* 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); } return bytes; } /* currently, on the 800 with atariserver, convert() takes: 5s for a DLA with no points (a file of zeroes). 16s for a DLA with 1000 points. 9s for the same DLA, with the fputs() in writepoint() commented out. */ int convert(void) { unsigned char x, y, j, pixx; REGISTER char *inp = inbuf; int particles = 0; outf = prompt_filename("Output CSV", "wb"); print_converting(); /* CSV file header row (column names) */ fprintf(outf, "x,y%s", eol); /* write output file one line at a time */ /* loop over all the pixels, by row, then column. this loop is rather slow. */ for(y = 0; y < HEIGHT; ++y) { if(user_abort()) { printf("\nUser abort!\n"); fclose(outf); remove(stringbuf); clear_keystroke(); return 0; } backspace2(); printf("%02d", y * 100 / HEIGHT); /* percentage */ fflush(stdout); for(x = 0; x < BYTEWIDTH; ++x) { if(*inp) { pixx = x * 8; for(j = 0; j < 8; ++j) { if(*inp & masks[j]) { writepoint(pixx + j, y); if(ferror(outf)) { PERROR(stringbuf); fclose(outf); remove(stringbuf); return 0; } particles++; } } } inp++; } } fclose(outf); enable_cursor(); return particles; } int main(int argc, char **argv) { int done, result = 0; init_console(); print_banner(); while(1) { disable_break(); result = read_file(); if(result <= 0) continue; printf("Read %d bytes.\n", result); /* 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(!check_dla(result)) { 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. */ done = 0; while(!done) { start_profile_clock(); result = convert(); if(result >= 0) { done = 1; backspace2(); printf("100%%\n%d particles.\n", result); print_profile_clock(); } else { if(!prompt_yn("Conversion failed, try again", 1)) done = 1; } } if(!prompt_yn("\nConvert another file", 1)) break; } EXIT(0); return 0; /* this never executes; it's here to shut up a warning */ }