shithub: rgbds

Download patch

ref: d2f52fdd0cb02c16be354bc04bc78bb2673ea582
parent: 05be8e00113ef07aabf8a5eeb1ef98cab6a8aaec
author: Anthony J. Bentley <[email protected]>
date: Wed Dec 22 08:48:44 EST 2010

Rewrite rgbfix from scratch, under a free ISC license.

Slight changes in usage; the man page has been updated accordingly.

--- a/man/rgbfix.1
+++ b/man/rgbfix.1
@@ -7,9 +7,11 @@
 .\" SECTION
 .Sh SYNOPSIS
 .Nm rgbfix
-.Op Fl Ccdjqrsv
+.Op Fl Ccjsv
 .Op Fl k Ar licensee_str
+.Op Fl l Ar licensee_id
 .Op Fl m Ar mbc_type
+.Op Fl n Ar rom_version
 .Op Fl p Ar pad_value
 .Op Fl r Ar ram_size
 .Op Fl t Ar title_str
@@ -24,20 +26,19 @@
 .Bl -tag -width Ds
 .\" ITEM
 .It Fl C
-Set the Game Boy Color compatible flag: [0x143] = 0x80.
-This flag and the
+Set the Game Boy Color\(enonly flag: [0x143] = 0xC0.
+If both this and the
 .Fl c
-flag are mutually exclusive.
+flag are set, this takes precedence.
 .\" ITEM
 .It Fl c
-Set the Game Boy Color only flag: [0x143] = 0xC0.
-This flag and the
+Set the Game Boy Color\(encompatible flag: [0x143] = 0x80.
+If both this and the
 .Fl C
-flag are mutually exclusive.
+flag are set,
+.Fl C
+takes precedence.
 .\" ITEM
-.It Fl d
-Don't perform any changes, just pretend to for debugging.
-.\" ITEM
 .It Fl j
 Set the non-Japanese region flag: [0x14A] = 1.
 .\" ITEM
@@ -45,9 +46,16 @@
 Set the new licensee string ([0x144\(en0x145]) to a given string, truncated
 to at most two characters.
 .\" ITEM
+.It Fl l Ar licensee_id
+Set the old licensee code, [0x14B], to a given value from 0 to 0xFF.
+This value is deprecated and should be set to 0x33 in all new software.
+.\" ITEM
 .It Fl m Ar mbc_type
 Set the MBC type, [0x147], to a given value from 0 to 0xFF.
 .\" ITEM
+.It Fl n Ar rom_version
+Set the ROM version, [0x14C], to a given value from 0 to 0xFF.
+.\" ITEM
 .It Fl p Ar pad_value
 Pad the image to a valid size with a given pad value from 0 to 0xFF.
 .Nm
@@ -55,14 +63,11 @@
 give a warning thereafter.
 The cartridge size byte ([0x148]) will be changed to reflect this new size.
 .\" ITEM
-.It Fl q
-Enable quiet mode, suppressing all text except errors.
-.\" ITEM
 .It Fl r Ar ram_size
 Set the RAM size, [0x149], to a given value from 0 to 0xFF.
 .\" ITEM
 .It Fl s
-Set the SGB flag: [0x146] = 3, and set the old licensee code: [0x14B] = 0x33.
+Set the SGB flag: [0x146] = 3.
 .\" ITEM
 .It Fl t Ar title
 Set the title string ([0x134\(en0x143]) to a given string, truncated to at
@@ -97,12 +102,12 @@
 (The Game Boy itself does not use the title, but some emulators or ROM managers
 might.)
 .Pp
-.D1 $ rgbfix \-vCs \-p 0 \-t foobar baz.gb
+.D1 $ rgbfix \-vcs \-p 0 \-t foobar baz.gb
 .Pp
 The following will duplicate the header (sans global checksum) of the game
 "Survival Kids":
 .Pp
-.D1 $ rgbfix \-Cjsv \-k A4 \-m 0x1B \-p 0xFF \-r 3 \-t SURVIVALKIDAVKE SurvivalKids.gbc
+.D1 $ rgbfix \-cjsv \-k A4 \-m 0x1B \-p 0xFF \-r 3 \-t SURVIVALKIDAVKE SurvivalKids.gbc
 .\" SECTION
 .Sh SEE ALSO
 .Xr rgbds 7 ,
--- a/src/fix/main.c
+++ b/src/fix/main.c
@@ -15,18 +15,402 @@
  */
 
 #include <err.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
 #include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
 
+static void
+usage(void)
+{
+	printf("usage: rgbfix [-Ccjsv] [-k licensee_str] [-l licensee_id] "
+	    "[-m mbc_type]\n" "           [-n rom_version] [-p pad_value] "
+	    "[-r ram_size] [-t title_str] file.gb\n");
+}
+
 int
 main(int argc, char *argv[])
 {
 	FILE *rom;
+	int ch;
+	char *ep;
 
+	/*
+	 * Open the ROM file
+	 */
+
 	if (argc < 2)
-		errx(1, "Usage: rgbfix romfile");
+		usage();
 
 	if ((rom = fopen(argv[argc - 1], "rb+")) == NULL)
 		err(1, "Error opening file %s", argv[argc - 1]);
+
+	/*
+	 * Parse command-line options
+	 */
+
+	/* all flags default to false unless options specify otherwise */
+	bool validate = false;
+	bool settitle = false;
+	bool colorcompatible = false;
+	bool coloronly = false;
+	bool nonjapan = false;
+	bool setlicensee = false;
+	bool setnewlicensee = false;
+	bool super = false;
+	bool setcartridge = false;
+	bool setramsize = false;
+	bool resize = false;
+	bool setversion = false;
+
+	char *title = NULL; /* game title in ASCII */
+	char *newlicensee = NULL; /* new licensee ID, two ASCII characters */
+
+	int licensee = -1;  /* old licensee ID */
+	int cartridge = -1; /* cartridge hardware ID */
+	int ramsize = -1;   /* RAM size ID */
+	int version = -1;   /* mask ROM version number */
+	int padvalue = -1;  /* to pad the rom with if it changes size */
+
+	while ((ch = getopt(argc, argv, "Ccjk:l:m:n:p:sr:t:v")) != -1) {
+		switch (ch) {
+		case 'C':
+			coloronly = true;
+			/* FALLTHROUGH */
+		case 'c':
+			colorcompatible = true;
+			break;
+		case 'j':
+			nonjapan = true;
+			break;
+		case 'k':
+			setnewlicensee = true;
+
+			if (strlen(optarg) != 2)
+				errx(1, "New licensee code %s is not the "
+				    "correct length of 2 characters", optarg);
+
+			newlicensee = optarg;
+			break;
+		case 'l':
+			setlicensee = true;
+
+			licensee = strtoul(optarg, &ep, 0);
+			if (optarg[0] == '\0' || *ep != '\0')
+				errx(1, "Invalid argument for option 'l'");
+			if (licensee < 0 || licensee > 0xFF)
+				errx(1, "Argument for option 'l' must be "
+				    "between 0 and 255");
+			break;
+		case 'm':
+			setcartridge = true;
+
+			cartridge = strtoul(optarg, &ep, 0);
+			if (optarg[0] == '\0' || *ep != '\0')
+				errx(1, "Invalid argument for option 'm'");
+			if (cartridge < 0 || cartridge > 0xFF)
+				errx(1, "Argument for option 'm' must be "
+				    "between 0 and 255");
+			break;
+		case 'n':
+			setversion = true;
+
+			version = strtoul(optarg, &ep, 0);
+			if (optarg[0] == '\0' || *ep != '\0')
+				errx(1, "Invalid argument for option 'n'");
+			if (version < 0 || version > 0xFF)
+				errx(1, "Argument for option 'n' must be "
+				    "between 0 and 255");
+			break;
+		case 'p':
+			resize = true;
+
+			padvalue = strtoul(optarg, &ep, 0);
+			if (optarg[0] == '\0' || *ep != '\0')
+				errx(1, "Invalid argument for option 'p'");
+			if (padvalue < 0 || padvalue > 0xFF)
+				errx(1, "Argument for option 'p' must be "
+				    "between 0 and 255");
+			break;
+		case 'r':
+			setramsize = true;
+
+			ramsize = strtoul(optarg, &ep, 0);
+			if (optarg[0] == '\0' || *ep != '\0')
+				errx(1, "Invalid argument for option 'r'");
+			if (ramsize < 0 || ramsize > 0xFF)
+				errx(1, "Argument for option 'r' must be "
+				    "between 0 and 255");
+			break;
+		case 's':
+			super = true;
+			break;
+		case 't':
+			settitle = true;
+
+			if (strlen(optarg) > 16)
+				errx(1, "Title %s is greater than the "
+				    "maximum of 16 characters", optarg);
+
+			if (strlen(optarg) == 16)
+				warnx("Title %s is 16 chars, it is best "
+				    "to keep it to 15 or fewer", optarg);
+
+			title = optarg;
+			break;
+		case 'v':
+			validate = true;
+			break;
+		default:
+			usage();
+			/* NOTREACHED */
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	/*
+	 * Write changes to ROM
+	 */
+
+	if (validate) {
+		/*
+		 * Offset 0x104–0x133: Nintendo Logo
+		 * This is a bitmap image that displays when the Game Boy is
+		 * turned on. It must be intact, or the game will not boot.
+		 */
+
+		/*
+		 * See also: global checksums at 0x14D–0x14F, They must
+		 * also be correct for the game to boot, so we fix them
+		 * as well when the -v flag is set.
+		 */
+
+		uint8_t ninlogo[48] = {
+			0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
+			0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
+			0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
+			0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
+			0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
+			0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E
+		};
+
+		fseek(rom, 0x104, SEEK_SET);
+		fwrite(ninlogo, 1, 48, rom);
+	}
+
+	if (settitle) {
+		/*
+		 * Offset 0x134–0x143: Game Title
+		 * This is a sixteen-character game title in ASCII (no high-
+		 * bit characters).
+		 */
+
+		/*
+		 * See also: CGB flag at 0x143. The sixteenth character of
+		 * the title is co-opted for use as the CGB flag, so they
+		 * may conflict.
+		 */
+
+		fseek(rom, 0x134, SEEK_SET);
+		fwrite(title, 1, strlen(title) + 1, rom);
+
+		while (ftell(rom) < 0x143)
+			fputc(0, rom);
+	}
+
+	if (colorcompatible) {
+		/*
+		 * Offset 0x143: Game Boy Color Flag
+		 * If bit 7 is set, the ROM has Game Boy Color features.
+		 * If bit 6 is also set, the ROM is for the Game Boy Color
+		 * only. (However, this is not actually enforced by the
+		 * Game Boy.)
+		 */
+
+		/*
+		 * See also: Game Title at 0x134–0x143. The sixteenth
+		 * character of the title overlaps with this flag, so they
+		 * may conflict.
+		 */
+
+		uint8_t byte;
+
+		fseek(rom, 0x143, SEEK_SET);
+		byte = fgetc(rom);
+
+		byte |= 1 << 7;
+		if (coloronly)
+			byte |= 1 << 6;
+
+		if (byte & 0x3F)
+			warnx("Color flag conflicts with game title");
+
+		fseek(rom, 0x143, SEEK_SET);
+		fputc(byte, rom);
+	}
+
+	if (setnewlicensee) {
+		/*
+		 * Offset 0x144–0x145: New Licensee Code
+		 * This is a two-character code identifying which company
+		 * created the game.
+		 */
+
+		/*
+		 * See also: the original Licensee ID at 0x14B.
+		 * This is deprecated and in all newer games is used instead
+		 * as a Super Game Boy flag.
+		 */
+
+		fseek(rom, 0x144, SEEK_SET);
+		fwrite(newlicensee, 1, 2, rom);
+	}
+
+	if (super) {
+		/*
+		 * Offset 0x146: Super Game Boy Flag
+		 * If not equal to 3, Super Game Boy functions will be
+		 * disabled.
+		 */
+
+		/*
+		 * See also: the original Licensee ID at 0x14B.
+		 * If the Licensee code is not equal to 0x33, Super Game Boy
+		 * functions will be disabled.
+		 */
+
+		if (!setlicensee)
+			warnx("You should probably set both '-s' and "
+			    "'-l 0x33'");
+
+		fseek(rom, 0x146, SEEK_SET);
+		fputc(3, rom);
+	}
+
+	if (setcartridge) {
+		/*
+		 * Offset 0x147: Cartridge Type
+		 * Identifies whether the ROM uses a memory bank controller,
+		 * external RAM, timer, rumble, or battery.
+		 */
+
+		fseek(rom, 0x147, SEEK_SET);
+		fputc(cartridge, rom);
+	}
+
+	if (resize) {
+		/*
+		 * Offset 0x148: Cartridge Size
+		 * Identifies the size of the cartridge ROM.
+		 */
+
+		/* We will pad the ROM to match the size given in the header. */
+		int romsize, newsize, headbyte;
+		fseek(rom, 0, SEEK_END);
+		romsize = ftell(rom);
+		newsize = 0x8000;
+
+		headbyte = 0;
+		while (romsize > newsize) {
+			newsize <<= 1;
+			headbyte++;
+		}
+
+		while (newsize != ftell(rom)) /* ROM needs resizing */
+			fputc(padvalue, rom);
+
+		if (newsize > 0x800000) /* ROM is bigger than 8MiB */
+			warnx("ROM size is bigger than 8MiB");
+
+		fseek(rom, 0x148, SEEK_SET);
+		fputc(headbyte, rom);
+	}
+
+	if (setramsize) {
+		/*
+		 * Offset 0x149: RAM Size
+		 */
+
+		fseek(rom, 0x149, SEEK_SET);
+		fputc(ramsize, rom);
+	}
+
+	if (nonjapan) {
+		/*
+		 * Offset 0x14A: Non-Japanese Region Flag
+		 */
+
+		fseek(rom, 0x14A, SEEK_SET);
+		fputc(1, rom);
+	}
+
+	if (setlicensee) {
+		/*
+		 * Offset 0x14B: Licensee Code
+		 * This identifies which company created the game.
+		 *
+		 * This byte is deprecated and should be set to 0x33 in new
+		 * releases.
+		 */
+
+		/*
+		 * See also: the New Licensee ID at 0x144–0x145.
+		 */
+
+		fseek(rom, 0x14B, SEEK_SET);
+		fputc(licensee, rom);
+	}
+
+	if (setversion) {
+		/*
+		 * Offset 0x14C: Mask ROM Version Number
+		 * Which version of the ROM this is.
+		 */
+
+		fseek(rom, 0x14C, SEEK_SET);
+		fputc(version, rom);
+	}
+
+	if (validate) {
+		/*
+		 * Offset 0x14D: Header Checksum
+		 */
+
+		uint8_t headcksum;
+
+		headcksum = 0;
+		fseek(rom, 0x134, SEEK_SET);
+		for (int i = 0; i < (0x14D - 0x134); ++i)
+			headcksum = headcksum - fgetc(rom) - 1;
+
+		fseek(rom, 0x14D, SEEK_SET);
+		fputc(headcksum, rom);
+
+		/*
+		 * Offset 0x14E–0x14F: Global Checksum
+		 */
+
+		uint16_t globalcksum;
+
+		globalcksum = 0;
+
+		rewind(rom);
+		for (int i = 0; i < 0x14E; ++i)
+			globalcksum += fgetc(rom);
+
+		int byte;
+		fseek(rom, 0x150, SEEK_SET);
+		while ((byte = fgetc(rom)) != EOF)
+			globalcksum += byte;
+
+		fseek(rom, 0x14E, SEEK_SET);
+		fputc(globalcksum >> 8, rom);
+		fputc(globalcksum & 0xFF, rom);
+	}
 
 	fclose(rom);