#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

#ifdef __MSDOS__
#include <bios.h>
#endif

#define GET_BYTE(p,i)   (p[i])
#define GET_WORD(p,i)   (((word) p[i] << 8) | p[i+1])

#define MAX_FILES 5

#define V1 1
#define V2 2
#define V3 3
#define V4 4
#define V5 5
#define V6 6
#define V7 7
#define V8 8

#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

typedef unsigned char byte;
typedef unsigned short word;

typedef enum { APPLE, ATARI, COMMODORE, AMSTRAD, IBM, UNKNOWN } tsystem;

typedef struct {
       word offset;
       word length;
} tarea;

typedef int bool;

FILE *fpin[MAX_FILES] = { 0, 0, 0, 0, 0 }, *fpout = 0;

struct {
       const char *name;
       long size;
} file_info[MAX_FILES];

int area_count = 0;

tarea area[10];

byte header[64];

word checksum = 0;
bool checksum_valid = FALSE;

long filesize = 0L;

int hiscore = 0;

void cleanup (void) {

       int i;

       for (i = 0; i < MAX_FILES; i++)
               if (fpin[i] != NULL)
                       { fclose (fpin[i]); fpin[i] = NULL;     }

       if (fpout != NULL)
               { fclose (fpout); fpout = NULL; }

}/* cleanup */

void error (const char *message) {

       cleanup ();

       fprintf (stderr, "Error: %s\n", message);

       exit (EXIT_FAILURE);

}/* error */

void add_area (word offset, word length) {

       int i;

       for (i = 0; i < area_count; i++)
               if (area[i].offset > offset)
                       break;

       memmove (&area[i+1], &area[i], (area_count - i) * sizeof (tarea));

       area[i].offset = offset;
       area[i].length = length;

       area_count++;

}/* add_area */

void infile (int disk, long offset, byte *buf, int size) {

       fseek (fpin[disk], offset, SEEK_SET);

       if (!fread (buf, size, 1, fpin[disk]))
               error ("Cannot read input file");

}/* infile */

bool check_for_header (int disk, long offset) {

       long start_pc, high_mem;

       byte buf[64];
       byte version;

       int score = 0;
       int i, j;

       infile (disk, offset, buf, sizeof (buf));

       version = GET_BYTE(buf,0);

       if (version >= V1 && version <= V8)
               score += 750;

       if (GET_BYTE(buf,1) <= 0x07) /* Configuration bits 3 to 7 clear */
               score += 10;
       if (GET_WORD(buf,2) < 400) /* Release number < 400 */
               score += 10;

       high_mem = GET_WORD(buf,4);

       if (version == V6)
               start_pc = 4 * (long) GET_WORD(buf,6) + 8 * (long) GET_WORD(buf,40);
       else
               start_pc = GET_WORD(buf,6);

       if (high_mem > start_pc)
               high_mem = start_pc;

       if (GET_WORD(buf,8) <= high_mem) /* Dictionary addr <= High memory */
               score += 10;
       if (GET_WORD(buf,10) <= GET_WORD(buf,14)) /* Object addr <= Dynamic area */
               score += 10;
       if (GET_WORD(buf,12) <= GET_WORD(buf,14)) /* Globals <= Dynamic area */
               score += 10;
       if (GET_WORD(buf,14) <= high_mem) /* Dynamic area <= High memory */
               score += 10;
       if (GET_WORD(buf,16) < 0x0200) /* Flags bits 9 to 15 clear */
               score += 10;

       if (version == V1) {
               for (i = 0; i < 6; i++)
                       if (GET_BYTE(buf,18+i) == 0) /* Serial == 0 */
                               score += 2;
       } else {
               for (i = 0; i < 6; i++)
                       if (GET_BYTE(buf,18+i) >= 32 && GET_BYTE(buf,18+i) < 127) /* Serial in ASCII */
                               score += 2;
       }

       if (version == V1) {
               if (GET_WORD(buf,24) == 0) /* Abbreviations == 0 */
                       score += 10;
       } else {
               if (GET_WORD(buf,24) <= GET_WORD(buf,14)) /* Abbreviations <= Dynamic area */
                       score += 10;
       }

       if (version == V1 || version == V2) {
               if (GET_WORD(buf,26) == 0) /* File size == 0 */
                       score += 10;
               if (GET_WORD(buf,28) == 0) /* Checksum == 0 */
                       score += 10;
       } else if (version == V3) {
               if (GET_WORD(buf,26) != 0) {
                       if (2 * (long) GET_WORD(buf,26) > high_mem)
                               score += 20;
               } else {
                       if (GET_WORD(buf,28) == 0) /* Checksum == 0 */
                               score += 20;
               }
       } else if (version == V4 || version == V5) {
               if (4 * (long) GET_WORD(buf,26) > high_mem)
                       score += 20;
       } else {
               if (8 * (long) GET_WORD(buf,26) > high_mem)
                       score += 20;
       }

       if (GET_WORD(buf,30) == 0) /* Interpreter == 0 */
               score += 10;
       if (GET_WORD(buf,32) == 0) /* Screen format == 0 */
               score += 10;
       if (GET_WORD(buf,34) == 0) /* Screen width == 0 */
               score += 10;
       if (GET_WORD(buf,36) == 0) /* Screen height == 0 */
               score += 10;
       if (GET_WORD(buf,38) == 0) /* Font format == 0 */
               score += 10;

       if (version != V6 && version != V7) {
               if (GET_WORD(buf,40) == 0) /* Functions offset == 0 */
                       score += 10;
               if (GET_WORD(buf,42) == 0) /* Strings offset == 0 */
                       score += 10;
       } else {
               if (GET_WORD(buf,40) < GET_WORD(buf,26)) /* Functions offset < File size */
                       score += 10;
               if (GET_WORD(buf,42) < GET_WORD(buf,26)) /* Strings offset < File size */
                       score += 10;
       }

       if (GET_WORD(buf,44) == 0) /* Default colours == 0 */
               score += 10;

       if (version <= V4) {
               if (GET_WORD(buf,46) == 0) /* Terminating keys addr == 0 */
                       score += 10;
       } else {
               if (GET_WORD(buf,46) <= GET_WORD(buf,14)) /* Terminating keys addr <= Dynamic size */
                       score += 10;
       }

       if (GET_WORD(buf,48) == 0) /* Line width == 0 */
               score += 10;
       if (GET_WORD(buf,50) == 0) /* Unused == 0 */
               score += 10;

       if (version <= V4) {

               if (GET_WORD(buf,52) == 0) /* Alphabet addr == 0 */
                       score += 10;
               if (GET_WORD(buf,54) == 0) /* Mouse addr == 0 */
                       score += 10;

       } else {

               if (GET_WORD(buf,52) <= GET_WORD(buf,14)) /* Alphabet addr <= Dynamic size */
                       score += 10;
               if (GET_WORD(buf,54) <= GET_WORD(buf,14)) /* Mouse addr <= Dynamic size */
                       score += 10;

       }

       for (i = 0; i < 8; i++)
               if (GET_BYTE(buf,56+i) == 0 || isprint (GET_BYTE(buf,56+i))) /* User name */
                       score += 1;

       area_count = 0;

       add_area (0, 64);
       add_area (GET_WORD(buf,8), 4);
       add_area (GET_WORD(buf,10), (version <= 3) ? 62 : 126);
       add_area (GET_WORD(buf,12), 480);
       add_area (GET_WORD(buf,14), 0);
       add_area (high_mem, 0);

       if (version != V1)
               add_area (GET_WORD(buf,24), (version == 2) ? 64 : 192);
       if (version >= V5 && GET_WORD(buf,46) != 0)
               add_area (GET_WORD(buf,46), 1);
       if (version >= V5 && GET_WORD(buf,52) != 0)
               add_area (GET_WORD(buf,52), 78);
       if (version >= V5 && GET_WORD(buf,54) != 0)
               add_area (GET_WORD(buf,54), 6);

       for (i = 0; i < area_count; i++)
               for (j = i+1; j < area_count; j++)
                       if (area[i].offset + area[i].length > area[j].offset)
                               score -= 10;

       if (score < hiscore)
               return FALSE;
       if (score < 900)
               return FALSE;

       memcpy (header, buf, 64);

       hiscore = score;

       return TRUE;

}/* check_for_header */

void begin_trans (long maxsize) {

       static struct {
               word release;
               byte serial[6];
               long filesize;
               word checksum;
       } old_games[] = {
               {  7, "UG3AU5",  85260L, 0x6fb6 }, /*  Zork 2  */
               { 15, "820308",  82110L, 0x7961 }, /*  Zork 2  */
               { 17, "820427",  82368L, 0xcf13 }, /*  Zork 2  */
               { 18, "820512",  82422L, 0xcf14 }, /*  Zork 2  */
               { 18, "820517",  82422L, 0xcf14 }, /*  Zork 2  */
               {  5, "000000",  82836L, 0xa8a4 }, /*  Zork 1  */
               { 15, "UG3AU5",  78566L, 0xe987 }, /*  Zork 1  */
               { 23, "820428",  75780L, 0xe6dc }, /*  Zork 1  */
               { 25, "820515",  75808L, 0xdfa0 }, /*  Zork 1  */
               { 18, "820311", 111342L, 0x39d5 }, /* Deadline */
               { 19, "820427", 111420L, 0x780e }, /* Deadline */
               { 21, "820512", 111706L, 0xbf83 }  /* Deadline */
       };

       int i;

       if (hiscore < 900)
               error ("Could not find story header");

       printf ("Story file release %d ", (int) GET_WORD(header,2));
       printf ("serial %c%c%c%c%c%c.\n",
               isprint (GET_BYTE(header,18)) ? GET_BYTE(header,18) : ' ',
               isprint (GET_BYTE(header,19)) ? GET_BYTE(header,19) : ' ',
               isprint (GET_BYTE(header,20)) ? GET_BYTE(header,20) : ' ',
               isprint (GET_BYTE(header,21)) ? GET_BYTE(header,21) : ' ',
               isprint (GET_BYTE(header,22)) ? GET_BYTE(header,22) : ' ',
               isprint (GET_BYTE(header,23)) ? GET_BYTE(header,23) : ' ');

       filesize = GET_WORD(header,26);
       checksum = GET_WORD(header,28);

       if (GET_BYTE(header,0) <= V3) {
               filesize *= 2;
               if (maxsize >= 0x20000L || maxsize == 0)
                       maxsize = 0x20000L;
       } else if (GET_BYTE(header,0) <= V5) {
               filesize *= 4;
               if (maxsize >= 0x40000L || maxsize == 0)
                       maxsize = 0x40000L;
       } else {
               filesize *= 8;
               if (maxsize >= 0x80000L || maxsize == 0)
                       maxsize = 0x80000L;
       }

       if (filesize == 0L) {

               for (i = 0; i < sizeof (old_games) / sizeof (old_games[0]); i++)

                       if (GET_WORD(header,2) == old_games[i].release)
                       if (GET_BYTE(header,0) == V1 || !strncmp ((char *) header + 18, (char *) old_games[i].serial, 6)) {
                               checksum = old_games[i].checksum;
                               filesize = old_games[i].filesize;
                       }

       }

       if (filesize > maxsize)
               error ("Disk image has bad format");

       if (filesize != 0L) {
               printf ("Story file size is %ld bytes.\n", filesize);
               checksum_valid = TRUE;
       } else {
               printf ("Story file size is unclear -- assuming %ld.\n", filesize = maxsize);
               checksum_valid = FALSE;
       }

       printf ("Writing story file...\n");

}/* begin_trans */

void trans (int disk) {

       byte buf[256];

       int size = sizeof (buf);

       if (filesize < size)
               size = (int) filesize;

       if (size != 0 && !fread (buf, size, 1, fpin[disk]))
               error ("Cannot read input file");
       if (size != 0 && !fwrite (buf, size, 1, fpout))
               error ("Cannot write output file");

       filesize -= size;

       while (--size >= 0) checksum -= buf[size];

}/* trans */

void end_trans (void) {

       int i;

       for (i = 0; i < sizeof (header); i++) checksum += header[i];

       if (!checksum_valid)
               printf ("Checksum unknown.\n");
       else if (checksum == 0)
               printf ("Checksum good.\n");
       else
               printf ("Checksum bad.\n");

}/* end_trans */


/****************************************************************************
*
*                         Apple II specific routines
*
****************************************************************************/


const char *interleave;

long apple_sector_offset (int sector) {

       char c = interleave[sector & 15];

       if (isalpha (c))
               sector = (sector & ~15) | (c - 'A' + 10);
       if (isdigit (c))
               sector = (sector & ~15) | (c - '0');

       return (long) sector * 256;

}/* apple_sector_offset */


/****************************************************************************
*
*                      Apple II V2/V3 specific routines
*
****************************************************************************/


void get_small_apple_game (void) {

       byte id[18];

       int sector = 48;

       infile (0, 0x0cbdL, id, sizeof (id));

       interleave = strncmp ((char *) id, "Apple II Version E", 18) ?
               "0DB97531ECA8642F" : "0123456789ABCDEF";

       printf ("Assuming sector interleaving scheme %s.\n", interleave);

       check_for_header (0, apple_sector_offset (sector));

       begin_trans (0L);

       while (filesize > 0L) {

               fseek (fpin[0], apple_sector_offset (sector++), SEEK_SET);
               trans (0);

       }

       end_trans ();

}/* get_small_apple_game */


/****************************************************************************
*
*                      Apple II V4/V5 specific routines
*
****************************************************************************/


void convert_disk (void) {

       byte buf[258];

       static const byte trans[] = {
               0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
               0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0x02, 0x03, 0xff, 0x04, 0x05, 0x06,
               0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x08, 0xff, 0xff, 0xff, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
               0xff, 0xff, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0xff, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a,
               0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1b, 0xff, 0x1c, 0x1d, 0x1e,
               0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0x20, 0x21, 0xff, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
               0xff, 0xff, 0xff, 0xff, 0xff, 0x29, 0x2a, 0x2b, 0xff, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32,
               0xff, 0xff, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0xff, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f };

       static const byte swap_bits[] = { 0, 2, 1, 3 };

       static byte track_data1[0x1a00];
       static byte track_data2[0x1a00];

       int track, sector, i;

       if ((fpin[2] = tmpfile ()) == NULL)
               error ("Cannot open temporary file");

       for (track = 0; track < 35; track++) {

               infile (1, (long) 0x1a00 * track, track_data1, sizeof (track_data1));

               for (i = 0; i < 0x1a00; i++)
                       if (track_data1[i] == 0xd5)
                               if (track_data1[(i+1) % 0x1a00] == 0xaa)
                                       if (track_data1[(i+2) % 0x1a00] == 0xad)
                                               goto start_found;

               error ("NIB file has bad format");

start_found:

               memcpy (track_data2, track_data1 + i, 0x1a00 - i);
               memcpy (track_data2 + 0x1a00 - i, track_data1, i);

               for (sector = 0; sector < 18; sector++) {

                       byte *p = track_data2 + 343 * sector + 5;

                       byte value = 0;

                       for (i = 0; i < 343; i++)
                               if (p[i] & 0x80)
                                       p[i] = value ^= trans[p[i] - 0x80];

                       if (value != 0)
                               fprintf (stderr, "Warning: Bad sector checksum on NIB image\n");

                       for (i = 0; i < 86; i++) {
                               buf[i+0*86] = (p[i+1*86] << 2) | swap_bits[(p[i] >> 0) & 3];
                               buf[i+1*86] = (p[i+2*86] << 2) | swap_bits[(p[i] >> 2) & 3];
                               buf[i+2*86] = (p[i+3*86] << 2) | swap_bits[(p[i] >> 4) & 3];
                       }

                       if (!fwrite (buf, 256, 1, fpin[2]))
                               error ("Cannot write temporary file");

               }

       }

}/* convert_disk */

void get_medium_apple_game (void) {

       int start = 0;
       int sector = 0;

       convert_disk ();

       interleave = "0DB97531ECA8642F";

       if (check_for_header (0, apple_sector_offset (48)))
               start = 48;
       if (check_for_header (0, apple_sector_offset (64)))
               start = 64;

       begin_trans (0L);

       for (sector = start; sector < start + 394; sector++) {

               fseek (fpin[0], apple_sector_offset (sector), SEEK_SET);
               trans (0);

       }

       fseek (fpin[2], 0L, SEEK_SET);

       while (filesize > 0L)
               trans (2);

       end_trans ();

}/* get_medium_apple_game */


/****************************************************************************
*
*                       Apple II V6 specific routines
*
****************************************************************************/


byte table[1024];

word block_map[MAX_FILES][256];

word read_table_word (int offs) {

       if (offs >= sizeof (table) / sizeof (table[0]))
               error ("Disk image has bad format");

       return GET_WORD(table,offs);

}/* read_table_word */

void find_story_block (word log_block, int *disk, word *phys_block) {

       int offs = 20;

       for (*disk = 0; *disk < read_table_word (2); *disk += 1) {

               word entries = read_table_word (offs + 4);

               offs += 8;

               while (entries--) {

                       word b1 = read_table_word (offs + 0);
                       word b2 = read_table_word (offs + 2);
                       word at = read_table_word (offs + 4);

                       offs += 6;

                       if (b1 <= log_block && log_block <= b2) {

                               if (log_block + at - b1 >= 256)
                                       error ("Disk image has bad format");

                               *phys_block = block_map[*disk][log_block + at - b1];

                               return;

                       }

               }

       }

       error ("Disk image has bad format");

}/* find_story_block */

void get_large_apple_game (int n) {

       byte lo[256], hi[256];

       byte id[12];
       char fn[12];

       int i, disk;

       const char *title[] = { "ZORK0", "SHOGUN", "JOURNEY", "ARTHUR", NULL };

       word log_block = 0;
       word phys_block = 0;

       int this_side = 0, this_game = 0, prev_game = 0;

       interleave = "0EDCBA987654321F";

       for (disk = 0; disk < n; disk++) {

               infile (disk, 0x0b05L, id, sizeof (12));

               for (i = 0; title[i]; i++)
                       if (!strncmp ((char *) id, title[i], strlen (title[i])))
                               goto detected;

               error ("Cannot identify disk image");

detected:

               this_game = i;
               this_side = id[strlen (title[this_game]) + 1] - '0';

               printf ("Image identified as %s side %d.\n", title[this_game], this_side);

               if (this_game != prev_game && disk != 0)
                       error ("Disk images from different games");
               if (this_side != disk + 1)
                       error ("Wrong ordering of disk images");

               sprintf (fn, "%s.D%d", title[this_game], this_side);

               infile (disk, 0x0b2cL, id, sizeof (id));
               if (!strncmp ((char *) id, fn, strlen (fn))) { infile (disk, 0x0b3cL, id, 1); goto found; }
               infile (disk, 0x0b53L, id, sizeof (id));
               if (!strncmp ((char *) id, fn, strlen (fn))) { infile (disk, 0x0b63L, id, 1); goto found; }
               infile (disk, 0x0b7aL, id, sizeof (id));
               if (!strncmp ((char *) id, fn, strlen (fn))) { infile (disk, 0x0b8aL, id, 1); goto found; }
               infile (disk, 0x0ba1L, id, sizeof (id));
               if (!strncmp ((char *) id, fn, strlen (fn))) { infile (disk, 0x0bb1L, id, 1); goto found; }

               error ("Disk image has bad format");

       found:

               infile (disk, apple_sector_offset (2 * id[0]), lo, sizeof (lo));
               infile (disk, apple_sector_offset (2 * id[0] + 1), hi, sizeof (hi));

               for (i = 0; i < 256; i++)
                       block_map[disk][i] = hi[i] * 256 + lo[i];

               prev_game = this_game;

       }

       infile (0, apple_sector_offset (2 * block_map[0][0]), table, 256);
       infile (0, apple_sector_offset (2 * block_map[0][0] + 1), table + 256, 256);

       if (GET_WORD(table,0) > 256) {
               infile (0, apple_sector_offset (2 * block_map[0][1]), table + 512, 256);
               infile (0, apple_sector_offset (2 * block_map[0][1] + 1), table + 768, 256);
       }

       find_story_block (0, &disk, &phys_block);

       check_for_header (disk, apple_sector_offset (2 * phys_block));

       begin_trans (0L);

       while (filesize > 0L) {

               find_story_block (log_block++, &disk, &phys_block);

               fseek (fpin[disk], apple_sector_offset (2 * phys_block), SEEK_SET);
               trans (disk);
               fseek (fpin[disk], apple_sector_offset (2 * phys_block + 1), SEEK_SET);
               trans (disk);

       }

       end_trans ();

}/* get_large_apple_game */


/****************************************************************************
*
*                         Apple II main routine
*
****************************************************************************/


void get_apple (int n) {

       int disk;

       if (n == 1) { /* V2 or V3 game */

               if (file_info[0].size == 232960L)
                       error ("The disk image should be in DSK format\n");

               get_small_apple_game ();

       }

       if (n == 2) { /* V4 or V5 game */

               if (file_info[0].size == 232960L)
                       error ("The first disk image should be in DSK format\n");
               if (file_info[1].size != 232960L)
                       error ("The second disk image must be in NIB format\n");

               get_medium_apple_game ();

       }

       if (n == 3) { /* Error */

               error ("Wrong number of disk images (try to omit the third image)");

       }

       if (n >= 4) { /* V6 game */

               for (disk = 0; disk < n; disk++)
                       if (file_info[disk].size == 232960L)
                               error ("All disk images should be in DSK format");

               get_large_apple_game (n);

       }

}/* get_apple */


/****************************************************************************
*
*                           Atari specific routines
*
****************************************************************************/


void get_atari (int n) {

       long offset = 0L;
       long start = 0L;

       int sectors = 0;
       int disk = 0;

       if (n >= 3)
               error ("Too many disk image files for an Atari game");

       if (check_for_header (0, 0x01f90L))
               start = 0x01f90L;
       if (check_for_header (0, 0x01c10L))
               start = 0x01c10L;
       if (check_for_header (0, 0x02410L))
               start = 0x02410L;

       if (start == 0L)
               for (offset = 16; offset < 0x16810L; offset += 256)
                       if (check_for_header (0, offset))
                               start = offset;

       begin_trans (0x16810L - start + ((n == 2) ? 0x16800L : 0));

       if ((GET_BYTE(header,1) & 4) == 4 && n == 1)
               error ("This game takes two disk images");
       if ((GET_BYTE(header,1) & 4) == 0 && n == 2)
               error ("This game takes only one disk image");

       fseek (fpin[0], start, SEEK_SET);

       while (filesize > 0L) {

               trans (disk);

               if (n == 2 && disk == 0 && (long) ++sectors * 256 >= GET_WORD(header,4))
                       fseek (fpin[disk = 1], 16L, SEEK_SET);

       }

       end_trans ();

}/* get_atari */


/****************************************************************************
*
*                           C=64 specific routines
*
****************************************************************************/


void dump_one_side (int track, bool skip17, bool skip18, int sectors_used) {

       int sector = 0;

       int maxtracks = 35 - track + 1;

       if (skip17) maxtracks--;
       if (skip18) maxtracks--;

       begin_trans ((long) maxtracks * sectors_used * 256);

       fseek (fpin[0], (long) (track - 1) * 0x1500, SEEK_SET);

       while (filesize > 0L) {

               if (track == 17 && skip17) { fseek (fpin[0], (long) 21 * 256, SEEK_CUR); track++; }
               if (track == 18 && skip18) { fseek (fpin[0], (long) 19 * 256, SEEK_CUR); track++; }

               if (track == 18)
                       fseek (fpin[0], 512L, SEEK_CUR);

               trans (0);

               if (track == 18)
                       fseek (fpin[0], -512L, SEEK_CUR);

               if (++sector == sectors_used) {

                       int total_sectors =
                               (track <= 17) ? 21 :
                               (track <= 24) ? 19 :
                               (track <= 30) ? 18 :
                               (track <= 35) ? 17 : 0;

                       fseek (fpin[0], (long) (total_sectors - sectors_used) * 256, SEEK_CUR);

                       track++; sector = 0;

               }

       }

       end_trans ();

}/* dump one side */

void dump_two_sides (int track, int last_track, int last_sector, bool skip) {

       int sector = 0, disk = 0;

       begin_trans (0L);

       fseek (fpin[0], (long) (track - 1) * 0x1500, SEEK_SET);

       while (filesize > 0L) {

               if (sector == 0 && track == 18)
                       { fseek (fpin[disk], 256L, SEEK_CUR); sector++; }
               if (sector == 0 && track == 20 && disk == 1 && skip)
                       { fseek (fpin[disk], 256L, SEEK_CUR); sector++; }

               trans (disk);

               if (ftell (fpout) == 0x23900L)
       disk = 2 * disk - disk;

               if (track != last_track || sector != last_sector || disk != 0) {

                       int total_sectors =
                               (track <= 17) ? 21 :
                               (track <= 24) ? 19 :
                               (track <= 30) ? 18 :
                               (track <= 35) ? 17 : 0;

                       if (++sector == total_sectors)
                               { sector = 0; track++; }

               } else {

                       disk = 1;
                       track = 1;
                       sector = 0;

                       fseek (fpin[1], 0L, SEEK_SET);

               }

       }

       end_trans ();

}/* dump two sides */

void get_v3_c64 (void) {

       byte id[6];
       int i;

       static struct {
               char interpreter;
               int first_track;
               bool skip17, skip18;
               int sectors_used;
               byte id[6];
       } v3chart[] = {
               { '1', 4, FALSE, FALSE, 16, { 0xf5, 0x60, 0x20, 0xf7, 0x1f, 0x20 } },
               { '2', 4, FALSE, FALSE, 16, { 0x00, 0xd0, 0xf5, 0x60, 0x20, 0xf9 } },
               { 'A', 4, FALSE, FALSE, 16, { 0xa0, 0x03, 0x20, 0x7b, 0x27, 0xa9 } },
               { 'B', 5,  TRUE,  TRUE, 16, { 0x0f, 0xa8, 0xa6, 0x78, 0x20, 0xba } },
               { 'C', 5,  TRUE, FALSE, 17, { 0xff, 0xa2, 0x0f, 0x20, 0xc9, 0xff } },
               { 'D', 5,  TRUE, FALSE, 17, { 0xdd, 0x27, 0x09, 0x30, 0x99, 0xf0 } },
               { 'E', 5,  TRUE, FALSE, 17, { 0xff, 0xa2, 0x00, 0xc9, 0x0a, 0x90 } },
               { 'F', 5,  TRUE, FALSE, 17, { 0xa2, 0xef, 0xa0, 0x27, 0xa9, 0x01 } },
               { 'G', 5,  TRUE, FALSE, 17, { 0x69, 0x6e, 0x75, 0x65, 0x2e, 0x0d } },
               { 'H', 5,  TRUE, FALSE, 17, { 0x45, 0x54, 0x55, 0x52, 0x4e, 0x5d } }
       };

       infile (0, 0x2000L, id, sizeof (id));

       for (i = 0; i < sizeof (v3chart) / sizeof (v3chart[0]); i++)
               if (!strncmp ((char *) id, (char *) v3chart[i].id, 6))
                       goto detected;

       error ("Cannot determine interpreter version");

detected:

       printf ("Detected V3 interpreter '%c'.\n", v3chart[i].interpreter);

       dump_one_side (
               v3chart[i].first_track,
               v3chart[i].skip17,
               v3chart[i].skip18,
               v3chart[i].sectors_used);

}/* get_v3_c64 */

void get_v4_c64 (void) {

       byte id[6];
       int i;

       static struct {
               char interpreter;
               int last_track, last_sector;
               bool skip; /* skip sector 0 track 20 side 1 */
               byte id[6];
       } v4chart[] = {
               { 'A', 19, 10, FALSE, { 0xdd, 0x30, 0x2d, 0xf0, 0x05, 0xca } }, /* C128 */
               { 'B', 19, 10, FALSE, { 0x2a, 0x29, 0x7f, 0xc9, 0x0d, 0xf0 } }, /* C128 */
               { 'L', 11,  6,  TRUE, { 0x02, 0x69, 0x20, 0xa2, 0x06, 0xdd } }
       };

       infile (0, 0x2000L, id, sizeof (id));

       for (i = 0; i < sizeof (v4chart) / sizeof (v4chart[0]); i++)
               if (!strncmp ((char *) id, (char *) v4chart[i].id, 6))
                       goto detected;

       error ("Cannot determine interpreter version");

detected:

       printf ("Detected V4 interpreter '%c'.\n", v4chart[i].interpreter);

       dump_two_sides (
               3,
               v4chart[i].last_track,
               v4chart[i].last_sector,
               v4chart[i].skip);

}/* get_v4_c64 */

void get_v5_c64 (void) {

       byte id[6];
       int i;

       static struct {
               char interpreter;
               int last_track, last_sector;
               bool skip; /* skip sector 0 track 20 side 1 */
               byte id[6];
       } v5chart[] = {
               { 'A', 21, 14, FALSE, { 0x01, 0xd0, 0x08, 0xa9, 0x12, 0x8d } }, /* C128 */
               { 'C', 13,  6,  TRUE, { 0x20, 0x73, 0x2f, 0xa6, 0x7c, 0xca } },
               { 'E', 13,  6,  TRUE, { 0xa5, 0x42, 0x88, 0x91, 0x3f, 0xa9 } },
               { 'H', 13,  6,  TRUE, { 0x91, 0x3f, 0xa9, 0x00, 0x85, 0x3e } },
               { 'J', 13,  6,  TRUE, { 0x38, 0xe9, 0x02, 0x91, 0x3f, 0xb0 } }
       };

       infile (0, 0x2300L, id, sizeof (id));

       for (i = 0; i < sizeof (v5chart) / sizeof (v5chart[0]); i++)
               if (!strncmp ((char *) id, (char *) v5chart[i].id, 6))
                       goto detected;

       error ("Cannot determine interpreter version");

detected:

       printf ("Detected V5 interpreter '%c'.", v5chart[i].interpreter);

       dump_two_sides (
               5,
               v5chart[i].last_track,
               v5chart[i].last_sector,
               v5chart[i].skip);

}/* get_v5_c64 */

void get_commodore (int n) {

       if (n >= 3)
               error ("Too many disk image files for a C64 game");

       check_for_header (0, 0x2a00L);
       check_for_header (0, 0x3f00L);
       check_for_header (0, 0x5400L);

       if (GET_BYTE(header,0) >= V4 && n == 1)
               error ("This game takes two disk images");
       if (GET_BYTE(header,0) <= V3 && n == 2)
               error ("This game takes only one disk image");

       switch (GET_BYTE(header,0)) {
               case V3: get_v3_c64 (); break;
               case V4: get_v4_c64 (); break;
               case V5: get_v5_c64 (); break;
               default: error ("Couldn't find start of story file");
       }

}/* get_commodore */


/****************************************************************************
*
*                            CPC specific routines
*
****************************************************************************/


long amstrad_sector_offset (int sector) {

       static int mapper[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
       static int previous = -1;

       int i;

       long offset = 256 + (long) (sector / 9) * 0x1300;

       if (sector / 9 != previous)

               for (i = 0; i < 9; i++) {

                       byte c;

                       infile (0, offset + 26 + 8 * i, &c, 1);

                       c = (c & 15) - 1;

                       if (c >= 9)
                               error ("Disk image has bad format");

                       mapper[c] = i;

               }

       previous = sector / 9;

       return offset + (2 * mapper[sector % 9] + 1) * 256;

}/* amstrad_sector_offset */

int scan_directory (int sector, byte *story_map) {

       byte buf[32];

       int part = 0;
       int count = 0;

       int entry, n;

       for (entry = 0; entry < 16; entry++) {

               infile (0, amstrad_sector_offset (sector) + 32 * entry, buf, 32);

               if (buf[0x00] != 0 || buf[0x0c] != part)
                       continue;

               buf[0x09] &= 0x7f;
               buf[0x0a] &= 0x7f;
               buf[0x0b] &= 0x7f;

               if (strncmp ((char *) buf + 9, "DAT", 3))
                       continue;

               if (buf[0x0f] > 0x80)
                       return 0;

               for (n = 0; 8 * n < buf[0x0f]; n++) {

                       if (count == 128 || buf[0x10 + n] + sector / 2 >= 180)
                               return 0;

                       story_map[count++] = buf[0x10 + n] + sector / 2;

               }

               part++;

       }

       return count;

}/* scan_directory */

void get_amstrad (int n) {

       byte map[128];

       int count;

       if (n >= 2)
               error ("Too many disk image files for a CPC game");

       count = scan_directory (18, map);

       if (count == 0)
               count = scan_directory (0, map);
       if (count == 0)
               error ("Disk image has bad format");

       check_for_header (0, amstrad_sector_offset (2 * map[0]));

       begin_trans ((long) count * 1024);

       count = 0;

       while (filesize > 0L) {

               fseek (fpin[0], amstrad_sector_offset (2 * map[count]), SEEK_SET);
               trans (0);
               trans (0);
               fseek (fpin[0], amstrad_sector_offset (2 * map[count] + 1), SEEK_SET);
               trans (0);
               trans (0);

               count++;

       }

       end_trans ();

}/* get_amstrad */


/****************************************************************************
*
*                            IBM specific routines
*
****************************************************************************/


void get_ibm (int drive) {

#ifdef __MSDOS__

       static byte buf[4096];

       int track;

       if ((fpin[0] = tmpfile ()) == NULL)
               error ("Cannot open temporary file");

       for (track = 6; track < 38; track++) {

               struct diskinfo_t dinfo;

               dinfo.drive = drive;
               dinfo.head = 0;
               dinfo.track = track;
               dinfo.sector = 1;
               dinfo.nsectors = 8;
               dinfo.buffer = buf;

               if (_bios_disk (_DISK_READ, &dinfo) != 8)
                       error ("Cannot read disk");

               if (!fwrite (buf, 4096, 1, fpin[0]))
                       error ("Cannot write temporary file");

       }

       check_for_header (0, 0);

       fseek (fpin[0], 0L, SEEK_SET);

       begin_trans (0L);

       while (filesize > 0L)
               trans (0);

       end_trans ();

#else

       error ("This version does not support IBM bootable disks");

#endif

}/* get_ibm */


/****************************************************************************
*
*                              Generic routines
*
****************************************************************************/


void get_generic (int n) {

       long i, offset;

       if (n >= 2)
               error ("Too many disk image files for generic extraction");

       if (!check_for_header (0, 0L)) {

               for (i = 0L; i < file_info[0].size - 64; i++)
                       if (check_for_header (0, i))
                               offset = i;

       } else offset = 0;

       begin_trans (file_info[0].size - offset);

       fseek (fpin[0], offset, SEEK_SET);

       while (filesize > 0L)
               trans (0);

       end_trans ();

}/* get_generic */


/****************************************************************************
*
*                                Main routine
*
****************************************************************************/


int main (int argc, char *argv[])
{

       int disk, n;

       enum tsystem this_type = UNKNOWN;
       enum tsystem prev_type = UNKNOWN;

       char extension[4], *p;

       if (argc < 3) {

               puts (
                       "\n"
                       "ZCut V1.1 -- extract Infocom story files\n"
                       "\n"
                       "Written by Stefan Jokisch in 1998.  Based on work by Matthew T. Russotto,\n"
                       "Paul D. Doherty, Stephen Tjasink, Mark Howell and Michael Jenkin.\n"
                       "\n"
                       "ZCut extracts Infocom story files (aka Z-code) from\n"
                       "\n"
                       "- Amstrad CPC disk images [.DSK]\n"
                       "- Apple ][ disk images [.DSK, .NIB]\n"
                       "- Atari 800/XL/XE disk images [.ATR]\n"
                       "- Commodore 64/128 disk images [.D64]\n"
                       "- IBM PC bootable disks (MS-DOS version only)\n"
                       "- any file if the Z-code is stored in one piece\n"
                       "\n"
                       "Usage: zcut input-file-1 [input-file-2...] output-file\n"
                       "       zcut drive: output-file");

               return EXIT_FAILURE;

       }

       if ((n = argc - 2) > MAX_FILES)
               error ("Too many arguments");

       if (n != 1 || argv[1][1] != ':' || argv[1][2] != 0)

               for (disk = 0; disk < n; disk++) {

                       file_info[disk].name = argv[disk + 1];

                       fpin[disk] = fopen (file_info[disk].name, "rb");

                       if (fpin[disk] == NULL)
                               error ("Cannot open disk image");

                       this_type = UNKNOWN;

                       if ((p = strrchr (file_info[disk].name, '.')) != NULL) {

                               strncpy (extension, p + 1, 3);

                               extension[0] = toupper (extension[0]);
                               extension[1] = toupper (extension[1]);
                               extension[2] = toupper (extension[2]);
                               extension[3] = 0;

                               fseek (fpin[disk], 0L, SEEK_END);

                               file_info[disk].size = ftell (fpin[disk]);

                               if (!strcmp (extension, "DSK") && file_info[disk].size == 194816L)
                                       this_type = AMSTRAD;
                               if (!strcmp (extension, "ATR") && file_info[disk].size == 92176L)
                                       this_type = ATARI;
                               if (!strcmp (extension, "ATR") && file_info[disk].size == 133136L)
                                       this_type = ATARI;
                               if (!strcmp (extension, "DSK") && file_info[disk].size == 143360L)
                                       this_type = APPLE;
                               if (!strcmp (extension, "NIB") && file_info[disk].size == 232960L)
                                       this_type = APPLE;
                               if (!strcmp (extension, "D64") && file_info[disk].size == 174848L)
                                       this_type = COMMODORE;

                       }

                       if (disk != 0 && this_type != prev_type)
                               error ("Disk images have different types");

                       prev_type = this_type;

               }

       else prev_type = IBM;

       fpout = fopen (argv[argc - 1], "wb");

       if (fpout == NULL)
               error ("Cannot open output file");

       printf ("Input type is ");

       switch (prev_type) {

       case AMSTRAD:
               printf ("Amstrad CPC disk image.\n");
               get_amstrad (n);
               break;
       case ATARI:
               printf ("Atari 800/XL/XE disk image.\n");
               get_atari (n);
               break;
       case APPLE:
               printf ("Apple ][ disk image.\n");
               get_apple (n);
               break;
       case COMMODORE:
               printf ("Commodore 64/128 disk image.\n");
               get_commodore (n);
               break;
       case IBM:
               printf ("IBM bootable disk.\n");
               get_ibm (toupper (argv[1][0]) - 'A');
               break;
       default:
               printf ("an unknown file format.\n");
               get_generic (n);
               break;

       }

       cleanup ();

       printf ("Operation successful.\n");

       return EXIT_SUCCESS;

}/* main */