ref: 41d544a4ebb7ef7d6343bfb5a96bcbd553d57b4f
parent: f28b4abafcedefc73a8406ca8cb35c286b916866
author: ISSOtm <[email protected]>
date: Fri Dec 25 20:53:16 EST 2020
Rewrite RGBFIX - Make it work inside pipelines - Add RGBFIX tests to the suite - Be more flexible in accepted MBC names - Add warnings for dangerous or nonsensical input params - Improve man page
--- a/Makefile
+++ b/Makefile
@@ -203,7 +203,7 @@
$Qenv $(MAKE) -j WARNFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-type-limits \
-Wno-sign-compare -Wvla -Wformat -Wformat-security -Wformat-overflow=2 \
-Wformat-truncation=1 -Wformat-y2k -Wswitch-enum -Wunused \
- -Wuninitialized -Wunknown-pragmas -Wstrict-overflow=5 \
+ -Wuninitialized -Wunknown-pragmas -Wstrict-overflow=4 \
-Wstringop-overflow=4 -Walloc-zero -Wduplicated-cond \
-Wfloat-equal -Wshadow -Wcast-qual -Wcast-align -Wlogical-op \
-Wnested-externs -Wno-aggressive-loop-optimizations -Winline \
--- a/include/helpers.h
+++ b/include/helpers.h
@@ -32,6 +32,58 @@
static inline _Noreturn unreachable_(void) {}
#endif
+// Use builtins whenever possible, and shim them otherwise
+#ifdef __GNUC__ // GCC or compatible
+ #define ctz __builtin_ctz
+ #define clz __builtin_clz
+
+#elif defined(_MSC_VER)
+ #include <assert.h>
+ #include <intrin.h>
+ #pragma intrinsic(_BitScanReverse, _BitScanForward)
+ static inline int ctz(unsigned int x)
+ {
+ unsigned long cnt;
+
+ assert(x != 0);
+ _BitScanForward(&cnt, x);
+ return cnt;
+ }
+ static inline int clz(unsigned int x)
+ {
+ unsigned long cnt;
+
+ assert(x != 0);
+ _BitScanReverse(&cnt, x);
+ return 31 - cnt;
+ }
+
+#else
+ // FIXME: these are rarely used, and need testing...
+ #include <limits.h>
+ static inline int ctz(unsigned int x)
+ {
+ int cnt = 0;
+
+ while (!(x & 1)) {
+ x >>= 1;
+ cnt++;
+ }
+ return cnt;
+ }
+
+ static inline int clz(unsigned int x)
+ {
+ int cnt = 0;
+
+ while (x <= UINT_MAX / 2) {
+ x <<= 1;
+ cnt++;
+ }
+ return cnt;
+ }
+#endif
+
// Macros for stringification
#define STR(x) #x
#define EXPAND_AND_STR(x) STR(x)
--- a/include/platform.h
+++ b/include/platform.h
@@ -34,9 +34,15 @@
/* MSVC doesn't use POSIX types or defines for `read` */
#ifdef _MSC_VER
+# include <io.h>
# define STDIN_FILENO 0
+# define STDOUT_FILENO 1
+# define STDERR_FILENO 2
# define ssize_t int
# define SSIZE_MAX INT_MAX
+#else
+# include <fcntl.h>
+# include <unistd.h>
#endif
/* MSVC doesn't support `[static N]` for array arguments from C99 */
@@ -44,6 +50,24 @@
# define MIN_NB_ELMS(N)
#else
# define MIN_NB_ELMS(N) static (N)
+#endif
+
+// MSVC uses a different name for O_RDWR, and needs an additional _O_BINARY flag
+#ifdef _MSC_VER
+# include <fcntl.h>
+# define O_RDWR _O_RDWR
+# define S_ISREG(field) ((field) & _S_IFREG)
+# define O_BINARY _O_BINARY
+#elif !defined(O_BINARY) // Cross-compilers define O_BINARY
+# define O_BINARY 0 // POSIX says we shouldn't care!
+#endif // _MSC_VER
+
+// Windows has stdin and stdout open as text by default, which we may not want
+#if defined(_MSC_VER) || defined(__MINGW32__)
+# include <io.h>
+# define setmode(fd, mode) _setmode(fd, mode)
+#else
+# define setmode(fd, mode) ((void)0)
#endif
#endif /* RGBDS_PLATFORM_H */
--- a/src/fix/main.c
+++ b/src/fix/main.c
@@ -1,11 +1,18 @@
/*
* This file is part of RGBDS.
*
- * Copyright (c) 2010-2018, Anthony J. Bentley and RGBDS contributors.
+ * Copyright (c) 2020, Eldred habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <assert.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
@@ -12,13 +19,18 @@
#include <stdlib.h>
#include <string.h>
-#include "extern/err.h"
#include "extern/getopt.h"
+#include "helpers.h"
+#include "platform.h"
#include "version.h"
+#define UNSPECIFIED 0x100 // May not be in byte range
+
+#define BANK_SIZE 0x4000
+
/* Short options */
-static char const *optstring = "Ccf:i:jk:l:m:n:p:r:st:Vv";
+static const char *optstring = "Ccf:i:jk:l:m:n:p:r:st:Vv";
/*
* Equivalent long options
@@ -49,12 +61,12 @@
{ NULL, no_argument, NULL, 0 }
};
-static void print_usage(void)
+static void printUsage(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"
+" [-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"
@@ -65,535 +77,1109 @@
"\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"},
+enum MbcType {
+ ROM = 0x00,
+ ROM_RAM = 0x08,
+ ROM_RAM_BATTERY = 0x09,
+
+ MBC1 = 0x01,
+ MBC1_RAM = 0x02,
+ MBC1_RAM_BATTERY = 0x03,
+
+ MBC2 = 0x05,
+ MBC2_BATTERY = 0x06,
+
+ MMM01 = 0x0B,
+ MMM01_RAM = 0x0C,
+ MMM01_RAM_BATTERY = 0x0D,
+
+ MBC3 = 0x11,
+ MBC3_TIMER_BATTERY = 0x0F,
+ MBC3_TIMER_RAM_BATTERY = 0x10,
+ MBC3_RAM = 0x12,
+ MBC3_RAM_BATTERY = 0x13,
+
+ MBC5 = 0x19,
+ MBC5_RAM = 0x1A,
+ MBC5_RAM_BATTERY = 0x1B,
+ MBC5_RUMBLE = 0x1C,
+ MBC5_RUMBLE_RAM = 0x1D,
+ MBC5_RUMBLE_RAM_BATTERY = 0x1E,
+
+ MBC6 = 0x20,
+
+ MBC7_SENSOR_RUMBLE_RAM_BATTERY = 0x22,
+
+ POCKET_CAMERA = 0xFC,
+
+ BANDAI_TAMA5 = 0xFD,
+
+ HUC3 = 0xFE,
+
+ HUC1_RAM_BATTERY = 0xFF,
+
+ // Error values
+ MBC_NONE = UNSPECIFIED, // No MBC specified, do not act on it
+ MBC_BAD, // Specified MBC does not exist / syntax error
+ MBC_WRONG_FEATURES, // MBC incompatible with specified features
+ MBC_BAD_RANGE, // MBC number out of range
};
-static long parse_cartridge(char *arg, char **ep)
+/**
+ * @return False on failure
+ */
+static bool readMBCSlice(char const **name, char const *expected)
{
- 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;
- }
+ while (*expected) {
+ char c = *(*name)++;
+
+ if (c == '\0') // Name too short
+ return false;
+
+ if (c >= 'a' && c <= 'z') // Perform the comparison case-insensitive
+ c = c - 'a' + 'A';
+ else if (c == '_') // Treat underscores as spaces
+ c = ' ';
+
+ if (c != *expected++)
+ return false;
}
- return strtoul(arg, ep, 0);
+ return true;
}
-int main(int argc, char *argv[])
+static enum MbcType parseMBC(char const *name)
{
- FILE *rom;
- int ch;
- char *ep;
+ if (name[0] >= '0' && name[0] <= '9') {
+ // Parse number, and return it as-is (unless it's too large)
+ char *endptr;
+ unsigned long mbc = strtoul(name, &endptr, 0);
- /*
- * Parse command-line options
- */
+ if (*endptr)
+ return MBC_BAD;
+ if (mbc > 0xFF)
+ return MBC_BAD_RANGE;
+ return mbc;
- /* 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;
+ } else {
+ // Begin by reading the MBC type:
+ uint16_t mbc;
+ char const *ptr = name;
- char *title = NULL; /* game title in ASCII */
- char *id = NULL; /* game ID in ASCII */
- char *newlicensee = NULL; /* new licensee ID, two ASCII characters */
+ // Trim off leading whitespace
+ while (*ptr == ' ' || *ptr == '\t')
+ ptr++;
- 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 */
+#define tryReadSlice(expected) \
+do { \
+ if (!readMBCSlice(&ptr, expected)) \
+ return MBC_BAD; \
+} while (0)
- while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts,
- NULL)) != -1) {
- switch (ch) {
- case 'C':
- coloronly = true;
- /* FALLTHROUGH */
- case 'c':
- colorcompatible = true;
+ switch (*ptr++) {
+ case 'R': // ROM / ROM_ONLY
+ case 'r':
+ tryReadSlice("OM");
+ // Handle optional " ONLY"
+ while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+ ptr++;
+ if (*ptr == 'O' || *ptr == 'o') {
+ ptr++;
+ tryReadSlice("NLY");
+ }
+ mbc = ROM;
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');
+
+ case 'M': // MBC{1, 2, 3, 5, 6, 7} / MMM01
+ case 'm':
+ switch (*ptr++) {
+ case 'B':
+ case 'b':
+ switch (*ptr++) {
+ case 'C':
+ case 'c':
+ break;
+ default:
+ return MBC_BAD;
+ }
+ switch (*ptr++) {
+ case '1':
+ mbc = MBC1;
+ break;
+ case '2':
+ mbc = MBC2;
+ break;
+ case '3':
+ mbc = MBC3;
+ break;
+ case '5':
+ mbc = MBC5;
+ break;
+ case '6':
+ mbc = MBC6;
+ break;
+ case '7':
+ mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY;
+ break;
+ default:
+ return MBC_BAD;
+ }
+ break;
+ case 'M':
+ case 'm':
+ tryReadSlice("M01");
+ mbc = MMM01;
+ break;
+ default:
+ return MBC_BAD;
+ }
break;
- case 'i':
- setid = true;
- if (strlen(optarg) != 4)
- errx(1, "Game ID %s must be exactly 4 characters",
- optarg);
+ case 'P': // POCKET_CAMERA
+ case 'p':
+ tryReadSlice("OCKET CAMERA");
+ mbc = POCKET_CAMERA;
+ break;
- id = optarg;
+ case 'B': // BANDAI_TAMA5
+ case 'b':
+ tryReadSlice("ANDAI TAMA5");
+ mbc = BANDAI_TAMA5;
break;
- case 'j':
- nonjapan = true;
+
+ case 'T': // TAMA5
+ case 't':
+ tryReadSlice("AMA5");
+ mbc = BANDAI_TAMA5;
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;
+ case 'H': // HuC{1, 3}
+ case 'h':
+ tryReadSlice("UC");
+ switch (*ptr++) {
+ case '1':
+ mbc = HUC1_RAM_BATTERY;
+ break;
+ case '3':
+ mbc = HUC3;
+ break;
+ default:
+ return MBC_BAD;
+ }
break;
- case 'l':
- setlicensee = true;
- licensee = strtoul(optarg, &ep, 0);
- if (optarg[0] == '\0' || *ep != '\0')
- errx(1, "Invalid argument for option 'l'");
+ default:
+ return MBC_BAD;
+ }
- if (licensee < 0 || licensee > 0xFF)
- errx(1, "Argument for option 'l' must be between 0 and 255");
+ // Read "additional features"
+ uint8_t features = 0;
+#define RAM 0x80
+#define BATTERY 0x40
+#define TIMER 0x20
+#define RUMBLE 0x10
+#define SENSOR 0x08
- break;
- case 'm':
- setcartridge = true;
+ for (;;) {
+ // Trim off trailing whitespace
+ while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+ ptr++;
- cartridge = parse_cartridge(optarg, &ep);
- if (optarg[0] == '\0' || *ep != '\0')
- errx(1, "Invalid argument for option 'm'");
+ // If done, start processing "features"
+ if (!*ptr)
+ break;
+ // We expect a '+' at this point
+ if (*ptr++ != '+')
+ return MBC_BAD;
+ // Trim off leading whitespace
+ while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+ ptr++;
- if (cartridge < 0 || cartridge > 0xFF)
- errx(1, "Argument for option 'm' must be between 0 and 255");
+ switch (*ptr++) {
+ case 'B': // BATTERY
+ case 'b':
+ tryReadSlice("ATTERY");
+ features |= BATTERY;
+ break;
- break;
- case 'n':
- setversion = true;
+ case 'R': // RAM or RUMBLE
+ case 'r':
+ switch (*ptr++) {
+ case 'U':
+ case 'u':
+ tryReadSlice("MBLE");
+ features |= RUMBLE;
+ break;
+ case 'A':
+ case 'a':
+ if (*ptr != 'M' && *ptr != 'm')
+ return MBC_BAD;
+ ptr++;
+ features |= RAM;
+ break;
+ default:
+ return MBC_BAD;
+ }
+ break;
- version = strtoul(optarg, &ep, 0);
+ case 'S': // SENSOR
+ case 's':
+ tryReadSlice("ENSOR");
+ features |= SENSOR;
+ break;
- if (optarg[0] == '\0' || *ep != '\0')
- errx(1, "Invalid argument for option 'n'");
+ case 'T': // TIMER
+ case 't':
+ tryReadSlice("IMER");
+ features |= TIMER;
+ break;
- if (version < 0 || version > 0xFF)
- errx(1, "Argument for option 'n' must be between 0 and 255");
+ default:
+ return MBC_BAD;
+ }
+ }
+#undef tryReadSlice
+ switch (mbc) {
+ case ROM:
+ if (!features)
+ break;
+ mbc = ROM_RAM - 1;
+ static_assert(ROM_RAM + 1 == ROM_RAM_BATTERY, "Enum sanity check failed!");
+ static_assert(MBC1 + 1 == MBC1_RAM, "Enum sanity check failed!");
+ static_assert(MBC1 + 2 == MBC1_RAM_BATTERY, "Enum sanity check failed!");
+ static_assert(MMM01 + 1 == MMM01_RAM, "Enum sanity check failed!");
+ static_assert(MMM01 + 2 == MMM01_RAM_BATTERY, "Enum sanity check failed!");
+ // fallthrough
+ case MBC1:
+ case MMM01:
+ if (features == RAM)
+ mbc++;
+ else if (features == (RAM | BATTERY))
+ mbc += 2;
+ else if (features)
+ return MBC_WRONG_FEATURES;
break;
- case 'p':
- resize = true;
- padvalue = strtoul(optarg, &ep, 0);
+ case MBC2:
+ if (features == BATTERY)
+ mbc = MBC2_BATTERY;
+ else if (features)
+ return MBC_WRONG_FEATURES;
+ break;
- 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");
-
+ case MBC3:
+ // Handle timer, which also requires battery
+ if (features & (TIMER & BATTERY)) {
+ features &= ~(TIMER | BATTERY); // Reset those bits
+ mbc = MBC3_TIMER_BATTERY;
+ // RAM is handled below
+ }
+ static_assert(MBC3 + 1 == MBC3_RAM, "Enum sanity check failed!");
+ static_assert(MBC3 + 2 == MBC3_RAM_BATTERY, "Enum sanity check failed!");
+ static_assert(MBC3_TIMER_BATTERY + 1 == MBC3_TIMER_RAM_BATTERY,
+ "Enum sanity check failed!");
+ if (features == RAM)
+ mbc++;
+ else if (features == (RAM | BATTERY))
+ mbc += 2;
+ else if (features)
+ return MBC_WRONG_FEATURES;
break;
- case 'r':
- setramsize = true;
- ramsize = strtoul(optarg, &ep, 0);
+ case MBC5:
+ if (features & RUMBLE) {
+ features &= ~RUMBLE;
+ mbc = MBC5_RUMBLE;
+ }
+ static_assert(MBC5 + 1 == MBC5_RAM, "Enum sanity check failed!");
+ static_assert(MBC5 + 2 == MBC5_RAM_BATTERY, "Enum sanity check failed!");
+ static_assert(MBC5_RUMBLE + 1 == MBC5_RUMBLE_RAM, "Enum sanity check failed!");
+ static_assert(MBC5_RUMBLE + 2 == MBC5_RUMBLE_RAM_BATTERY,
+ "Enum sanity check failed!");
+ if (features == RAM)
+ mbc++;
+ else if (features == (RAM | BATTERY))
+ mbc += 2;
+ else if (features)
+ return MBC_WRONG_FEATURES;
+ break;
- if (optarg[0] == '\0' || *ep != '\0')
- errx(1, "Invalid argument for option 'r'");
+ case MBC6:
+ case POCKET_CAMERA:
+ case BANDAI_TAMA5:
+ case HUC3:
+ // No extra features accepted
+ if (features)
+ return MBC_WRONG_FEATURES;
+ break;
- if (ramsize < 0 || ramsize > 0xFF)
- errx(1, "Argument for option 'r' must be between 0 and 255");
+ case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+ if (features != (SENSOR | RUMBLE | RAM | BATTERY))
+ return MBC_WRONG_FEATURES;
+ break;
+ case HUC1_RAM_BATTERY:
+ if (features != (RAM | BATTERY)) // HuC1 expects RAM+BATTERY
+ return MBC_WRONG_FEATURES;
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);
+ // Trim off trailing whitespace
+ while (*ptr == ' ' || *ptr == '\t')
+ ptr++;
- if (strlen(optarg) == 16)
- warnx("Title \"%s\" is 16 chars, it is best to keep it to 15 or fewer",
- optarg);
+ // If there is still something past the whitespace, error out
+ if (*ptr)
+ return MBC_BAD;
- 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 */
- }
+ return mbc;
}
+}
- argc -= optind;
- argv += optind;
+static char const *mbcName(enum MbcType type)
+{
+ switch (type) {
+ case ROM:
+ return "ROM";
+ case ROM_RAM:
+ return "ROM+RAM";
+ case ROM_RAM_BATTERY:
+ return "ROM+RAM+BATTERY";
+ case MBC1:
+ return "MBC1";
+ case MBC1_RAM:
+ return "MBC1+RAM";
+ case MBC1_RAM_BATTERY:
+ return "MBC1+RAM+BATTERY";
+ case MBC2:
+ return "MBC2";
+ case MBC2_BATTERY:
+ return "MBC2+BATTERY";
+ case MMM01:
+ return "MMM01";
+ case MMM01_RAM:
+ return "MMM01+RAM";
+ case MMM01_RAM_BATTERY:
+ return "MMM01+RAM+BATTERY";
+ case MBC3:
+ return "MBC3";
+ case MBC3_TIMER_BATTERY:
+ return "MBC3+TIMER+BATTERY";
+ case MBC3_TIMER_RAM_BATTERY:
+ return "MBC3+TIMER+RAM+BATTERY";
+ case MBC3_RAM:
+ return "MBC3+RAM";
+ case MBC3_RAM_BATTERY:
+ return "MBC3+RAM+BATTERY";
+ case MBC5:
+ return "MBC5";
+ case MBC5_RAM:
+ return "MBC5+RAM";
+ case MBC5_RAM_BATTERY:
+ return "MBC5+RAM+BATTERY";
+ case MBC5_RUMBLE:
+ return "MBC5+RUMBLE";
+ case MBC5_RUMBLE_RAM:
+ return "MBC5+RUMBLE+RAM";
+ case MBC5_RUMBLE_RAM_BATTERY:
+ return "MBC5+RUMBLE+RAM+BATTERY";
+ case MBC6:
+ return "MBC6";
+ case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+ return "MBC7+SENSOR+RUMBLE+RAM+BATTERY";
+ case POCKET_CAMERA:
+ return "POCKET CAMERA";
+ case BANDAI_TAMA5:
+ return "BANDAI TAMA5";
+ case HUC3:
+ return "HUC3";
+ case HUC1_RAM_BATTERY:
+ return "HUC1+RAM+BATTERY";
- if (argc == 0) {
- fputs("FATAL: no input files\n", stderr);
- print_usage();
+ // Error values
+ case MBC_NONE:
+ case MBC_BAD:
+ case MBC_WRONG_FEATURES:
+ case MBC_BAD_RANGE:
+ unreachable_();
}
- /*
- * Open the ROM file
- */
+ unreachable_();
+}
- rom = fopen(argv[argc - 1], "rb+");
+static bool hasRAM(enum MbcType type)
+{
+ switch (type) {
+ case ROM:
+ case MBC1:
+ case MBC2: // Technically has RAM, but not marked as such
+ case MBC2_BATTERY:
+ case MMM01:
+ case MBC3:
+ case MBC3_TIMER_BATTERY:
+ case MBC5:
+ case MBC5_RUMBLE:
+ case MBC6: // TODO: not sure
+ case BANDAI_TAMA5: // TODO: not sure
+ case MBC_NONE:
+ case MBC_BAD:
+ case MBC_WRONG_FEATURES:
+ case MBC_BAD_RANGE:
+ return false;
- if (rom == NULL)
- err(1, "Error opening file %s", argv[argc - 1]);
+ case ROM_RAM:
+ case ROM_RAM_BATTERY:
+ case MBC1_RAM:
+ case MBC1_RAM_BATTERY:
+ case MMM01_RAM:
+ case MMM01_RAM_BATTERY:
+ case MBC3_TIMER_RAM_BATTERY:
+ case MBC3_RAM:
+ case MBC3_RAM_BATTERY:
+ case MBC5_RAM:
+ case MBC5_RAM_BATTERY:
+ case MBC5_RUMBLE_RAM:
+ case MBC5_RUMBLE_RAM_BATTERY:
+ case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+ case POCKET_CAMERA:
+ case HUC3:
+ case HUC1_RAM_BATTERY:
+ return true;
+ }
- /*
- * Read ROM header
- *
- * Offsets in the buffer are 0x100 less than the equivalent in ROM.
- */
+ unreachable_();
+}
- uint8_t header[0x50];
+static uint8_t nbErrors;
- 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");
+static format_(printf, 1, 2) void report(char const *fmt, ...)
+{
+ va_list ap;
- 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.
- */
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
- /*
- * 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.
- */
+ if (nbErrors != UINT8_MAX)
+ nbErrors++;
+}
- 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
- };
+static const uint8_t ninLogo[] = {
+ 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];
- }
+static enum { DMG, BOTH, CGB } model = DMG; // If DMG, byte is left alone
+#define FIX_LOGO 0x80
+#define TRASH_LOGO 0x40
+#define FIX_HEADER_SUM 0x20
+#define TRASH_HEADER_SUM 0x10
+#define FIX_GLOBAL_SUM 0x08
+#define TRASH_GLOBAL_SUM 0x04
+static uint8_t fixSpec = 0;
+static const char *gameID = NULL;
+static uint8_t gameIDLen;
+static bool japanese = true;
+static const char *newLicensee = NULL;
+static uint8_t newLicenseeLen;
+static uint16_t oldLicensee = UNSPECIFIED;
+static enum MbcType cartridgeType = MBC_NONE;
+static uint16_t romVersion = UNSPECIFIED;
+static uint16_t padValue = UNSPECIFIED;
+static uint16_t ramSize = UNSPECIFIED;
+static bool sgb = false; // If false, SGB flags are left alone
+static const char *title = NULL;
+static uint8_t titleLen;
- memcpy(header + 0x04, ninlogo, sizeof(ninlogo));
- }
+static uint8_t maxTitleLen(void)
+{
+ return gameID ? 11 : model != DMG ? 15 : 16;
+}
- if (settitle) {
- /*
- * Offset 0x134–0x143: Game Title
- * This is a sixteen-character game title in ASCII (no high-
- * bit characters).
- */
+static ssize_t readBytes(int fd, uint8_t *buf, size_t len)
+{
+ // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
+ assert(len <= SSIZE_MAX);
- /*
- * 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.
- */
+ ssize_t total = 0;
- /*
- * See also: Game ID at 0x13F–0x142. These four ASCII
- * characters may conflict with the title.
- */
+ while (len) {
+ ssize_t ret = read(fd, buf, len);
- strncpy((char *)header + 0x34, title, 16);
+ if (ret == -1 && errno != EINTR) // Return errors, unless we only were interrupted
+ return -1;
+ // EOF reached
+ if (ret == 0)
+ return total;
+ // If anything was read, accumulate it, and continue
+ if (ret != -1) {
+ total += ret;
+ len -= ret;
+ buf += ret;
+ }
}
- if (setid) {
- /*
- * Offset 0x13F–0x142: Game ID
- * This is a four-character game ID in ASCII (no high-bit
- * characters).
- */
+ return total;
+}
- memcpy(header + 0x3F, id, 4);
+static ssize_t writeBytes(int fd, void *buf, size_t len)
+{
+ // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
+ assert(len <= SSIZE_MAX);
+
+ ssize_t total = 0;
+
+ while (len) {
+ ssize_t ret = write(fd, buf, len);
+
+ if (ret == -1 && errno != EINTR) // Return errors, unless we only were interrupted
+ return -1;
+ // EOF reached
+ if (ret == 0)
+ return total;
+ // If anything was read, accumulate it, and continue
+ if (ret != -1) {
+ total += ret;
+ len -= ret;
+ }
}
- 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.)
- */
+ return total;
+}
- /*
- * See also: Game Title at 0x134–0x143. The sixteenth
- * character of the title overlaps with this flag, so they
- * may conflict.
- */
+/**
+ * @param input File descriptor to be used for reading
+ * @param output File descriptor to be used for writing, may be equal to `input`
+ * @param name The file's name, to be displayed for error output
+ * @param fileSize The file's size if known, 0 if not.
+ */
+static void processFile(int input, int output, char const *name, off_t fileSize)
+{
+ // Both of these should be true for seekable files, and neither otherwise
+ if (input == output)
+ assert(fileSize != 0);
+ else
+ assert(fileSize == 0);
- header[0x43] |= 1 << 7;
- if (coloronly)
- header[0x43] |= 1 << 6;
+ uint8_t rom0[BANK_SIZE];
+ ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0));
- if (header[0x43] & 0x3F)
- warnx("Color flag conflicts with game title");
+ if (rom0Len == -1) {
+ report("FATAL: Failed to read \"%s\"'s header: %s\n", name, strerror(errno));
+ return;
+ } else if (rom0Len < 0x150) {
+ report("FATAL: \"%s\" too short, expected at least 336 ($150) bytes, got only %ld\n",
+ name, rom0Len);
+ return;
}
+ // Accept partial reads if the file contains at least the header
- if (setnewlicensee) {
- /*
- * Offset 0x144–0x145: New Licensee Code
- * This is a two-character code identifying which company
- * created the game.
- */
+ if (fixSpec & (FIX_LOGO | TRASH_LOGO)) {
+ if (fixSpec & FIX_LOGO) {
+ memcpy(&rom0[0x104], ninLogo, sizeof(ninLogo));
+ } else {
+ for (uint8_t i = 0; i < sizeof(ninLogo); i++)
+ rom0[i + 0x104] = ~ninLogo[i];
+ }
+ }
- /*
- * 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.
- */
+ if (title)
+ memcpy(&rom0[0x134], title, titleLen);
- header[0x44] = newlicensee[0];
- header[0x45] = newlicensee[1];
- }
+ if (gameID)
+ memcpy(&rom0[0x13f], gameID, gameIDLen);
- if (super) {
- /*
- * Offset 0x146: Super Game Boy Flag
- * If not equal to 3, Super Game Boy functions will be
- * disabled.
- */
+ if (model != DMG)
+ rom0[0x143] = model == BOTH ? 0x80 : 0xc0;
- /*
- * 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 (newLicensee)
+ memcpy(&rom0[0x144], newLicensee, newLicenseeLen);
- if (!setlicensee)
- warnx("You should probably set both '-s' and '-l 0x33'");
+ if (sgb)
+ rom0[0x146] = 0x03;
- header[0x46] = 3;
- }
+ // If a valid MBC was specified...
+ if (cartridgeType < MBC_NONE)
+ rom0[0x147] = cartridgeType;
- if (setcartridge) {
- /*
- * Offset 0x147: Cartridge Type
- * Identifies whether the ROM uses a memory bank controller,
- * external RAM, timer, rumble, or battery.
- */
+ if (ramSize != UNSPECIFIED)
+ rom0[0x149] = ramSize;
- header[0x47] = cartridge;
- }
+ if (!japanese)
+ rom0[0x14a] = 0x01;
- if (resize) {
- /*
- * Offset 0x148: Cartridge Size
- * Identifies the size of the cartridge ROM.
- */
+ if (oldLicensee != UNSPECIFIED)
+ rom0[0x14b] = oldLicensee;
- /* We will pad the ROM to match the size given in the header. */
- long romsize, newsize;
- int headbyte;
- uint8_t *buf;
+ if (romVersion != UNSPECIFIED)
+ rom0[0x14c] = romVersion;
- if (fseek(rom, 0, SEEK_END) != 0)
- err(1, "Could not pad ROM file");
+ // Remain to be handled the ROM size, and header checksum.
+ // The latter depends on the former, and so will be handled after it.
+ // The former requires knowledge of the file's total size, so read that first.
- romsize = ftell(rom);
- if (romsize == -1)
- err(1, "Could not pad ROM file");
+ uint16_t globalSum = 0;
- newsize = 0x8000;
+ // To keep file sizes fairly reasonable, we'll cap the amount of banks at 65536
+ // Official mappers only go up to 512 banks, but at least the TPP1 spec allows up to
+ // 65536 banks = 1 GiB.
+ // This should be reasonable for the time being, and may be extended later.
+ uint8_t *romx = NULL; // Pointer to ROMX banks when they've been buffered
+ uint32_t nbBanks = 1; // Number of banks *targeted*, including ROM0
+ size_t totalRomxLen = 0; // *Actual* size of ROMX data
+ uint8_t bank[BANK_SIZE]; // Temp buffer used to store a whole bank's worth of data
- headbyte = 0;
- while (romsize > newsize) {
- newsize <<= 1;
- headbyte++;
+ // Handle ROMX
+ if (input == output) {
+ if (fileSize >= 0x10000 * BANK_SIZE) {
+ report("FATAL: \"%s\" has more than 65536 banks\n", name);
+ return;
}
+ // This should be guaranteed from the size cap...
+ static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
+ // Compute number of banks and ROMX len from file size
+ nbBanks = (fileSize + (BANK_SIZE - 1)) / BANK_SIZE;
+ // = ceil(totalRomxLen / BANK_SIZE)
+ totalRomxLen = fileSize >= BANK_SIZE ? fileSize - BANK_SIZE : 0;
+ } else if (rom0Len == BANK_SIZE) {
+ // Copy ROMX when reading a pipe, and we're not at EOF yet
+ for (;;) {
+ romx = realloc(romx, nbBanks * BANK_SIZE);
+ if (!romx) {
+ report("FATAL: Failed to realloc ROMX buffer: %s\n",
+ strerror(errno));
+ return;
+ }
+ ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE],
+ BANK_SIZE);
- if (newsize > 0x800000) /* ROM is bigger than 8MiB */
- warnx("ROM size is bigger than 8MiB");
+ // Update bank count, ONLY IF at least one byte was read
+ if (bankLen) {
+ // We're gonna read another bank, check that it won't be too much
+ static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
+ if (nbBanks == 0x10000) {
+ report("FATAL: \"%s\" has more than 65536 banks\n", name);
+ free(romx);
+ return;
+ }
+ nbBanks++;
- buf = malloc(newsize - romsize);
- if (buf == NULL)
- errx(1, "Couldn't allocate memory for padded ROM.");
+ // Update global checksum, too
+ for (uint16_t i = 0; i < bankLen; i++)
+ globalSum += romx[totalRomxLen + i];
+ totalRomxLen += bankLen;
+ }
+ // Stop when an incomplete bank has been read
+ if (bankLen != BANK_SIZE)
+ break;
+ }
+ }
- memset(buf, padvalue, newsize - romsize);
- if (fwrite(buf, 1, newsize - romsize, rom) != newsize - romsize)
- err(1, "Could not pad ROM file");
+ // Handle setting the ROM size if padding was requested
+ // Pad to the next valid power of 2. This is because padding is required by flashers, which
+ // flash to ROM chips, whose size is always a power of 2... so there'd be no point in
+ // padding to something else.
+ // Additionally, a ROM must be at least 32k, so we guarantee a whole amount of banks...
+ if (padValue != UNSPECIFIED) {
+ // We want at least 2 banks
+ if (nbBanks == 1) {
+ if (rom0Len != sizeof(rom0)) {
+ memset(&rom0[rom0Len], padValue, sizeof(rom0) - rom0Len);
+ // The global checksum hasn't taken ROM0 into consideration yet!
+ // ROM0 was padded, so treat it as entirely written: update its size
+ // Update how many bytes were read in total, too
+ rom0Len = sizeof(rom0);
+ }
+ nbBanks = 2;
+ } else {
+ assert(rom0Len == sizeof(rom0));
+ }
+ assert(nbBanks >= 2);
+ // Alter number of banks to reflect required value
+ // x&(x-1) is zero iff x is a power of 2, or 0; we know for sure it's non-zero,
+ // so this is true (non-zero) when we don't have a power of 2
+ if (nbBanks & (nbBanks - 1))
+ nbBanks = 1 << (CHAR_BIT * sizeof(nbBanks) - clz(nbBanks));
+ // Write final ROM size
+ rom0[0x148] = ctz(nbBanks / 2);
+ // Alter global checksum based on how many bytes will be added (not counting ROM0)
+ globalSum += padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen);
+ }
- header[0x48] = headbyte;
+ // Handle the header checksum after the ROM size has been written
+ if (fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) {
+ uint8_t sum = 0;
- free(buf);
+ for (uint16_t i = 0x134; i < 0x14d; i++)
+ sum -= rom0[i] + 1;
+ rom0[0x14d] = fixSpec & TRASH_HEADER_SUM ? ~sum : sum;
}
- if (setramsize) {
- /*
- * Offset 0x149: RAM Size
- */
+ if (fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) {
+ // Computation of the global checksum assumes 0s being stored in its place
+ rom0[0x14e] = 0;
+ rom0[0x14f] = 0;
+ for (uint16_t i = 0; i < rom0Len; i++)
+ globalSum += rom0[i];
+ // Pipes have already read ROMX and updated globalSum, but not regular files
+ if (input == output) {
+ for (;;) {
+ ssize_t romxLen = readBytes(input, bank, sizeof(bank));
- header[0x49] = ramsize;
+ for (uint16_t i = 0; i < romxLen; i++)
+ globalSum += bank[i];
+ if (romxLen != sizeof(bank))
+ break;
+ }
+ }
+
+ if (fixSpec & TRASH_GLOBAL_SUM)
+ globalSum = ~globalSum;
+ rom0[0x14e] = globalSum >> 8;
+ rom0[0x14f] = globalSum & 0xff;
}
- if (nonjapan) {
- /*
- * Offset 0x14A: Non-Japanese Region Flag
- */
+ // In case the output depends on the input, reset to the beginning of the file, and only
+ // write the header
+ if (input == output) {
+ if (lseek(output, 0, SEEK_SET) == (off_t)-1) {
+ report("FATAL: Failed to rewind \"%s\": %s\n", name, strerror(errno));
+ goto free_romx;
+ }
+ // If modifying the file in-place, we only need to edit the header
+ // However, padding may have modified ROM0 (added padding), so don't in that case
+ if (padValue == UNSPECIFIED)
+ rom0Len = 0x150;
+ }
+ ssize_t writeLen = writeBytes(output, rom0, rom0Len);
- header[0x4A] = 1;
+ if (writeLen == -1) {
+ report("FATAL: Failed to write \"%s\"'s ROM0: %s\n", name, strerror(errno));
+ goto free_romx;
+ } else if (writeLen < rom0Len) {
+ report("FATAL: Could only write %ld of \"%s\"'s %ld ROM0 bytes\n",
+ writeLen, name, rom0Len);
+ goto free_romx;
}
- 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.
- */
+ // Output ROMX if it was buffered
+ if (romx) {
+ writeLen = writeBytes(output, romx, totalRomxLen);
+ if (writeLen == -1) {
+ report("FATAL: Failed to write \"%s\"'s ROMX: %s\n", name, strerror(errno));
+ goto free_romx;
+ } else if (writeLen < totalRomxLen) {
+ report("FATAL: Could only write %ld of \"%s\"'s %ld ROMX bytes\n",
+ writeLen, name, totalRomxLen);
+ goto free_romx;
+ }
+ }
- /*
- * See also: the New Licensee ID at 0x144–0x145.
- */
+ // Output padding
+ if (padValue != UNSPECIFIED) {
+ if (input == output) {
+ if (lseek(output, 0, SEEK_END) == (off_t)-1) {
+ report("FATAL: Failed to seek to end of \"%s\": %s\n",
+ name, strerror(errno));
+ goto free_romx;
+ }
+ }
+ memset(bank, padValue, sizeof(bank));
+ size_t len = (nbBanks - 1) * BANK_SIZE - totalRomxLen; // Don't count ROM0!
- header[0x4B] = licensee;
+ while (len) {
+ static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading");
+ size_t thisLen = len > sizeof(bank) ? sizeof(bank) : len;
+ ssize_t ret = writeBytes(output, bank, thisLen);
+
+ if (ret != thisLen) {
+ report("FATAL: Failed to write \"%s\"'s padding: %s\n",
+ name, strerror(errno));
+ break;
+ }
+ len -= thisLen;
+ }
}
- if (setversion) {
- /*
- * Offset 0x14C: Mask ROM Version Number
- * Which version of the ROM this is.
- */
+free_romx:
+ free(romx);
+}
- header[0x4C] = version;
- }
+#undef trySeek
- if (fixheadsum || trashheadsum) {
- /*
- * Offset 0x14D: Header Checksum
- */
+static bool processFilename(char const *name)
+{
+ nbErrors = 0;
+ if (!strcmp(name, "-")) {
+ setmode(STDIN_FILENO, O_BINARY);
+ setmode(STDOUT_FILENO, O_BINARY);
+ name = "<stdin>";
+ processFile(STDIN_FILENO, STDOUT_FILENO, name, 0);
- uint8_t headcksum = 0;
+ } else {
+ // POSIX specifies that the results of O_RDWR on a FIFO are undefined.
+ // However, this is necessary to avoid a TOCTTOU, if the file was changed between
+ // `stat()` and `open(O_RDWR)`, which could trigger the UB anyway.
+ // Thus, we're going to hope that either the `open` fails, or it succeeds but IO
+ // operations may fail, all of which we handle.
+ int input = open(name, O_RDWR | O_BINARY);
+ struct stat stat;
- for (int i = 0x34; i < 0x4D; ++i)
- headcksum = headcksum - header[i] - 1;
+ if (input == -1) {
+ report("FATAL: Failed to open \"%s\" for reading+writing: %s\n",
+ name, strerror(errno));
+ goto fail;
+ }
- if (trashheadsum)
- headcksum = ~headcksum;
+ if (fstat(input, &stat) == -1) {
+ report("FATAL: Failed to stat \"%s\": %s\n", name, strerror(errno));
+ } else if (!S_ISREG(stat.st_mode)) { // FIXME: Do we want to support other types?
+ report("FATAL: \"%s\" is not a regular file, and thus cannot be modified in-place\n",
+ name);
+ } else if (stat.st_size < 0x150) {
+ // This check is in theory redundant with the one in `processFile`, but it
+ // prevents passing a file size of 0, which usually indicates pipes
+ report("FATAL: \"%s\" too short, expected at least 336 ($150) bytes, got only %ld\n",
+ name, stat.st_size);
+ } else {
+ processFile(input, input, name, stat.st_size);
+ }
- header[0x4D] = headcksum;
+ close(input);
}
+ if (nbErrors)
+fail:
+ fprintf(stderr, "Fixing \"%s\" failed with %u error%s\n",
+ name, nbErrors, nbErrors == 1 ? "" : "s");
+ return nbErrors;
+}
- /*
- * Before calculating the global checksum, we must write the modified
- * header to the ROM.
- */
+int main(int argc, char *argv[])
+{
+ nbErrors = 0;
+ char ch;
- if (fseek(rom, 0x100, SEEK_SET) != 0)
- err(1, "Could not locate header for writing");
+ while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts, NULL)) != -1) {
+ switch (ch) {
+ size_t len;
+#define parseByte(output, name) \
+do { \
+ char *endptr; \
+ unsigned long tmp; \
+ \
+ if (optarg[0] == 0) { \
+ report("error: Argument to option '" name "' may not be empty\n"); \
+ } else { \
+ if (optarg[0] == '$') { \
+ tmp = strtoul(&optarg[1], &endptr, 16); \
+ } else { \
+ tmp = strtoul(optarg, &endptr, 0); \
+ } \
+ if (*endptr) \
+ report("error: Expected number as argument to option '" name "', got %s\n", \
+ optarg); \
+ else if (tmp > 0xFF) \
+ report("error: Argument to option '" name "' is larger than 255: %lu\n", tmp); \
+ else \
+ output = tmp; \
+ } \
+} while (0)
- if (fwrite(header, sizeof(uint8_t), sizeof(header), rom)
- != sizeof(header))
- err(1, "Could not write modified ROM header");
+ case 'C':
+ case 'c':
+ model = ch == 'c' ? BOTH : CGB;
+ if (titleLen > 15) {
+ titleLen = 15;
+ fprintf(stderr, "warning: Truncating title \"%s\" to 15 chars\n",
+ title);
+ }
+ break;
- if (fixglobalsum || trashglobalsum) {
- /*
- * Offset 0x14E–0x14F: Global Checksum
- */
+ case 'f':
+ fixSpec = 0;
+ while (*optarg) {
+ switch (*optarg) {
+#define SPEC_l FIX_LOGO
+#define SPEC_L TRASH_LOGO
+#define SPEC_h FIX_HEADER_SUM
+#define SPEC_H TRASH_HEADER_SUM
+#define SPEC_g FIX_GLOBAL_SUM
+#define SPEC_G TRASH_GLOBAL_SUM
+#define or(new, bad) \
+do { \
+ if (fixSpec & SPEC_##bad) \
+ fprintf(stderr, \
+ "warning: '" #new "' overriding '" #bad "' in fix spec\n"); \
+ fixSpec = (fixSpec & ~SPEC_##bad) | SPEC_##new; \
+} while (0)
+ case 'l':
+ or(l, L);
+ break;
+ case 'L':
+ or(L, l);
+ break;
- uint16_t globalcksum = 0;
+ case 'h':
+ or(h, H);
+ break;
+ case 'H':
+ or(H, h);
+ break;
- if (fseek(rom, 0, SEEK_SET) != 0)
- err(1, "Could not start calculating global checksum");
+ case 'g':
+ or(g, G);
+ break;
+ case 'G':
+ or(G, g);
+ break;
- int i = 0;
- int byte;
+ default:
+ fprintf(stderr, "warning: Ignoring '%c' in fix spec\n",
+ *optarg);
+#undef or
+ }
+ optarg++;
+ }
+ break;
- while ((byte = fgetc(rom)) != EOF) {
- if (i != 0x14E && i != 0x14F)
- globalcksum += byte;
- i++;
- }
+ case 'i':
+ gameID = optarg;
+ len = strlen(gameID);
+ if (len > 4) {
+ len = 4;
+ fprintf(stderr, "warning: Truncating game ID \"%s\" to 4 chars\n",
+ gameID);
+ }
+ gameIDLen = len;
+ if (titleLen > 11) {
+ titleLen = 11;
+ fprintf(stderr, "warning: Truncating title \"%s\" to 11 chars\n",
+ title);
+ }
+ break;
- if (ferror(rom))
- err(1, "Could not calculate global checksum");
+ case 'j':
+ japanese = false;
+ break;
- if (trashglobalsum)
- globalcksum = ~globalcksum;
+ case 'k':
+ newLicensee = optarg;
+ len = strlen(newLicensee);
+ if (len > 2) {
+ len = 2;
+ fprintf(stderr,
+ "warning: Truncating new licensee \"%s\" to 2 chars\n",
+ newLicensee);
+ }
+ newLicenseeLen = len;
+ break;
- fseek(rom, 0x14E, SEEK_SET);
- fputc(globalcksum >> 8, rom);
- fputc(globalcksum & 0xFF, rom);
- if (ferror(rom))
- err(1, "Could not write global checksum");
+ case 'l':
+ parseByte(oldLicensee, "l");
+ break;
+
+ case 'm':
+ cartridgeType = parseMBC(optarg);
+ if (cartridgeType == MBC_BAD) {
+ report("error: Unknown MBC \"%s\"\n", optarg);
+ } else if (cartridgeType == MBC_WRONG_FEATURES) {
+ report("error: Features incompatible with MBC (\"%s\")\n", optarg);
+ } else if (cartridgeType == MBC_BAD_RANGE) {
+ report("error: Specified MBC ID out of range 0-255: %s\n",
+ optarg);
+ } else if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
+ fprintf(stderr, "warning: ROM+RAM / ROM+RAM+BATTERY are under-specified and poorly supported\n");
+ }
+ break;
+
+ case 'n':
+ parseByte(romVersion, "n");
+ break;
+
+ case 'p':
+ parseByte(padValue, "p");
+ break;
+
+ case 'r':
+ parseByte(ramSize, "r");
+ break;
+
+ case 's':
+ sgb = true;
+ break;
+
+ case 't':
+ title = optarg;
+ len = strlen(title);
+ uint8_t maxLen = maxTitleLen();
+
+ if (len > maxLen) {
+ len = maxLen;
+ fprintf(stderr, "warning: Truncating title \"%s\" to %u chars\n",
+ title, maxLen);
+ }
+ titleLen = len;
+ break;
+
+ case 'V':
+ printf("rgbfix %s\n", get_package_version_string());
+ exit(0);
+
+ case 'v':
+ fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM;
+ break;
+
+ default:
+ fprintf(stderr, "FATAL: unknown option '%c'\n", ch);
+ printUsage();
+ exit(1);
+ }
+#undef parseByte
}
- if (fclose(rom) != 0)
- err(1, "Could not complete ROM write");
+ if (ramSize != UNSPECIFIED && cartridgeType < UNSPECIFIED) {
+ if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
+ if (ramSize != 1)
+ fprintf(stderr, "warning: MBC \"%s\" should have 2kiB of RAM (-r 1)\n",
+ mbcName(cartridgeType));
+ } else if (hasRAM(cartridgeType)) {
+ if (!ramSize) {
+ fprintf(stderr,
+ "warning: MBC \"%s\" has RAM, but RAM size was set to 0\n",
+ mbcName(cartridgeType));
+ } else if (ramSize == 1) {
+ fprintf(stderr,
+ "warning: RAM size 1 (2 kiB) was specified for MBC \"%s\"\n",
+ mbcName(cartridgeType));
+ } // TODO: check possible values?
+ } else if (ramSize) {
+ fprintf(stderr,
+ "warning: MBC \"%s\" has no RAM, but RAM size was set to %u\n",
+ mbcName(cartridgeType), ramSize);
+ }
+ }
- return 0;
+ if (sgb && oldLicensee != UNSPECIFIED && oldLicensee != 0x33)
+ fprintf(stderr,
+ "warning: SGB compatibility enabled, but old licensee is %#x, not 0x33\n",
+ oldLicensee);
+
+ argv += optind;
+ bool failed = nbErrors;
+
+ if (!*argv) {
+ failed |= processFilename("-");
+ } else {
+ do {
+ failed |= processFilename(*argv);
+ } while (*++argv);
+ }
+
+ return failed;
}
--- a/src/fix/rgbfix.1
+++ b/src/fix/rgbfix.1
@@ -24,39 +24,48 @@
.Op Fl p Ar pad_value
.Op Fl r Ar ram_size
.Op Fl t Ar title_str
-.Ar file
+.Op Ar
.Sh DESCRIPTION
The
.Nm
-program changes headers of Game Boy ROM images.
+program changes headers of Game Boy ROM images, typically generated by
+.Xr rgblink 1 ,
+though it will work with
+.Em any
+Game Boy ROM.
It also performs other correctness operations, such as padding.
+.Nm
+only changes the fields for which it has values specified.
+Developers are advised to fill those fields with 0x00 bytes in their source code before running
+.Nm ,
+and to have already populated whichever fields they don't specify using
+.Nm .
.Pp
Note that options can be abbreviated as long as the abbreviation is unambiguous:
-.Fl Fl verb
+.Fl Fl color-o
is
-.Fl Fl verbose ,
+.Fl Fl color-only ,
but
-.Fl Fl ver
+.Fl Fl color
is invalid because it could also be
-.Fl Fl version .
-The arguments are as follows:
+.Fl Fl color-compatible .
+Options later in the command line override those set earlier.
+Accepted options are as follows:
.Bl -tag -width Ds
.It Fl C , Fl Fl color-only
-Set the Game Boy Color\(enonly flag:
-.Ad 0x143
-= 0xC0.
-If both this and the
+Set the Game Boy Color\(enonly flag
+.Pq Ad 0x143
+to 0xC0.
+This overrides
.Fl c
-flag are set, this takes precedence.
+if it was set prior.
.It Fl c , Fl Fl color-compatible
Set the Game Boy Color\(encompatible flag:
-.Ad 0x143
-= 0x80.
-If both this and the
-.Fl C
-flag are set,
-.Fl C
-takes precedence.
+.Pq Ad 0x143
+to 0x80.
+This overrides
+.Fl c
+if it was set prior.
.It Fl f Ar fix_spec , Fl Fl fix-spec Ar fix_spec
Fix certain header values that the Game Boy checks for correctness.
Alternatively, intentionally trash these values by writing their binary inverse instead.
@@ -83,55 +92,63 @@
.It Fl i Ar game_id , Fl Fl game-id Ar game_id
Set the game ID string
.Pq Ad 0x13F Ns \(en Ns Ad 0x142
-to a given string of exactly 4 characters.
-If both this and the title are set, the game ID will overwrite the overlapping portion of the title.
+to a given string.
+If it's longer than 4 chars, it will be truncated, and a warning emitted.
.It Fl j , Fl Fl non-japanese
-Set the non-Japanese region flag:
-.Ad 0x14A
-= 1.
+Set the non-Japanese region flag
+.Pq Ad 0x14A
+to 0x01.
.It Fl k Ar licensee_str , Fl Fl new-licensee Ar licensee_str
Set the new licensee string
.Pq Ad 0x144 Ns \(en Ns Ad 0x145
-to a given string, truncated to at most two characters.
+to a given string.
+If it's longer than 2 chars, it will be truncated, and a warning emitted.
.It Fl l Ar licensee_id , Fl Fl old-licensee Ar licensee_id
-Set the old licensee code,
-.Ad 0x14B ,
+Set the old licensee code
+.Pq Ad 0x14B
to a given value from 0 to 0xFF.
This value is deprecated and should be set to 0x33 in all new software.
.It Fl m Ar mbc_type , Fl Fl mbc-type Ar mbc_type
-Set the MBC type,
-.Ad 0x147 ,
+Set the MBC type
+.Pq Ad 0x147
to a given value from 0 to 0xFF.
This value may also be an MBC name from the Pan Docs.
.It Fl n Ar rom_version , Fl Fl rom-version Ar rom_version
-Set the ROM version,
-.Ad 0x14C ,
+Set the ROM version
+.Pq Ad 0x14C
to a given value from 0 to 0xFF.
.It Fl p Ar pad_value , Fl Fl pad-value Ar pad_value
-Pad the image to a valid size with a given pad value from 0 to 0xFF.
+Pad the ROM image to a valid size with a given pad value from 0 to 255 (0xFF).
.Nm
will automatically pick a size from 32 KiB, 64 KiB, 128 KiB, ..., 8192 KiB.
The cartridge size byte
.Pq Ad 0x148
will be changed to reflect this new size.
+The recommended padding value is 0xFF, to speed up writing the ROM to flash chips, and to avoid "nop slides" into VRAM.
.It Fl r Ar ram_size , Fl Fl ram-size Ar ram_size
-Set the RAM size,
-.Ad 0x149 ,
+Set the RAM size
+.Pq Ad 0x149
to a given value from 0 to 0xFF.
.It Fl s , Fl Fl sgb-compatible
-Set the SGB flag:
-.Ad 0x146
-= 3. This flag will be ignored by the SGB unless the old licensee code is 0x33!
+Set the SGB flag
+.Pq Ad 0x146
+to 0x03.
+This flag will be ignored by the SGB unless the old licensee code is 0x33!
+If this is given as well as
+.Fl l ,
+but is not set to 0x33, a warning will be printed.
.It Fl t Ar title , Fl Fl title Ar title
Set the title string
.Pq Ad 0x134 Ns \(en Ns Ad 0x143
-to a given string, truncated to at most 16 characters.
-It is recommended to use 15 characters instead, to avoid clashing with the CGB flag
-.Po Fl c
+to a given string.
+If the title is longer than the max length, it will be truncated, and a warning emitted.
+The max length is 11 characters if the game ID
+.Pq Fl i
+is specified, 15 characters if the CGB flag
+.Fl ( c
or
-.Fl C
-.Pc .
-If both this and the game ID are set, the game ID will overwrite the overlapping portion of the title.
+.Fl C )
+is specified but the game ID is not, and 16 characters otherwise.
.It Fl V , Fl Fl version
Print the version of the program and exit.
.It Fl v , Fl Fl validate
@@ -139,7 +156,7 @@
.Fl f Cm lhg .
.El
.Sh EXAMPLES
-Most values in the ROM header are only cosmetic.
+Most values in the ROM header do not matter to the actual console, and most are seldom useful anyway.
The bare minimum requirements for a workable program are the header checksum, the Nintendo logo, and (if needed) the CGB/SGB flags.
It is a good idea to pad the image to a valid size as well
.Pq Do valid Dc meaning a power of 2, times 32 KiB .
@@ -152,14 +169,13 @@
The following will make a SGB-enabled, color-enabled game with a title of
.Dq foobar ,
and pad it to a valid size.
-.Po
-The Game Boy itself does not use the title, but some emulators or ROM managers do.
-.Pc
+.Pq The Game Boy itself does not use the title, but some emulators or ROM managers do.
.Pp
.D1 $ rgbfix -vcs -l 0x33 -p 255 -t foobar baz.gb
.Pp
-The following will duplicate the header (sans global checksum) of the game
-.Dq Survival Kids :
+The following will duplicate the header of the game
+.Dq Survival Kids ,
+sans global checksum:
.Pp
.D1 $ rgbfix -cjsv -k A4 -l 0x33 -m 0x1B -p 0xFF -r 3 -t SURVIVALKIDAVKE \
SurvivalKids.gbc
--- /dev/null
+++ b/test/fix/.gitignore
@@ -1,0 +1,1 @@
+/padding*_*
--- /dev/null
+++ b/test/fix/README.md
@@ -1,0 +1,16 @@
+# RGBFIX tests
+
+These tests check that RGBFIX behaves properly.
+
+## Structure of a test
+
+- `test.bin`: The file passed as input to RGBFIX.
+- `test.flags`: The command-line flags passed to RGBFIX's invocation.
+ Actually, only the first line is considered; the rest of the file may contain comments about the test.
+- `test.gb`: The expected output.
+ May not exist, generally when the test expects an error, in which case the comparison is skipped.
+- `test.err`: The expected error output.
+
+## Special tests
+
+- `noexist.err` is the expected error output when RGBFIX is given a non-existent input file.
--- /dev/null
+++ b/test/fix/bad-fix-char.bin
@@ -1,0 +1,3 @@
+���!H0�;<�N˗���=����^{KC}b�Q�&�3� �/]����d��Z=���b�a�i:�eS�>��U@�Ay<ae���$vf�孈┚��g� VC��~>������ �s_Cӛ��&nWFoK���X`'������bl�{����
+�����8���<$:V�"㳙ځh��D��ж��Qc����㸺Z��T�������Vۼ�tŒ��4�B��dT�� �I��
+�(/��{h�:��Qm�$*| ��VP��.^"&�<��k!���`��2���� &B� l�7f���Ze�yf.�m����!�����N5��d�E��qF
\ No newline at end of file
--- /dev/null
+++ b/test/fix/bad-fix-char.err
@@ -1,0 +1,3 @@
+warning: Ignoring 'm' in fix spec
+warning: Ignoring 'a' in fix spec
+warning: Ignoring 'o' in fix spec
--- /dev/null
+++ b/test/fix/bad-fix-char.flags
@@ -1,0 +1,1 @@
+-f lmao
--- /dev/null
+++ b/test/fix/color.bin
@@ -1,0 +1,2 @@
+��+2l}~��Ѧ��}��1x����&)Y�q*����ɒ��^ H�p@v6}��[��&���h���Bn~�T�0��uf�\�8���������Ñ�W���� 5^Bq<�K�"S%��U!��r,7��g�4Ȩ��������hL��؝�)����6U@�`ݑT�T����Dɦ�8z�5��~A�鱽ܜ�| �>�]�/�K���d~��"gF*AU�"W�7]}��/�}<��s[lx�ܔ}Ы[���8������ �����,�������!d�,!O��� [=��������28h���/�0��}��x��Դ�����{�}l�"J�����۟�k���0_8z���\�-�r��F��R�vO�h�r�S�HN��]�uƽ#�ʼ�;ߧr��6���Lz�m���5[�9+.Hs���_~z�5� /�b�ɊS](�6k��Pu�9�J1��ٵ� ,(C���%�k�����4���=��Ѫ���ı&O���� �.��"�
\ No newline at end of file
--- /dev/null
+++ b/test/fix/color.flags
@@ -1,0 +1,1 @@
+-C
--- /dev/null
+++ b/test/fix/color.gb
@@ -1,0 +1,2 @@
+��+2l}~��Ѧ��}��1x����&)Y�q*����ɒ��^ H�p@v6}��[��&���h���Bn~�T�0��uf�\�8���������Ñ�W���� 5^Bq<�K�"S%��U!��r,7��g�4Ȩ��������hL��؝�)����6U@�`ݑT�T����Dɦ�8z�5��~A�鱽ܜ�| �>�]�/�K���d~��"gF*AU�"W�7]}��/�}<��s[lx�ܔ}Ы[���8������ �����,�������!d�,!O��� [=��������28h���/�0��}��x��Դ�����{�}l�"J�����۟�k���0_8z���\�-�r��F��R�vO�h�r�S�HN��]�uƽ#�ʼ�;ߧr��6���Lz�m���5[�9+.Hs���_~z�5� /�b�ɊS](�6k��Pu�9�J1��ٵ� ,(C���%�k�����4���=��Ѫ���ı&O���� �.��"�
\ No newline at end of file
binary files /dev/null b/test/fix/compatible.bin differ
--- /dev/null
+++ b/test/fix/compatible.flags
@@ -1,0 +1,1 @@
+-c
binary files /dev/null b/test/fix/compatible.gb differ
--- /dev/null
+++ b/test/fix/empty.err
@@ -1,0 +1,2 @@
+FATAL: "<filename>" too short, expected at least 336 ($150) bytes, got only 0
+Fixing "<filename>" failed with 1 error
binary files /dev/null b/test/fix/fix-override.bin differ
--- /dev/null
+++ b/test/fix/fix-override.err
@@ -1,0 +1,1 @@
+warning: 'l' overriding 'L' in fix spec
--- /dev/null
+++ b/test/fix/fix-override.flags
@@ -1,0 +1,1 @@
+-f Ll
binary files /dev/null b/test/fix/fix-override.gb differ
binary files /dev/null b/test/fix/gameid-trunc.bin differ
--- /dev/null
+++ b/test/fix/gameid-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating game ID "FOUR!" to 4 chars
--- /dev/null
+++ b/test/fix/gameid-trunc.flags
@@ -1,0 +1,1 @@
+-i 'FOUR!'
binary files /dev/null b/test/fix/gameid-trunc.gb differ
binary files /dev/null b/test/fix/gameid.bin differ
--- /dev/null
+++ b/test/fix/gameid.flags
@@ -1,0 +1,1 @@
+-i RGBD
binary files /dev/null b/test/fix/gameid.gb differ
binary files /dev/null b/test/fix/global-large.bin differ
--- /dev/null
+++ b/test/fix/global-large.flags
@@ -1,0 +1,1 @@
+-fg
binary files /dev/null b/test/fix/global-large.gb differ
binary files /dev/null b/test/fix/global-larger.bin differ
--- /dev/null
+++ b/test/fix/global-larger.flags
@@ -1,0 +1,1 @@
+-fg
binary files /dev/null b/test/fix/global-larger.gb differ
binary files /dev/null b/test/fix/global-trash.bin differ
--- /dev/null
+++ b/test/fix/global-trash.flags
@@ -1,0 +1,1 @@
+-f G
binary files /dev/null b/test/fix/global-trash.gb differ
binary files /dev/null b/test/fix/global.bin differ
--- /dev/null
+++ b/test/fix/global.flags
@@ -1,0 +1,1 @@
+-f g
binary files /dev/null b/test/fix/global.gb differ
binary files /dev/null b/test/fix/header-edit.bin differ
--- /dev/null
+++ b/test/fix/header-edit.flags
@@ -1,0 +1,2 @@
+-Cf h
+Checks that the header checksum properly accounts for header modifications
binary files /dev/null b/test/fix/header-edit.gb differ
binary files /dev/null b/test/fix/header-trash.bin differ
--- /dev/null
+++ b/test/fix/header-trash.flags
@@ -1,0 +1,1 @@
+-f H
binary files /dev/null b/test/fix/header-trash.gb differ
binary files /dev/null b/test/fix/header.bin differ
--- /dev/null
+++ b/test/fix/header.flags
@@ -1,0 +1,1 @@
+-f h
binary files /dev/null b/test/fix/header.gb differ
binary files /dev/null b/test/fix/jp.bin differ
--- /dev/null
+++ b/test/fix/jp.flags
@@ -1,0 +1,1 @@
+-j
binary files /dev/null b/test/fix/jp.gb differ
binary files /dev/null b/test/fix/logo-trash.bin differ
--- /dev/null
+++ b/test/fix/logo-trash.flags
@@ -1,0 +1,1 @@
+-f L
binary files /dev/null b/test/fix/logo-trash.gb differ
binary files /dev/null b/test/fix/logo.bin differ
--- /dev/null
+++ b/test/fix/logo.flags
@@ -1,0 +1,1 @@
+-f l
binary files /dev/null b/test/fix/logo.gb differ
binary files /dev/null b/test/fix/mbc.bin differ
--- /dev/null
+++ b/test/fix/mbc.flags
@@ -1,0 +1,1 @@
+-m 177
binary files /dev/null b/test/fix/mbc.gb differ
binary files /dev/null b/test/fix/mbcless-ram.bin differ
--- /dev/null
+++ b/test/fix/mbcless-ram.err
@@ -1,0 +1,1 @@
+warning: MBC "ROM" has no RAM, but RAM size was set to 2
--- /dev/null
+++ b/test/fix/mbcless-ram.flags
@@ -1,0 +1,1 @@
+-m ROM -r 2
binary files /dev/null b/test/fix/mbcless-ram.gb differ
--- /dev/null
+++ b/test/fix/new-lic-trunc.bin
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n� ���9�Q�H�p��yF������ߘ��8^��]�b�� F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:���Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����Y˛c3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V; ���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating new licensee "HOMEBREW" to 2 chars
--- /dev/null
+++ b/test/fix/new-lic-trunc.flags
@@ -1,0 +1,1 @@
+-k HOMEBREW
--- /dev/null
+++ b/test/fix/new-lic-trunc.gb
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n� ���9�Q�H�p��yF������ߘ��8^��]�b�� F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:���Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����YHOc3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V; ���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic.bin
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n� ���9�Q�H�p��yF������ߘ��8^��]�b�� F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:���Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����Y˛c3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V; ���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic.flags
@@ -1,0 +1,1 @@
+-k HB
--- /dev/null
+++ b/test/fix/new-lic.gb
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n� ���9�Q�H�p��yF������ߘ��8^��]�b�� F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:���Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����YHBc3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V; ���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/noexist.err
@@ -1,0 +1,2 @@
+FATAL: Failed to open "noexist" for reading+writing: No such file or directory
+Fixing "noexist" failed with 1 error
binary files /dev/null b/test/fix/old-lic-hex.bin differ
--- /dev/null
+++ b/test/fix/old-lic-hex.flags
@@ -1,0 +1,1 @@
+-l 0x2a
binary files /dev/null b/test/fix/old-lic-hex.gb differ
--- /dev/null
+++ b/test/fix/old-lic.bin
@@ -1,0 +1,2 @@
+�EZ/:�����Ѐ��J/����_<�&�A�����XgAP
+`�1@Wh��||ø�e뻬e^"7(*YV��T���OY!���̔]���=���y�V~x�a�]�;���¡�zUm��z:d@֪�b� 'vʆq�W��_�����2)G�v(k���g,�p*@��!�Dõ�0k�����e�%ǚod���$r���<�:��cgi�"aF'=f����EcVcL.03"w),� HQi�D�f�3�s(-$?a0�@��.O�B{y�z�ʫ� h�/\�S�]�o��m'2D#/gr�W3�Χ��&U���EgMh�-A�6�����4M|1�q0O��*$�O�/n���g��2R��Q�X�_���[숔�p��\�)ƧTS�T�aP*'[�x��>߅�Ub[Ԧ�9s��I��P ��ģ�dn��$��(�ɔ���Eys�ڈ�Ƈ6�v���H �A����qnK�O{ֻw�k&��kQ�OjCe��@
\ No newline at end of file
--- /dev/null
+++ b/test/fix/old-lic.flags
@@ -1,0 +1,1 @@
+-l 42
--- /dev/null
+++ b/test/fix/old-lic.gb
@@ -1,0 +1,2 @@
+�EZ/:�����Ѐ��J/����_<�&�A�����XgAP
+`�1@Wh��||ø�e뻬e^"7(*YV��T���OY!���̔]���=���y�V~x�a�]�;���¡�zUm��z:d@֪�b� 'vʆq�W��_�����2)G�v(k���g,�p*@��!�Dõ�0k�����e�%ǚod���$r���<�:��cgi�"aF'=f����EcVcL.03"w),� HQi�D�f�3�s(-$?a0�@��.O�B{y�z�ʫ� h�/\�S�]�o��m'2D#/gr�W3�Χ��&U���EgMh�-A�*6�����4M|1�q0O��*$�O�/n���g��2R��Q�X�_���[숔�p��\�)ƧTS�T�aP*'[�x��>߅�Ub[Ԧ�9s��I��P ��ģ�dn��$��(�ɔ���Eys�ڈ�Ƈ6�v���H �A����qnK�O{ֻw�k&��kQ�OjCe��@
\ No newline at end of file
binary files /dev/null b/test/fix/padding-bigperfect.bin differ
--- /dev/null
+++ b/test/fix/padding-bigperfect.flags
@@ -1,0 +1,1 @@
+-p0xff
binary files /dev/null b/test/fix/padding-bigperfect.gb differ
binary files /dev/null b/test/fix/padding-imperfect.bin differ
--- /dev/null
+++ b/test/fix/padding-imperfect.flags
@@ -1,0 +1,1 @@
+-p255
binary files /dev/null b/test/fix/padding-imperfect.gb differ
binary files /dev/null b/test/fix/padding-large.bin differ
--- /dev/null
+++ b/test/fix/padding-large.flags
@@ -1,0 +1,1 @@
+-p 0xff
binary files /dev/null b/test/fix/padding-large.gb differ
binary files /dev/null b/test/fix/padding-larger.bin differ
--- /dev/null
+++ b/test/fix/padding-larger.flags
@@ -1,0 +1,1 @@
+-p 255
binary files /dev/null b/test/fix/padding-larger.gb differ
binary files /dev/null b/test/fix/padding-perfect.bin differ
--- /dev/null
+++ b/test/fix/padding-perfect.flags
@@ -1,0 +1,1 @@
+-p 0xFF
binary files /dev/null b/test/fix/padding-perfect.gb differ
binary files /dev/null b/test/fix/padding-rom0.bin differ
--- /dev/null
+++ b/test/fix/padding-rom0.flags
@@ -1,0 +1,1 @@
+-p 255
binary files /dev/null b/test/fix/padding-rom0.gb differ
binary files /dev/null b/test/fix/padding.bin differ
--- /dev/null
+++ b/test/fix/padding.flags
@@ -1,0 +1,1 @@
+-p0xFF
binary files /dev/null b/test/fix/padding.gb differ
binary files /dev/null b/test/fix/plain.bin differ
binary files /dev/null b/test/fix/plain.gb differ
binary files /dev/null b/test/fix/ram.bin differ
--- /dev/null
+++ b/test/fix/ram.flags
@@ -1,0 +1,1 @@
+-r 232
binary files /dev/null b/test/fix/ram.gb differ
binary files /dev/null b/test/fix/ramful-mbc-no-ram.bin differ
--- /dev/null
+++ b/test/fix/ramful-mbc-no-ram.err
@@ -1,0 +1,1 @@
+warning: MBC "MBC3+RAM" has RAM, but RAM size was set to 0
--- /dev/null
+++ b/test/fix/ramful-mbc-no-ram.flags
@@ -1,0 +1,1 @@
+-m MBC3+RAM -r 0
binary files /dev/null b/test/fix/ramful-mbc-no-ram.gb differ
binary files /dev/null b/test/fix/ramful-mbc.bin differ
--- /dev/null
+++ b/test/fix/ramful-mbc.flags
@@ -1,0 +1,1 @@
+-m MBC3+RAM -r 2
binary files /dev/null b/test/fix/ramful-mbc.gb differ
binary files /dev/null b/test/fix/ramless-mbc.bin differ
--- /dev/null
+++ b/test/fix/ramless-mbc.flags
@@ -1,0 +1,1 @@
+-m MBC3 -r 0
binary files /dev/null b/test/fix/ramless-mbc.gb differ
binary files /dev/null b/test/fix/rom-ram.bin differ
--- /dev/null
+++ b/test/fix/rom-ram.err
@@ -1,0 +1,1 @@
+warning: ROM+RAM / ROM+RAM+BATTERY are under-specified and poorly supported
--- /dev/null
+++ b/test/fix/rom-ram.flags
@@ -1,0 +1,1 @@
+-m8
binary files /dev/null b/test/fix/rom-ram.gb differ
binary files /dev/null b/test/fix/sgb-licensee.bin differ
--- /dev/null
+++ b/test/fix/sgb-licensee.err
@@ -1,0 +1,1 @@
+warning: SGB compatibility enabled, but old licensee is 0x45, not 0x33
--- /dev/null
+++ b/test/fix/sgb-licensee.flags
@@ -1,0 +1,1 @@
+-sl69
binary files /dev/null b/test/fix/sgb.bin differ
--- /dev/null
+++ b/test/fix/sgb.flags
@@ -1,0 +1,1 @@
+-s
binary files /dev/null b/test/fix/sgb.gb differ
--- /dev/null
+++ b/test/fix/test.sh
@@ -1,0 +1,99 @@
+#!/bin/bash
+
+export LC_ALL=C
+
+tmpdir="$(mktemp -d)"
+src="$PWD"
+rc=0
+
+cp ../../{rgbfix,contrib/gbdiff.bash} "$tmpdir"
+cd "$tmpdir"
+trap "cd; rm -rf '$tmpdir'" EXIT
+
+bold="$(tput bold)"
+resbold="$(tput sgr0)"
+red="$(tput setaf 1)"
+green="$(tput setaf 2)"
+rescolors="$(tput op)"
+
+tryDiff () {
+ if ! diff -u --strip-trailing-cr "$1" "$2"; then
+ echo "${bold}${red}${3:-$1} mismatch!${rescolors}${resbold}"
+ false
+ fi
+}
+
+tryCmp () {
+ if ! cmp "$1" "$2"; then
+ ./gbdiff.bash "$1" "$2"
+ echo "${bold}${red}${3:-$1} mismatch!${rescolors}${resbold}"
+ false
+ fi
+}
+
+runTest () {
+ flags="$(head -n 1 "$2/$1.flags")" # Allow other lines to serve as comments
+
+ for variant in '' ' piped'; do
+ our_rc=0
+ if [[ $progress -ne 0 ]]; then
+ echo "${bold}${green}$1${variant}...${rescolors}${resbold}"
+ fi
+ if [[ -z "$variant" ]]; then
+ cp "$2/$1.bin" out.gb
+ if [[ -n "$(eval ./rgbfix $flags out.gb '2>out.err')" ]]; then
+ echo "${bold}${red}Fixing $1 in-place shouldn't output anything on stdout!${rescolors}${resbold}"
+ our_rc=1
+ fi
+ subst='out.gb'
+ else
+ # Stop! This is not a Useless Use Of Cat. Using cat instead of
+ # stdin redirection makes the input an unseekable pipe - a scenario
+ # that's harder to deal with.
+ cat "$2/$1.bin" | eval ./rgbfix "$flags" '>out.gb' '2>out.err'
+ subst='<stdin>'
+ fi
+
+ sed "s/$subst/<filename>/g" "out.err" | tryDiff "$2/$1.err" - "$1.err${variant}"
+ our_rc=$(($? || $our_rc))
+ if [[ -r "$2/$1.gb" ]]; then
+ tryCmp "$2/$1.gb" "out.gb" "$1.gb${variant}"
+ our_rc=$(($? || $our_rc))
+ fi
+
+ rc=$(($rc || $our_rc))
+ if [[ $our_rc -ne 0 ]]; then break; fi
+ done
+}
+
+rm -f padding*_* # Delete padding test cases generated but not deleted (e.g. interrupted)
+
+progress=1
+for i in "$src"/*.bin; do
+ runTest "$(basename "$i" .bin)" "$src"
+done
+
+# Check the result with all different padding bytes
+echo "${bold}Checking padding...${resbold}"
+cp "$src"/padding{,-large,-larger}.bin .
+touch padding{,-large,-larger}.err
+progress=0
+for b in {0..254}; do
+ printf "\r$b..."
+ for suffix in '' -large -larger; do
+ cat <<<'-p $b' >padding$suffix.flags
+ tr '\377' \\$(($b / 64))$((($b / 8) % 8))$(($b % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
+ runTest padding${suffix} .
+ done
+done
+printf "\rDone! \n"
+
+# TODO: check MBC names
+
+# Check that RGBFIX errors out when inputting a non-existent file...
+./rgbfix noexist 2>out.err
+rc=$(($rc || $? != 1))
+tryDiff "$src/noexist.err" out.err noexist.err
+rc=$(($rc || $?))
+
+exit $rc
binary files /dev/null b/test/fix/title-color-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-color-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-color-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -C
+Checks that the CGB flag correctly truncates the title to 15 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-color-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-color-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-color-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-color-trunc.flags
@@ -1,0 +1,2 @@
+-C -t 0123456789ABCDEF
+Checks that the CGB flag correctly truncates the title to 15 chars only
binary files /dev/null b/test/fix/title-color-trunc.gb differ
binary files /dev/null b/test/fix/title-compat-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-compat-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-compat-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -c
+Checks that the CGB compat flag correctly truncates the title to 15 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-compat-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-compat-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-compat-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-compat-trunc.flags
@@ -1,0 +1,2 @@
+-c -t 0123456789ABCDEF
+Checks that the CGB compat flag correctly truncates the title to 15 chars only
binary files /dev/null b/test/fix/title-compat-trunc.gb differ
binary files /dev/null b/test/fix/title-gameid-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-gameid-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 11 chars
--- /dev/null
+++ b/test/fix/title-gameid-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -i rgbd
+Checks that the game ID flag correctly truncates the title to 11 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-gameid-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-gameid-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-gameid-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 11 chars
--- /dev/null
+++ b/test/fix/title-gameid-trunc.flags
@@ -1,0 +1,2 @@
+-i rgbd -t 0123456789ABCDEF
+Checks that the game ID flag correctly truncates the title to 11 chars only
binary files /dev/null b/test/fix/title-gameid-trunc.gb differ
binary files /dev/null b/test/fix/title-pad.bin differ
--- /dev/null
+++ b/test/fix/title-pad.flags
@@ -1,0 +1,1 @@
+-t "I LOVE YOU"
binary files /dev/null b/test/fix/title-pad.gb differ
binary files /dev/null b/test/fix/title-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEFGHIJK" to 16 chars
--- /dev/null
+++ b/test/fix/title-trunc.flags
@@ -1,0 +1,1 @@
+-t 0123456789ABCDEFGHIJK
binary files /dev/null b/test/fix/title-trunc.gb differ
binary files /dev/null b/test/fix/title.bin differ
--- /dev/null
+++ b/test/fix/title.flags
@@ -1,0 +1,1 @@
+-t "Game Boy dev rox"
binary files /dev/null b/test/fix/title.gb differ
--- /dev/null
+++ b/test/fix/tooshort.bin
@@ -1,0 +1,1 @@
+g�1B���m�k��&)�v
\ No newline at end of file
--- /dev/null
+++ b/test/fix/tooshort.err
@@ -1,0 +1,2 @@
+FATAL: "<filename>" too short, expected at least 336 ($150) bytes, got only 20
+Fixing "<filename>" failed with 1 error
binary files /dev/null b/test/fix/verify-pad.bin differ
--- /dev/null
+++ b/test/fix/verify-pad.flags
@@ -1,0 +1,4 @@
+-vp 69
+Check that the global checksum is correctly affected by padding:
+Padding adds extra bytes (carefully picked *not* to be 0, or other values),
+which must be properly accounted for.
binary files /dev/null b/test/fix/verify-pad.gb differ
binary files /dev/null b/test/fix/verify-trash.bin differ
--- /dev/null
+++ b/test/fix/verify-trash.flags
@@ -1,0 +1,2 @@
+-f LHG
+Checks that the global checksum is correctly affected by the header checksum
binary files /dev/null b/test/fix/verify-trash.gb differ
binary files /dev/null b/test/fix/verify.bin differ
--- /dev/null
+++ b/test/fix/verify.flags
@@ -1,0 +1,2 @@
+-v
+Checks that the global checksum is correctly affected by the header checksum
binary files /dev/null b/test/fix/verify.gb differ
binary files /dev/null b/test/fix/version.bin differ
--- /dev/null
+++ b/test/fix/version.flags
@@ -1,0 +1,1 @@
+-n 11
binary files /dev/null b/test/fix/version.gb differ
--- a/test/run-tests.sh
+++ b/test/run-tests.sh
@@ -14,13 +14,11 @@
# Tests included with the repository
-pushd asm
-./test.sh
-popd
-
-pushd link
-./test.sh
-popd
+for dir in asm link fix; do
+ pushd $dir
+ ./test.sh
+ popd
+done
# Test some significant external projects that use RGBDS
# When adding new ones, don't forget to add them to the .gitignore!