mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
451 lines
13 KiB
C++
451 lines
13 KiB
C++
// SPDX-License-Identifier: MIT
|
|
|
|
#include "fix/mbc.hpp"
|
|
|
|
#include <optional>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
#include "helpers.hpp" // unreachable_
|
|
#include "platform.hpp" // strcasecmp
|
|
#include "util.hpp"
|
|
|
|
#include "fix/warning.hpp"
|
|
|
|
// Associate every MBC type with its name and whether it has RAM
|
|
static std::unordered_map<MbcType, std::pair<char const *, bool>> mbcData{
|
|
{ROM, {"ROM", false} },
|
|
{ROM_RAM, {"ROM+RAM", true} },
|
|
{ROM_RAM_BATTERY, {"ROM+RAM+BATTERY", true} },
|
|
{MBC1, {"MBC1", false} },
|
|
{MBC1_RAM, {"MBC1+RAM", true} },
|
|
{MBC1_RAM_BATTERY, {"MBC1+RAM+BATTERY", true} },
|
|
// MBC2 technically has RAM, but is not marked as such
|
|
{MBC2, {"MBC2", false} },
|
|
{MBC2_BATTERY, {"MBC2+BATTERY", false} },
|
|
{MMM01, {"MMM01", false} },
|
|
{MMM01_RAM, {"MMM01+RAM", true} },
|
|
{MMM01_RAM_BATTERY, {"MMM01+RAM+BATTERY", true} },
|
|
{MBC3, {"MBC3", false} },
|
|
{MBC3_TIMER_BATTERY, {"MBC3+TIMER+BATTERY", false} },
|
|
{MBC3_TIMER_RAM_BATTERY, {"MBC3+TIMER+RAM+BATTERY", true} },
|
|
{MBC3_RAM, {"MBC3+RAM", true} },
|
|
{MBC3_RAM_BATTERY, {"MBC3+RAM+BATTERY", true} },
|
|
{MBC5, {"MBC5", false} },
|
|
{MBC5_RAM, {"MBC5+RAM", true} },
|
|
{MBC5_RAM_BATTERY, {"MBC5+RAM+BATTERY", true} },
|
|
{MBC5_RUMBLE, {"MBC5+RUMBLE", false} },
|
|
{MBC5_RUMBLE_RAM, {"MBC5+RUMBLE+RAM", true} },
|
|
{MBC5_RUMBLE_RAM_BATTERY, {"MBC5+RUMBLE+RAM+BATTERY", true} },
|
|
// MBC6 "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB)
|
|
{MBC6, {"MBC6", true} },
|
|
{MBC7_SENSOR_RUMBLE_RAM_BATTERY, {"MBC7+SENSOR+RUMBLE+RAM+BATTERY", true} },
|
|
{POCKET_CAMERA, {"POCKET CAMERA", true} },
|
|
// Bandai TAMA5 "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0
|
|
{BANDAI_TAMA5, {"BANDAI TAMA5", false} },
|
|
{HUC3, {"HUC3", true} },
|
|
{HUC1_RAM_BATTERY, {"HUC1+RAM+BATTERY", true} },
|
|
// TPP1 may or may not have RAM, don't use these flags for it
|
|
{TPP1, {"TPP1", false} },
|
|
{TPP1_RUMBLE, {"TPP1+RUMBLE", false} },
|
|
{TPP1_MULTIRUMBLE_RUMBLE, {"TPP1+MULTIRUMBLE", false} },
|
|
{TPP1_TIMER, {"TPP1+TIMER", false} },
|
|
{TPP1_TIMER_RUMBLE, {"TPP1+TIMER+RUMBLE", false} },
|
|
{TPP1_TIMER_MULTIRUMBLE_RUMBLE, {"TPP1+TIMER+MULTIRUMBLE", false} },
|
|
{TPP1_BATTERY, {"TPP1+BATTERY", false} },
|
|
{TPP1_BATTERY_RUMBLE, {"TPP1+BATTERY+RUMBLE", false} },
|
|
{TPP1_BATTERY_MULTIRUMBLE_RUMBLE, {"TPP1+BATTERY+MULTIRUMBLE", false} },
|
|
{TPP1_BATTERY_TIMER, {"TPP1+BATTERY+TIMER", false} },
|
|
{TPP1_BATTERY_TIMER_RUMBLE, {"TPP1+BATTERY+TIMER+RUMBLE", false} },
|
|
{TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE, {"TPP1+BATTERY+TIMER+MULTIRUMBLE", false}},
|
|
};
|
|
|
|
static char const *acceptedMBCNames =
|
|
"Accepted MBC names:\n"
|
|
"\tROM ($00) [aka ROM_ONLY]\n"
|
|
"\tMBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03)\n"
|
|
"\tMBC2 ($05), MBC2+BATTERY ($06)\n"
|
|
"\tROM+RAM ($08) [deprecated], ROM+RAM+BATTERY ($09) [deprecated]\n"
|
|
"\tMMM01 ($0B), MMM01+RAM ($0C), MMM01+RAM+BATTERY ($0D)\n"
|
|
"\tMBC3+TIMER+BATTERY ($0F), MBC3+TIMER+RAM+BATTERY ($10)\n"
|
|
"\tMBC3 ($11), MBC3+RAM ($12), MBC3+RAM+BATTERY ($13)\n"
|
|
"\tMBC5 ($19), MBC5+RAM ($1A), MBC5+RAM+BATTERY ($1B)\n"
|
|
"\tMBC5+RUMBLE ($1C), MBC5+RUMBLE+RAM ($1D), MBC5+RUMBLE+RAM+BATTERY ($1E)\n"
|
|
"\tMBC6 ($20)\n"
|
|
"\tMBC7+SENSOR+RUMBLE+RAM+BATTERY ($22)\n"
|
|
"\tPOCKET_CAMERA ($FC)\n"
|
|
"\tBANDAI_TAMA5 ($FD) [aka TAMA5]\n"
|
|
"\tHUC3 ($FE)\n"
|
|
"\tHUC1+RAM+BATTERY ($FF)\n"
|
|
"\n"
|
|
"\tTPP1_1.0, TPP1_1.0+RUMBLE, TPP1_1.0+MULTIRUMBLE, TPP1_1.0+TIMER,\n"
|
|
"\tTPP1_1.0+TIMER+RUMBLE, TPP1_1.0+TIMER+MULTIRUMBLE, TPP1_1.0+BATTERY,\n"
|
|
"\tTPP1_1.0+BATTERY+RUMBLE, TPP1_1.0+BATTERY+MULTIRUMBLE,\n"
|
|
"\tTPP1_1.0+BATTERY+TIMER, TPP1_1.0+BATTERY+TIMER+RUMBLE,\n"
|
|
"\tTPP1_1.0+BATTERY+TIMER+MULTIRUMBLE"; // No trailing newline
|
|
|
|
char const *mbc_Name(MbcType type) {
|
|
auto search = mbcData.find(type);
|
|
return search != mbcData.end() ? search->second.first : "(unknown)";
|
|
}
|
|
|
|
bool mbc_HasRAM(MbcType type) {
|
|
auto search = mbcData.find(type);
|
|
return search != mbcData.end() && search->second.second;
|
|
}
|
|
|
|
static void skipMBCSpace(char const *&ptr) {
|
|
ptr += strspn(ptr, " \t_");
|
|
}
|
|
|
|
static char normalizeMBCChar(char c) {
|
|
if (c == '_') {
|
|
return ' '; // Treat underscores as spaces
|
|
}
|
|
return toUpper(c); // Uppercase for comparison with `mbc_Name`s
|
|
}
|
|
|
|
[[noreturn]]
|
|
static void fatalUnknownMBC(char const *name) {
|
|
fatal("Unknown MBC \"%s\"\n%s", name, acceptedMBCNames);
|
|
}
|
|
|
|
[[noreturn]]
|
|
static void fatalWrongMBCFeatures(char const *name) {
|
|
fatal("Features incompatible with MBC (\"%s\")\n%s", name, acceptedMBCNames);
|
|
}
|
|
|
|
MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) {
|
|
char const *ptr = name + strspn(name, " \t"); // Skip leading blank space
|
|
|
|
if (!strcasecmp(ptr, "help") || !strcasecmp(ptr, "list")) {
|
|
puts(acceptedMBCNames); // Outputs to stdout and appends a newline
|
|
exit(0);
|
|
}
|
|
|
|
// Parse numeric MBC and return it as-is (unless it's too large)
|
|
if (char c = *ptr; isDigit(c) || c == '$' || c == '&' || c == '%') {
|
|
if (std::optional<uint64_t> mbc = parseWholeNumber(ptr); !mbc) {
|
|
fatalUnknownMBC(name);
|
|
} else if (*mbc > 0xFF) {
|
|
fatal("Specified MBC ID out of range 0-255: \"%s\"", name);
|
|
} else {
|
|
return static_cast<MbcType>(*mbc);
|
|
}
|
|
}
|
|
|
|
// Begin by reading the MBC type:
|
|
uint16_t mbc = UINT16_MAX;
|
|
|
|
auto tryReadSlice = [&ptr, &name](char const *expected) {
|
|
while (*expected) {
|
|
// If `name` is too short, the character will be '\0' and this will return `false`
|
|
if (normalizeMBCChar(*ptr++) != *expected++) {
|
|
fatalUnknownMBC(name);
|
|
}
|
|
}
|
|
};
|
|
|
|
switch (*ptr++) {
|
|
case 'R': // ROM / ROM_ONLY
|
|
case 'r':
|
|
tryReadSlice("OM");
|
|
// Handle optional " ONLY"
|
|
skipMBCSpace(ptr);
|
|
if (*ptr == 'O' || *ptr == 'o') {
|
|
++ptr;
|
|
tryReadSlice("NLY");
|
|
}
|
|
mbc = ROM;
|
|
break;
|
|
|
|
case 'M': // MBC{1, 2, 3, 5, 6, 7} / MMM01
|
|
case 'm':
|
|
switch (*ptr++) {
|
|
case 'B':
|
|
case 'b':
|
|
tryReadSlice("C");
|
|
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;
|
|
}
|
|
break;
|
|
case 'M':
|
|
case 'm':
|
|
tryReadSlice("M01");
|
|
mbc = MMM01;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'P': // POCKET_CAMERA
|
|
case 'p':
|
|
tryReadSlice("OCKET CAMERA");
|
|
mbc = POCKET_CAMERA;
|
|
break;
|
|
|
|
case 'B': // BANDAI_TAMA5
|
|
case 'b':
|
|
tryReadSlice("ANDAI TAMA5");
|
|
mbc = BANDAI_TAMA5;
|
|
break;
|
|
|
|
case 'T': // TAMA5 / TPP1
|
|
case 't':
|
|
switch (*ptr++) {
|
|
case 'A':
|
|
tryReadSlice("MA5");
|
|
mbc = BANDAI_TAMA5;
|
|
break;
|
|
case 'P':
|
|
tryReadSlice("P1");
|
|
// Parse version
|
|
skipMBCSpace(ptr);
|
|
// Major
|
|
if (std::optional<uint64_t> major = parseNumber(ptr, BASE_10); !major) {
|
|
fatal("Failed to parse TPP1 major revision number");
|
|
} else if (*major != 1) {
|
|
fatal("RGBFIX only supports TPP1 version 1.0");
|
|
} else {
|
|
tpp1Major = *major;
|
|
}
|
|
tryReadSlice(".");
|
|
// Minor
|
|
if (std::optional<uint64_t> minor = parseNumber(ptr, BASE_10); !minor) {
|
|
fatal("Failed to parse TPP1 minor revision number");
|
|
} else if (*minor > 0xFF) {
|
|
fatal("TPP1 minor revision number must be 8-bit");
|
|
} else {
|
|
tpp1Minor = *minor;
|
|
}
|
|
mbc = TPP1;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'H': // HuC{1, 3}
|
|
case 'h':
|
|
tryReadSlice("UC");
|
|
switch (*ptr++) {
|
|
case '1':
|
|
mbc = HUC1_RAM_BATTERY;
|
|
break;
|
|
case '3':
|
|
mbc = HUC3;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (mbc == UINT16_MAX) {
|
|
fatalUnknownMBC(name);
|
|
}
|
|
|
|
// Read "additional features"
|
|
uint8_t features = 0;
|
|
// clang-format off: vertically align values
|
|
static constexpr uint8_t RAM = 1 << 7;
|
|
static constexpr uint8_t BATTERY = 1 << 6;
|
|
static constexpr uint8_t TIMER = 1 << 5;
|
|
static constexpr uint8_t RUMBLE = 1 << 4;
|
|
static constexpr uint8_t SENSOR = 1 << 3;
|
|
static constexpr uint8_t MULTIRUMBLE = 1 << 2;
|
|
// clang-format on
|
|
|
|
while (*ptr) {
|
|
// We expect a '+' at this point
|
|
skipMBCSpace(ptr);
|
|
tryReadSlice("+");
|
|
skipMBCSpace(ptr);
|
|
|
|
switch (*ptr++) {
|
|
case 'B': // BATTERY
|
|
case 'b':
|
|
tryReadSlice("ATTERY");
|
|
features |= BATTERY;
|
|
break;
|
|
|
|
case 'M':
|
|
case 'm':
|
|
tryReadSlice("ULTIRUMBLE");
|
|
features |= MULTIRUMBLE;
|
|
break;
|
|
|
|
case 'R': // RAM or RUMBLE
|
|
case 'r':
|
|
switch (*ptr++) {
|
|
case 'U':
|
|
case 'u':
|
|
tryReadSlice("MBLE");
|
|
features |= RUMBLE;
|
|
break;
|
|
case 'A':
|
|
case 'a':
|
|
tryReadSlice("M");
|
|
features |= RAM;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'S': // SENSOR
|
|
case 's':
|
|
tryReadSlice("ENSOR");
|
|
features |= SENSOR;
|
|
break;
|
|
|
|
case 'T': // TIMER
|
|
case 't':
|
|
tryReadSlice("IMER");
|
|
features |= TIMER;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case MBC2:
|
|
if (features == BATTERY) {
|
|
mbc = MBC2_BATTERY;
|
|
} else if (features) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case MBC3:
|
|
// Handle timer, which also requires battery
|
|
if (features & TIMER) {
|
|
if (!(features & BATTERY)) {
|
|
warning(WARNING_MBC, "\"MBC3+TIMER\" implies \"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) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
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) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case MBC6:
|
|
case POCKET_CAMERA:
|
|
case BANDAI_TAMA5:
|
|
case HUC3:
|
|
// No extra features accepted
|
|
if (features) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
|
|
if (features != (SENSOR | RUMBLE | RAM | BATTERY)) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case HUC1_RAM_BATTERY:
|
|
if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
break;
|
|
|
|
case TPP1: {
|
|
// clang-format off: vertically align values
|
|
static constexpr uint8_t BATTERY_TPP1 = 1 << 3;
|
|
static constexpr uint8_t TIMER_TPP1 = 1 << 2;
|
|
static constexpr uint8_t MULTIRUMBLE_TPP1 = 1 << 1;
|
|
static constexpr uint8_t RUMBLE_TPP1 = 1 << 0;
|
|
// clang-format on
|
|
|
|
if (features & RAM) {
|
|
warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size");
|
|
}
|
|
if (features & BATTERY) {
|
|
mbc |= BATTERY_TPP1;
|
|
}
|
|
if (features & TIMER) {
|
|
mbc |= TIMER_TPP1;
|
|
}
|
|
if (features & RUMBLE) {
|
|
mbc |= RUMBLE_TPP1;
|
|
}
|
|
if (features & SENSOR) {
|
|
fatalWrongMBCFeatures(name);
|
|
}
|
|
if (features & MULTIRUMBLE) {
|
|
mbc |= MULTIRUMBLE_TPP1 | RUMBLE_TPP1; // Multiple rumble speeds imply rumble
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return static_cast<MbcType>(mbc);
|
|
}
|