2009-04-22 20:04:28 +02:00

700 lines
22 KiB
C

/*
sms.c - Sega Master System/Game Gear support for uCON64
Copyright (c) 1999 - 2001 NoisyB <noisyb@gmx.net>
Copyright (c) 2003 - 2004 dbjh
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <sys/stat.h>
#include "misc/chksum.h"
#include "misc/misc.h"
#include "misc/string.h"
#include "misc/file.h"
#ifdef USE_ZLIB
#include "misc/archive.h"
#endif
#include "misc/getopt2.h" // st_getopt2_t
#include "ucon64.h"
#include "ucon64_misc.h"
#include "sms.h"
#include "backup/mgd.h"
#include "backup/smd.h"
#define SMS_HEADER_START 0x7ff0
#define SMS_HEADER_LEN (sizeof (st_sms_header_t))
static int sms_chksum (unsigned char *rom_buffer, int rom_size);
const st_getopt2_t sms_usage[] =
{
{
NULL, 0, 0, 0,
NULL, "Sega Master System(II/III)/Game Gear (Handheld)"/*"1986/19XX SEGA http://www.sega.com"*/,
NULL
},
{
"sms", 0, 0, UCON64_SMS,
NULL, "force recognition",
&ucon64_wf[WF_OBJ_SMS_SWITCH]
},
{
"int", 0, 0, UCON64_INT,
NULL, "force ROM is in interleaved format (SMD)",
&ucon64_wf[WF_OBJ_ALL_SWITCH]
},
{
"nint", 0, 0, UCON64_NINT,
NULL, "force ROM is not in interleaved format (RAW)",
&ucon64_wf[WF_OBJ_ALL_SWITCH]
},
{
"mgd", 0, 0, UCON64_MGD,
NULL, "convert to Multi Game*/MGD2/MGH/RAW (gives SMS name)",
&ucon64_wf[WF_OBJ_ALL_DEFAULT_NO_SPLIT]
},
{
"mgdgg", 0, 0, UCON64_MGDGG,
NULL, "same as " OPTION_LONG_S "mgd, but gives GG name",
&ucon64_wf[WF_OBJ_SMS_DEFAULT_NO_SPLIT]
},
{
"smd", 0, 0, UCON64_SMD,
NULL, "convert to Super Magic Drive/SMD",
&ucon64_wf[WF_OBJ_ALL_DEFAULT_NO_SPLIT]
},
{
"smds", 0, 0, UCON64_SMDS,
NULL, "convert emulator (*.srm) SRAM to Super Magic Drive/SMD",
NULL
},
{
"chk", 0, 0, UCON64_CHK,
NULL, "fix ROM checksum (SMS only)",
&ucon64_wf[WF_OBJ_ALL_DEFAULT]
},
{
"multi", 1, 0, UCON64_MULTI,
"SIZE", "make multi-game file for use with SMS-PRO/GG-PRO flash card,\n"
"truncated to SIZE Mbit; file with loader must be specified\n"
"first, then all the ROMs, multi-game file to create last",
&ucon64_wf[WF_OBJ_ALL_INIT_PROBE_STOP]
},
{NULL, 0, 0, 0, NULL, NULL, NULL}
};
typedef struct st_sms_header
{
char signature[8]; // "TMR "{"SEGA", "ALVS", "SMSC"}/"TMG SEGA"
unsigned char pad[2]; // 8
unsigned char checksum_low; // 10
unsigned char checksum_high; // 11
unsigned char partno_low; // 12
unsigned char partno_high; // 13
unsigned char version; // 14
unsigned char checksum_range; // 15, and country info
} st_sms_header_t;
static st_sms_header_t sms_header;
static int is_gamegear;
// see src/backup/mgd.h for the file naming scheme
int
sms_mgd (st_rominfo_t *rominfo, int console)
{
char src_name[FILENAME_MAX], dest_name[FILENAME_MAX];
unsigned char *buffer;
int size = ucon64.file_size - rominfo->buheader_len;
strcpy (src_name, ucon64.rom);
mgd_make_name (ucon64.rom, console, size, dest_name);
ucon64_file_handler (dest_name, src_name, OF_FORCE_BASENAME);
if (!(buffer = (unsigned char *) malloc (size)))
{
fprintf (stderr, ucon64_msg[ROM_BUFFER_ERROR], size);
exit (1);
}
ucon64_fread (buffer, rominfo->buheader_len, size, src_name);
if (rominfo->interleaved)
smd_deinterleave (buffer, size);
ucon64_fwrite (buffer, 0, size, dest_name, "wb");
free (buffer);
printf (ucon64_msg[WROTE], dest_name);
remove_temp_file ();
mgd_write_index_file ((char *) basename2 (dest_name), 1);
if (size <= 4 * MBIT)
printf ("NOTE: It may be necessary to change the suffix in order to make the game work\n"
" on an MGD2. You could try suffixes like .010, .024, .040, .048 or .078.\n");
return 0;
}
int
sms_smd (st_rominfo_t *rominfo)
{
st_smd_header_t header;
char src_name[FILENAME_MAX], dest_name[FILENAME_MAX];
unsigned char *buffer;
int size = ucon64.file_size - rominfo->buheader_len;
memset (&header, 0, SMD_HEADER_LEN);
header.size = size / 8192 >> 8;
header.id0 = 3; //size / 8192;
header.id1 = 0xaa;
header.id2 = 0xbb;
header.type = 6;
strcpy (src_name, ucon64.rom);
strcpy (dest_name, ucon64.rom);
set_suffix (dest_name, ".smd");
ucon64_file_handler (dest_name, src_name, 0);
if (!(buffer = (unsigned char *) malloc (size)))
{
fprintf (stderr, ucon64_msg[ROM_BUFFER_ERROR], size);
exit (1);
}
ucon64_fread (buffer, rominfo->buheader_len, size, src_name);
if (!rominfo->interleaved)
smd_interleave (buffer, size);
ucon64_fwrite (&header, 0, SMD_HEADER_LEN, dest_name, "wb");
ucon64_fwrite (buffer, rominfo->buheader_len, size, dest_name, "ab");
free (buffer);
printf (ucon64_msg[WROTE], dest_name);
remove_temp_file ();
return 0;
}
int
sms_smds (void)
{
st_smd_header_t header;
char src_name[FILENAME_MAX], dest_name[FILENAME_MAX];
memset (&header, 0, SMD_HEADER_LEN);
header.id1 = 0xaa;
header.id2 = 0xbb;
header.type = 7; // SRAM file
strcpy (src_name, ucon64.rom);
strcpy (dest_name, ucon64.rom);
set_suffix (dest_name, ".sav");
ucon64_file_handler (dest_name, src_name, 0);
ucon64_fwrite (&header, 0, SMD_HEADER_LEN, dest_name, "wb");
fcopy (src_name, 0, fsizeof (src_name), dest_name, "ab");
printf (ucon64_msg[WROTE], dest_name);
remove_temp_file ();
return 0;
}
int
sms_chk (st_rominfo_t *rominfo)
{
char buf[2], dest_name[FILENAME_MAX];
int offset = rominfo->header_start + 10;
if (is_gamegear)
{
fprintf (stderr, "ERROR: This option works only for SMS (not Game Gear) files\n");
return -1;
}
strcpy (dest_name, ucon64.rom);
ucon64_file_handler (dest_name, NULL, 0);
fcopy (ucon64.rom, 0, ucon64.file_size, dest_name, "wb");
buf[0] = rominfo->current_internal_crc; // low byte
buf[1] = rominfo->current_internal_crc >> 8; // high byte
// change checksum
if (rominfo->interleaved)
{
ucon64_fputc (dest_name, rominfo->buheader_len +
(offset & ~0x3fff) + 0x2000 + (offset & 0x3fff) / 2, buf[0], "r+b");
ucon64_fputc (dest_name, rominfo->buheader_len +
(offset & ~0x3fff) + (offset & 0x3fff) / 2, buf[1], "r+b");
}
else
ucon64_fwrite (buf, rominfo->buheader_len + offset, 2, dest_name, "r+b");
dumper (stdout, buf, 2, rominfo->buheader_len + offset, DUMPER_HEX);
printf (ucon64_msg[WROTE], dest_name);
return 0;
}
static void
write_game_table_entry (FILE *destfile, int file_no, int totalsize)
{
static int sram_page = 0, sram_msg_printed = 0;
int n;
unsigned char name[0x0c], flags = 0; // x, D (reserved), x, x, x, x, S1, S0
const char *p;
fseek (destfile, 0x2000 + (file_no - 1) * 0x10, SEEK_SET);
fputc (0xff, destfile); // 0x0 = 0xff
memset (name, ' ', 0x0c);
p = basename2 (ucon64.rom);
n = strlen (p);
if (n > 0x0c)
n = 0x0c;
memcpy (name, p, n);
for (n = 0; n < 0x0c; n++)
{
if (!isprint ((int) name[n]))
name[n] = '.';
else
name[n] = toupper (name[n]); // loader only supports upper case characters
}
fwrite (name, 1, 0x0c, destfile); // 0x01 - 0x0c = name
fputc (0, destfile); // 0x0d = 0
fputc (totalsize / 16384, destfile); // 0x0e = bank code
if (sram_page > 3)
{
if (!sram_msg_printed)
{
puts ("NOTE: This ROM (and all following) will share SRAM with ROM 4");
sram_msg_printed = 1;
}
sram_page = 3;
}
flags = sram_page++;
fputc (flags, destfile); // 0x1f = flags
}
int
sms_multi (int truncate_size, char *fname)
{
#define BUFSIZE 0x20000
// BUFSIZE must be a multiple of 16 kB (for deinterleaving) and larger than or
// equal to 1 Mbit (for check sum calculation)
int n, n_files, file_no, bytestowrite, byteswritten, totalsize = 0, done,
truncated = 0, paddedsize, org_do_not_calc_crc = ucon64.do_not_calc_crc;
struct stat fstate;
FILE *srcfile, *destfile;
char destname[FILENAME_MAX];
unsigned char *buffer;
if (truncate_size == 0)
{
fprintf (stderr, "ERROR: Can't make multi-game file of 0 bytes\n");
return -1;
}
if (!(buffer = (unsigned char *) malloc (BUFSIZE)))
{
fprintf (stderr, ucon64_msg[FILE_BUFFER_ERROR], BUFSIZE);
return -1;
}
if (fname != NULL)
{
strcpy (destname, fname);
n_files = ucon64.argc;
}
else
{
strcpy (destname, ucon64.argv[ucon64.argc - 1]);
n_files = ucon64.argc - 1;
}
ucon64_file_handler (destname, NULL, OF_FORCE_BASENAME);
if ((destfile = fopen (destname, "w+b")) == NULL)
{
fprintf (stderr, ucon64_msg[OPEN_WRITE_ERROR], destname);
return -1;
}
printf ("Creating multi-game file for SMS-PRO/GG-PRO: %s\n", destname);
file_no = 0;
for (n = 1; n < n_files; n++)
{
if (access (ucon64.argv[n], F_OK))
continue; // "file" does not exist (option)
stat (ucon64.argv[n], &fstate);
if (!S_ISREG (fstate.st_mode))
continue;
if (file_no == 32) // loader + 31 games
{
printf ("WARNING: A multi-game file can contain a maximum of 31 games. The other files\n"
" are ignored.\n");
break;
}
ucon64.console = UCON64_UNKNOWN;
ucon64.rom = ucon64.argv[n];
ucon64.file_size = fsizeof (ucon64.rom);
// DON'T use fstate.st_size, because file could be compressed
ucon64.rominfo->buheader_len = UCON64_ISSET (ucon64.buheader_len) ?
ucon64.buheader_len : 0;
ucon64.rominfo->interleaved = UCON64_ISSET (ucon64.interleaved) ?
ucon64.interleaved : 0;
ucon64.do_not_calc_crc = 1;
if (sms_init (ucon64.rominfo) != 0)
printf ("WARNING: %s does not appear to be an SMS/GG ROM\n", ucon64.rom);
if ((srcfile = fopen (ucon64.rom, "rb")) == NULL)
{
fprintf (stderr, ucon64_msg[OPEN_READ_ERROR], ucon64.rom);
continue;
}
if (ucon64.rominfo->buheader_len)
fseek (srcfile, ucon64.rominfo->buheader_len, SEEK_SET);
if (file_no == 0)
{
printf ("Loader: %s\n", ucon64.rom);
if (ucon64.file_size - ucon64.rominfo->buheader_len != 3 * 16384)
printf ("WARNING: Are you sure %s is a loader binary?\n", ucon64.rom);
}
else
{
printf ("ROM%d: %s\n", file_no, ucon64.rom);
write_game_table_entry (destfile, file_no, totalsize);
fseek (destfile, totalsize, SEEK_SET); // restore file pointer
}
file_no++;
done = 0;
byteswritten = 0; // # of bytes written per file
while (!done)
{
bytestowrite = fread (buffer, 1, BUFSIZE, srcfile);
if (ucon64.rominfo->interleaved)
smd_deinterleave (buffer, BUFSIZE);
// yes, BUFSIZE. bytestowrite might not be n * 16 kB
if (totalsize + bytestowrite > truncate_size)
{
bytestowrite = truncate_size - totalsize;
done = 1;
truncated = 1;
printf ("Output file is %d Mbit, truncating %s, skipping %d bytes\n",
truncate_size / MBIT, ucon64.rom, ucon64.file_size -
ucon64.rominfo->buheader_len - (byteswritten + bytestowrite));
}
totalsize += bytestowrite;
if (bytestowrite == 0)
done = 1;
fwrite (buffer, 1, bytestowrite, destfile);
byteswritten += bytestowrite;
}
fclose (srcfile);
if (truncated)
break;
// games have to be aligned to (start at) a 16 kB boundary
paddedsize = (totalsize + 16384 - 1) & ~(16384 - 1);
// printf ("paddedsize: %d (%f); totalsize: %d (%f)\n",
// paddedsize, paddedsize / (1.0 * MBIT), totalsize, totalsize / (1.0 * MBIT));
if (paddedsize > totalsize)
{
if (paddedsize > truncate_size)
{
truncated = 1; // not *really* truncated
paddedsize = truncate_size;
}
memset (buffer, 0, BUFSIZE);
while (totalsize < paddedsize)
{
bytestowrite = paddedsize - totalsize > BUFSIZE ?
BUFSIZE : paddedsize - totalsize;
fwrite (buffer, 1, bytestowrite, destfile);
totalsize += bytestowrite;
}
}
if (truncated)
break;
}
// fill the next game table entry
fseek (destfile, 0x2000 + (file_no - 1) * 0x10, SEEK_SET);
fputc (0, destfile); // indicate no next game
/*
The SMS requires the check sum to match the data. The ToToTEK loaders have
the lower nibble of the "check sum range byte" set to 0x0f. Maybe ToToTEK
will change this or maybe somebody else will write a loader. To avoid extra
code to handle those cases we just overwite the value.
We don't handle a GG multi-game file differently, because we cannot detect
that the user intended to make such a file (other than by adding a new GG
multi-game option). ToToTEK's GG loader has an SMS header.
*/
fseek (destfile, 0, SEEK_SET);
n = fread (buffer, 1, 0x20000, destfile); // 0x0f => check sum range = 0x20000
buffer[SMS_HEADER_START + 15] |= 0x0f; // overwrite check sum range byte
sms_header.checksum_range = 0x0f; // sms_chksum() uses this variable
n = sms_chksum (buffer, n);
buffer[SMS_HEADER_START + 10] = n; // low byte
buffer[SMS_HEADER_START + 11] = n >> 8; // high byte
fseek (destfile, SMS_HEADER_START + 10, SEEK_SET);
fwrite (buffer + SMS_HEADER_START + 10, 1, 6, destfile);
fclose (destfile);
ucon64.console = UCON64_SMS;
ucon64.do_not_calc_crc = org_do_not_calc_crc;
return 0;
}
static int
sms_testinterleaved (st_rominfo_t *rominfo)
{
unsigned char buf[0x4000] = { 0 };
ucon64_fread (buf, rominfo->buheader_len + 0x4000, // header in 2nd 16 kB block
0x2000 + (SMS_HEADER_START - 0x4000 + 8) / 2, ucon64.rom);
if (!(memcmp (buf + SMS_HEADER_START - 0x4000, "TMR SEGA", 8) &&
memcmp (buf + SMS_HEADER_START - 0x4000, "TMR ALVS", 8) && // SMS
memcmp (buf + SMS_HEADER_START - 0x4000, "TMR SMSC", 8) && // SMS (unofficial)
memcmp (buf + SMS_HEADER_START - 0x4000, "TMG SEGA", 8))) // GG
return 0;
smd_deinterleave (buf, 0x4000);
if (!(memcmp (buf + SMS_HEADER_START - 0x4000, "TMR SEGA", 8) &&
memcmp (buf + SMS_HEADER_START - 0x4000, "TMR ALVS", 8) &&
memcmp (buf + SMS_HEADER_START - 0x4000, "TMR SMSC", 8) &&
memcmp (buf + SMS_HEADER_START - 0x4000, "TMG SEGA", 8)))
return 1;
return 0; // unknown, act as if it's not interleaved
}
#define SEARCHBUFSIZE (SMS_HEADER_START + 8 + 16 * 1024)
#define N_SEARCH_STR 4
static int
sms_header_len (void)
/*
At first sight it seems reasonable to also determine whether the file is
interleaved in this function. However, we run into a chicken-and-egg problem:
in order to deinterleave the data we have to know the header length. And in
order to determine the header length we have to know whether the file is
interleaved :-) Of course we could assume the header has an even size, but it
turns out that that is not always the case. For example, there is a copy of
GG Shinobi (E) [b1] floating around with a "header" of 5 bytes.
In short: this function works only for files that are not interleaved.
*/
{
// first two hacks for Majesco Game Gear BIOS (U) [!]
if (ucon64.file_size == 1024)
return 0;
else if (ucon64.file_size == 1024 + SMD_HEADER_LEN)
return SMD_HEADER_LEN;
else
{
char buffer[SEARCHBUFSIZE], *ptr, search_str[N_SEARCH_STR][9] =
{ "TMR SEGA", "TMR ALVS", "TMR SMSC", "TMG SEGA" };
int n;
ucon64_fread (buffer, 0, SEARCHBUFSIZE, ucon64.rom);
for (n = 0; n < N_SEARCH_STR; n++)
if ((ptr = (char *)
memmem2 (buffer, SEARCHBUFSIZE, search_str[n], 8, 0)) != NULL)
return ptr - buffer - SMS_HEADER_START;
n = ucon64.file_size % (16 * 1024); // SMD_HEADER_LEN
if (ucon64.file_size > n)
return n;
else
return 0;
}
}
#undef SEARCHBUFSIZE
#undef N_SEARCH_STR
int
sms_init (st_rominfo_t *rominfo)
{
int result = -1, x;
unsigned char buf[16384] = { 0 }, *rom_buffer;
is_gamegear = 0;
memset (&sms_header, 0, SMS_HEADER_LEN);
if (UCON64_ISSET (ucon64.buheader_len)) // -hd, -nhd or -hdn option was specified
rominfo->buheader_len = ucon64.buheader_len;
else
rominfo->buheader_len = sms_header_len ();
rominfo->interleaved = UCON64_ISSET (ucon64.interleaved) ?
ucon64.interleaved : sms_testinterleaved (rominfo);
if (rominfo->interleaved)
{
ucon64_fread (buf, rominfo->buheader_len + 0x4000, // header in 2nd 16 kB block
0x2000 + (SMS_HEADER_START - 0x4000 + SMS_HEADER_LEN) / 2, ucon64.rom);
smd_deinterleave (buf, 0x4000);
memcpy (&sms_header, buf + SMS_HEADER_START - 0x4000, SMS_HEADER_LEN);
}
else
ucon64_fread (&sms_header, rominfo->buheader_len + SMS_HEADER_START,
SMS_HEADER_LEN, ucon64.rom);
rominfo->header_start = SMS_HEADER_START;
rominfo->header_len = SMS_HEADER_LEN;
rominfo->header = &sms_header;
ucon64_fread (buf, 0, 11, ucon64.rom);
// Note that the identification bytes are the same as for Genesis SMD files
// The init function for Genesis files is called before this function so it
// is alright to set result to 0
if ((buf[8] == 0xaa && buf[9] == 0xbb && buf[10] == 6) ||
!(memcmp (sms_header.signature, "TMR SEGA", 8) &&
memcmp (sms_header.signature, "TMR ALVS", 8) && // SMS
memcmp (sms_header.signature, "TMR SMSC", 8) && // SMS (unofficial)
memcmp (sms_header.signature, "TMG SEGA", 8)) || // GG
ucon64.console == UCON64_SMS)
result = 0;
else
result = -1;
x = sms_header.checksum_range & 0xf0;
if (x == 0x50 || x == 0x60 || x == 0x70)
is_gamegear = 1;
switch (x)
{
case 0x30: // SMS, falling through
case 0x50: // GG
rominfo->country = "Japan";
break;
case 0x40: // SMS, falling through
case 0x70: // GG
rominfo->country = "U.S.A. & Europe";
break;
case 0x60: // GG
rominfo->country = "Japan, U.S.A. & Europe";
break;
default:
rominfo->country = "Unknown";
break;
}
if (!UCON64_ISSET (ucon64.do_not_calc_crc) && result == 0)
{
int size = ucon64.file_size - rominfo->buheader_len;
if (!(rom_buffer = (unsigned char *) malloc (size)))
{
fprintf (stderr, ucon64_msg[ROM_BUFFER_ERROR], size);
return -1;
}
ucon64_fread (rom_buffer, rominfo->buheader_len, size, ucon64.rom);
if (rominfo->interleaved)
{
ucon64.fcrc32 = crc32 (0, rom_buffer, size);
smd_deinterleave (rom_buffer, size);
}
ucon64.crc32 = crc32 (0, rom_buffer, size);
if (!is_gamegear)
{
rominfo->has_internal_crc = 1;
rominfo->internal_crc_len = 2;
rominfo->current_internal_crc = sms_chksum (rom_buffer, size);
rominfo->internal_crc = sms_header.checksum_low;
rominfo->internal_crc += sms_header.checksum_high << 8;
}
free (rom_buffer);
}
sprintf ((char *) buf, "Part number: 0x%04x\n",
sms_header.partno_low + (sms_header.partno_high << 8) +
((sms_header.version & 0xf0) << 12));
strcat (rominfo->misc, (char *) buf);
sprintf ((char *) buf, "Version: %d", sms_header.version & 0xf);
strcat (rominfo->misc, (char *) buf);
rominfo->console_usage = sms_usage[0].help;
rominfo->copier_usage = rominfo->buheader_len ? smd_usage[0].help : mgd_usage[0].help;
return result;
}
int
sms_chksum (unsigned char *rom_buffer, int rom_size)
{
unsigned short int sum;
int i, i_end;
switch (sms_header.checksum_range & 0xf)
{
case 0xc:
i_end = 0x7ff0;
break;
case 0xe: // falling through
case 0xf:
i_end = 0x20000;
break;
case 0:
i_end = 0x40000;
break;
case 1:
i_end = 0x80000;
break;
default:
i_end = rom_size;
break;
}
if (i_end > rom_size)
i_end = rom_size;
sum = 0;
for (i = 0; i < i_end; i++)
sum += rom_buffer[i];
if (i_end >= (int) (SMS_HEADER_START + SMS_HEADER_LEN))
for (i = SMS_HEADER_START; i < (int) (SMS_HEADER_START + SMS_HEADER_LEN); i++)
sum -= rom_buffer[i];
return sum;
}