shithub: rgbds

ref: c20ac350746b318e6399b1bb11d1ad7800263bef
dir: /src/fix/main.c/

View raw version
/*
 * This file is part of RGBDS.
 *
 * Copyright (c) 2010-2018, Anthony J. Bentley and RGBDS contributors.
 *
 * SPDX-License-Identifier: MIT
 */

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "extern/err.h"
#include "extern/getopt.h"

#include "version.h"

/* Short options */
static char const *optstring = "Ccf:i:jk:l:m:n:p:r:st:Vv";

/*
 * Equivalent long options
 * Please keep in the same order as short opts
 *
 * Also, make sure long opts don't create ambiguity:
 * A long opt's name should start with the same letter as its short opt,
 * except if it doesn't create any ambiguity (`verbose` versus `version`).
 * This is because long opt matching, even to a single char, is prioritized
 * over short opt matching
 */
static struct option const longopts[] = {
	{ "color-only",       no_argument,       NULL, 'C' },
	{ "color-compatible", no_argument,       NULL, 'c' },
	{ "fix-spec",         required_argument, NULL, 'f' },
	{ "game-id",          required_argument, NULL, 'i' },
	{ "non-japanese",     no_argument,       NULL, 'j' },
	{ "new-licensee",     required_argument, NULL, 'k' },
	{ "old-licensee",     required_argument, NULL, 'l' },
	{ "mbc-type",         required_argument, NULL, 'm' },
	{ "rom-version",      required_argument, NULL, 'n' },
	{ "pad-value",        required_argument, NULL, 'p' },
	{ "ram-size",         required_argument, NULL, 'r' },
	{ "sgb-compatible",   no_argument,       NULL, 's' },
	{ "title",            required_argument, NULL, 't' },
	{ "version",          no_argument,       NULL, 'V' },
	{ "validate",         no_argument,       NULL, 'v' },
	{ NULL,               no_argument,       NULL, 0   }
};

static void print_usage(void)
{
	fputs(
"Usage: rgbfix [-jsVv] [-C | -c] [-f <fix_spec>] [-i <game_id>] [-k <licensee>]\n"
"              [-l <licensee_byte>] [-m <mbc_type>] [-n <rom_version>]\n"
"              [-p <pad_value>] [-r <ram_size>] [-t <title_str>] <file>\n"
"Useful options:\n"
"    -m, --mbc-type <value>      set the MBC type byte to this value; refer\n"
"                                  to the man page for a list of values\n"
"    -p, --pad-value <value>     pad to the next valid size using this value\n"
"    -r, --ram-size <code>       set the cart RAM size byte to this value\n"
"    -V, --version               print RGBFIX version and exit\n"
"    -v, --validate              fix the header logo and both checksums (-f lhg)\n"
"\n"
"For help, use `man rgbfix' or go to https://rgbds.gbdev.io/docs/\n",
	      stderr);
	exit(1);
}

/*
 * Cartridge type names from Pan Docs, also allowing "_" instead of " "
 * and with "ROM" as an alias for "ROM ONLY".
 * https://gbdev.io/pandocs/#_0147-cartridge-type
 */
struct {
	long value;
	const char *name;
} cartridge_types[] = {
	{0x00, "ROM ONLY"},
	{0x00, "ROM_ONLY"},
	{0x00, "ROM"},
	{0x01, "MBC1"},
	{0x02, "MBC1+RAM"},
	{0x03, "MBC1+RAM+BATTERY"},
	{0x05, "MBC2"},
	{0x06, "MBC2+BATTERY"},
	{0x08, "ROM+RAM"},
	{0x09, "ROM+RAM+BATTERY"},
	{0x0B, "MMM01"},
	{0x0C, "MMM01+RAM"},
	{0x0D, "MMM01+RAM+BATTERY"},
	{0x0F, "MBC3+TIMER+BATTERY"},
	{0x10, "MBC3+TIMER+RAM+BATTERY"},
	{0x11, "MBC3"},
	{0x12, "MBC3+RAM"},
	{0x13, "MBC3+RAM+BATTERY"},
	{0x19, "MBC5"},
	{0x1A, "MBC5+RAM"},
	{0x1B, "MBC5+RAM+BATTERY"},
	{0x1C, "MBC5+RUMBLE"},
	{0x1D, "MBC5+RUMBLE+RAM"},
	{0x1E, "MBC5+RUMBLE+RAM+BATTERY"},
	{0x20, "MBC6"},
	{0x22, "MBC7+SENSOR+RUMBLE+RAM+BATTERY"},
	{0xFC, "POCKET CAMERA"},
	{0xFC, "POCKET_CAMERA"},
	{0xFD, "BANDAI TAMA5"},
	{0xFD, "BANDAI_TAMA5"},
	{0xFE, "HuC3"},
	{0xFF, "HuC1+RAM+BATTERY"},
};

static long parse_cartridge(char *arg, char **ep)
{
	for (int i = 0; i < sizeof(cartridge_types) / sizeof(cartridge_types[0]); i++) {
		if (!strcmp(arg, cartridge_types[i].name)) {
			*ep = arg + strlen(arg);
			return cartridge_types[i].value;
		}
	}
	return strtoul(arg, ep, 0);
}

int main(int argc, char *argv[])
{
	FILE *rom;
	int ch;
	char *ep;

	/*
	 * Parse command-line options
	 */

	/* all flags default to false unless options specify otherwise */
	bool fixlogo = false;
	bool fixheadsum = false;
	bool fixglobalsum = false;
	bool trashlogo = false;
	bool trashheadsum = false;
	bool trashglobalsum = false;
	bool settitle = false;
	bool setid = 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 *id = NULL; /* game ID in ASCII */
	char *newlicensee = NULL; /* new licensee ID, two ASCII characters */

	int licensee = 0;  /* old licensee ID */
	int cartridge = 0; /* cartridge hardware ID */
	int ramsize = 0;   /* RAM size ID */
	int version = 0;   /* mask ROM version number */
	int padvalue = 0;  /* to pad the rom with if it changes size */

	while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts,
					   NULL)) != -1) {
		switch (ch) {
		case 'C':
			coloronly = true;
			/* FALLTHROUGH */
		case 'c':
			colorcompatible = true;
			break;
		case 'f':
			fixlogo = strchr(optarg, 'l');
			fixheadsum = strchr(optarg, 'h');
			fixglobalsum = strchr(optarg, 'g');
			trashlogo = strchr(optarg, 'L');
			trashheadsum = strchr(optarg, 'H');
			trashglobalsum = strchr(optarg, 'G');
			break;
		case 'i':
			setid = true;

			if (strlen(optarg) != 4)
				errx(1, "Game ID %s must be exactly 4 characters",
				     optarg);

			id = optarg;
			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 = parse_cartridge(optarg, &ep);
			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':
			printf("rgbfix %s\n", get_package_version_string());
			exit(0);
		case 'v':
			fixlogo = true;
			fixheadsum = true;
			fixglobalsum = true;
			break;
		default:
			print_usage();
			/* NOTREACHED */
		}
	}

	argc -= optind;
	argv += optind;

	if (argc == 0) {
		fputs("FATAL: no input files\n", stderr);
		print_usage();
	}

	/*
	 * Open the ROM file
	 */

	rom = fopen(argv[argc - 1], "rb+");

	if (rom == NULL)
		err(1, "Error opening file %s", argv[argc - 1]);

	/*
	 * Read ROM header
	 *
	 * Offsets in the buffer are 0x100 less than the equivalent in ROM.
	 */

	uint8_t header[0x50];

	if (fseek(rom, 0x100, SEEK_SET) != 0)
		err(1, "Could not locate ROM header");
	if (fread(header, sizeof(uint8_t), sizeof(header), rom)
	    != sizeof(header))
		err(1, "Could not read ROM header");

	if (fixlogo || trashlogo) {
		/*
		 * 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 requested with the -f flag.
		 */

		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
		};

		if (trashlogo) {
			for (int i = 0; i < sizeof(ninlogo); i++)
				ninlogo[i] = ~ninlogo[i];
		}

		memcpy(header + 0x04, ninlogo, sizeof(ninlogo));
	}

	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.
		 */

		/*
		 * See also: Game ID at 0x13F–0x142. These four ASCII
		 * characters may conflict with the title.
		 */

		strncpy((char *)header + 0x34, title, 16);
	}

	if (setid) {
		/*
		 * Offset 0x13F–0x142: Game ID
		 * This is a four-character game ID in ASCII (no high-bit
		 * characters).
		 */

		memcpy(header + 0x3F, id, 4);
	}

	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.
		 */

		header[0x43] |= 1 << 7;
		if (coloronly)
			header[0x43] |= 1 << 6;

		if (header[0x43] & 0x3F)
			warnx("Color flag conflicts with game title");
	}

	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.
		 */

		header[0x44] = newlicensee[0];
		header[0x45] = newlicensee[1];
	}

	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'");

		header[0x46] = 3;
	}

	if (setcartridge) {
		/*
		 * Offset 0x147: Cartridge Type
		 * Identifies whether the ROM uses a memory bank controller,
		 * external RAM, timer, rumble, or battery.
		 */

		header[0x47] = cartridge;
	}

	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. */
		long romsize, newsize;
		int headbyte;
		uint8_t *buf;

		if (fseek(rom, 0, SEEK_END) != 0)
			err(1, "Could not pad ROM file");

		romsize = ftell(rom);
		if (romsize == -1)
			err(1, "Could not pad ROM file");

		newsize = 0x8000;

		headbyte = 0;
		while (romsize > newsize) {
			newsize <<= 1;
			headbyte++;
		}

		if (newsize > 0x800000) /* ROM is bigger than 8MiB */
			warnx("ROM size is bigger than 8MiB");

		buf = malloc(newsize - romsize);
		if (buf == NULL)
			errx(1, "Couldn't allocate memory for padded ROM.");

		memset(buf, padvalue, newsize - romsize);
		if (fwrite(buf, 1, newsize - romsize, rom) != newsize - romsize)
			err(1, "Could not pad ROM file");

		header[0x48] = headbyte;

		free(buf);
	}

	if (setramsize) {
		/*
		 * Offset 0x149: RAM Size
		 */

		header[0x49] = ramsize;
	}

	if (nonjapan) {
		/*
		 * Offset 0x14A: Non-Japanese Region Flag
		 */

		header[0x4A] = 1;
	}

	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.
		 */

		header[0x4B] = licensee;
	}

	if (setversion) {
		/*
		 * Offset 0x14C: Mask ROM Version Number
		 * Which version of the ROM this is.
		 */

		header[0x4C] = version;
	}

	if (fixheadsum || trashheadsum) {
		/*
		 * Offset 0x14D: Header Checksum
		 */

		uint8_t headcksum = 0;

		for (int i = 0x34; i < 0x4D; ++i)
			headcksum = headcksum - header[i] - 1;

		if (trashheadsum)
			headcksum = ~headcksum;

		header[0x4D] = headcksum;
	}

	/*
	 * Before calculating the global checksum, we must write the modified
	 * header to the ROM.
	 */

	if (fseek(rom, 0x100, SEEK_SET) != 0)
		err(1, "Could not locate header for writing");

	if (fwrite(header, sizeof(uint8_t), sizeof(header), rom)
	    != sizeof(header))
		err(1, "Could not write modified ROM header");

	if (fixglobalsum || trashglobalsum) {
		/*
		 * Offset 0x14E–0x14F: Global Checksum
		 */

		uint16_t globalcksum = 0;

		if (fseek(rom, 0, SEEK_SET) != 0)
			err(1, "Could not start calculating global checksum");

		int i = 0;
		int byte;

		while ((byte = fgetc(rom)) != EOF) {
			if (i != 0x14E && i != 0x14F)
				globalcksum += byte;
			i++;
		}

		if (ferror(rom))
			err(1, "Could not calculate global checksum");

		if (trashglobalsum)
			globalcksum = ~globalcksum;

		fseek(rom, 0x14E, SEEK_SET);
		fputc(globalcksum >> 8, rom);
		fputc(globalcksum & 0xFF, rom);
		if (ferror(rom))
			err(1, "Could not write global checksum");
	}

	if (fclose(rom) != 0)
		err(1, "Could not complete ROM write");

	return 0;
}