/* sms.c - Sega Master System/Game Gear support for uCON64 Copyright (c) 1999 - 2001 NoisyB 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 #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #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; }