// SPDX-License-Identifier: MIT #include "fix/mbc.hpp" #include #include #include #include #include #include #include #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> 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 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(*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 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 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(mbc); }