From 44ee0419116509538330491bff160d7f8741b48f Mon Sep 17 00:00:00 2001 From: Jesse Talavera Date: Wed, 6 Nov 2024 20:20:21 -0500 Subject: [PATCH 1/3] Add a `GBAHeader` struct --- src/GBACart.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/GBACart.h b/src/GBACart.h index 726a234d10..0c5157ff06 100644 --- a/src/GBACart.h +++ b/src/GBACart.h @@ -35,6 +35,25 @@ enum CartType RumblePak = 0x202, }; +// See https://problemkaputt.de/gbatek.htm#gbacartridgeheader for details +struct GBAHeader +{ + u32 EntryPoint; + u8 NintendoLogo[156]; + char Title[12]; + char GameCode[4]; + char MakerCode[2]; + u8 FixedValue; // must be 0x96 + u8 MainUnitCode; + u8 DeviceType; + u8 Reserved0[7]; + u8 SoftwareVersion; + u8 ComplementCheck; + u8 Reserved1[2]; +}; + +static_assert(sizeof(GBAHeader) == 192, "GBAHeader should be 192 bytes"); + // CartCommon -- base code shared by all cart types class CartCommon { @@ -91,6 +110,7 @@ class CartGame : public CartCommon [[nodiscard]] const u8* GetROM() const override { return ROM.get(); } [[nodiscard]] u32 GetROMLength() const override { return ROMLength; } + [[nodiscard]] const GBAHeader& GetHeader() const noexcept { return *reinterpret_cast(ROM.get()); } u8* GetSaveMemory() const override; u32 GetSaveMemoryLength() const override; From e7e481f9e080949f99f7bd5d3b72de8c7831bc8a Mon Sep 17 00:00:00 2001 From: Jesse Talavera Date: Thu, 7 Nov 2024 15:06:30 -0500 Subject: [PATCH 2/3] Add extra `GBAAddon` entries for the Boktai carts - Each game in the trilogy has a different effect on Lunar Knights (the only commercial DS game to support the solar sensor) --- src/NDS.h | 6 ++++++ src/frontend/qt_sdl/EmuInstance.cpp | 6 ++++++ src/frontend/qt_sdl/Window.cpp | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/NDS.h b/src/NDS.h index b2bfb38596..b44260dbdb 100644 --- a/src/NDS.h +++ b/src/NDS.h @@ -211,6 +211,12 @@ enum { GBAAddon_RAMExpansion = 1, GBAAddon_RumblePak = 2, + // Each game in the GBA Boktai trilogy uses the same solar sensor, + // but Lunar Knights (the only NDS game to use the solar sensor) + // applies slightly different effects depending on the game. + GBAAddon_SolarSensorBoktai1 = 3, + GBAAddon_SolarSensorBoktai2 = 4, + GBAAddon_SolarSensorBoktai3 = 5, }; class SPU; diff --git a/src/frontend/qt_sdl/EmuInstance.cpp b/src/frontend/qt_sdl/EmuInstance.cpp index 2ac3f42d98..1dad230761 100644 --- a/src/frontend/qt_sdl/EmuInstance.cpp +++ b/src/frontend/qt_sdl/EmuInstance.cpp @@ -2088,6 +2088,12 @@ QString EmuInstance::gbaAddonName(int addon) return "Rumble Pak"; case GBAAddon_RAMExpansion: return "Memory expansion"; + case GBAAddon_SolarSensorBoktai1: + return "Solar Sensor (Boktai 1)"; + case GBAAddon_SolarSensorBoktai2: + return "Solar Sensor (Boktai 2)"; + case GBAAddon_SolarSensorBoktai3: + return "Solar Sensor (Boktai 3)"; } return "???"; diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 596a0f5c31..8a3237a928 100644 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -320,7 +320,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : QMenu * submenu = menu->addMenu("Insert add-on cart"); QAction *act; - int addons[] = {GBAAddon_RAMExpansion, GBAAddon_RumblePak, -1}; + int addons[] = {GBAAddon_RAMExpansion, GBAAddon_RumblePak, GBAAddon_SolarSensorBoktai1, GBAAddon_SolarSensorBoktai2, GBAAddon_SolarSensorBoktai3, -1}; for (int i = 0; addons[i] != -1; i++) { int addon = addons[i]; From f6692dff8c0c53f77639a08e5e746a286312bb41 Mon Sep 17 00:00:00 2001 From: Jesse Talavera Date: Thu, 7 Nov 2024 15:12:21 -0500 Subject: [PATCH 3/3] Copy the logo data from the NDS ROM's header to the Boktai stub's header --- src/GBACart.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ src/GBACart.h | 20 +++++++++++++++- src/NDS.cpp | 20 ++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/GBACart.cpp b/src/GBACart.cpp index a62aca6b27..cd43dc048f 100644 --- a/src/GBACart.cpp +++ b/src/GBACart.cpp @@ -539,6 +539,57 @@ CartGameSolarSensor::CartGameSolarSensor(std::unique_ptr&& rom, u32 len, s { } +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const NDSCart::CartCommon& cart, void* userdata) noexcept +{ + return CreateFakeSolarSensorROM(gamecode, cart.GetHeader().NintendoLogo, userdata); +} + +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const GBACart::CartGame& cart, void* userdata) noexcept +{ + return CreateFakeSolarSensorROM(gamecode, cart.GetHeader().NintendoLogo, userdata); +} + +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const u8* logo, void* userdata) noexcept +{ + if (!gamecode) + return nullptr; + + if (strnlen(gamecode, sizeof(GBAHeader::GameCode)) > sizeof(GBAHeader::GameCode)) + return nullptr; + + bool solarsensor = false; + for (const char* i : SOLAR_SENSOR_GAMECODES) + { + if (strcmp(gamecode, i) == 0) { + solarsensor = true; + break; + } + } + + if (!solarsensor) + return nullptr; + + // just 256 bytes; we don't need a whole ROM! + constexpr size_t FAKE_BOKTAI_ROM_LENGTH = 0x100; + std::unique_ptr rom = std::make_unique(FAKE_BOKTAI_ROM_LENGTH); + + // create a fake ROM + GBAHeader& header = *reinterpret_cast(rom.get()); + memcpy(header.Title, BOKTAI_STUB_TITLE, strnlen(BOKTAI_STUB_TITLE, sizeof(header.Title))); + memcpy(header.GameCode, gamecode, strnlen(gamecode, sizeof(header.GameCode))); + header.FixedValue = 0x96; + if (logo) + { + memcpy(header.NintendoLogo, logo, sizeof(header.NintendoLogo)); + } + else + { + memset(header.NintendoLogo, 0xFF, sizeof(header.NintendoLogo)); + } + + return std::make_unique(std::move(rom), FAKE_BOKTAI_ROM_LENGTH, nullptr, 0, userdata); +} + const int CartGameSolarSensor::kLuxLevels[11] = {0, 5, 11, 18, 27, 42, 62, 84, 109, 139, 183}; void CartGameSolarSensor::Reset() @@ -874,6 +925,18 @@ void GBACartSlot::LoadAddon(void* userdata, int type) noexcept case GBAAddon_RumblePak: Cart = std::make_unique(userdata); break; + case GBAAddon_SolarSensorBoktai1: + // US Boktai 1 + Cart = CreateFakeSolarSensorROM("U3IE", nullptr, userdata); + break; + case GBAAddon_SolarSensorBoktai2: + // US Boktai 2 + Cart = CreateFakeSolarSensorROM("U32E", nullptr, userdata); + break; + case GBAAddon_SolarSensorBoktai3: + // JP Boktai 3 + Cart = CreateFakeSolarSensorROM("U33J", nullptr, userdata); + break; default: Log(LogLevel::Warn, "GBACart: !! invalid addon type %d\n", type); diff --git a/src/GBACart.h b/src/GBACart.h index 0c5157ff06..a7096b45e8 100644 --- a/src/GBACart.h +++ b/src/GBACart.h @@ -39,7 +39,7 @@ enum CartType struct GBAHeader { u32 EntryPoint; - u8 NintendoLogo[156]; + u8 NintendoLogo[156]; // must be valid char Title[12]; char GameCode[4]; char MakerCode[2]; @@ -111,6 +111,7 @@ class CartGame : public CartCommon [[nodiscard]] const u8* GetROM() const override { return ROM.get(); } [[nodiscard]] u32 GetROMLength() const override { return ROMLength; } [[nodiscard]] const GBAHeader& GetHeader() const noexcept { return *reinterpret_cast(ROM.get()); } + [[nodiscard]] GBAHeader& GetHeader() noexcept { return *reinterpret_cast(ROM.get()); } u8* GetSaveMemory() const override; u32 GetSaveMemoryLength() const override; @@ -329,6 +330,23 @@ std::unique_ptr ParseROM(const u8* romdata, u32 romlen, const u8* sr /// or \c nullptr if there was an error. std::unique_ptr ParseROM(std::unique_ptr&& romdata, u32 romlen, std::unique_ptr&& sramdata, u32 sramlen, void* userdata = nullptr); +/// Creates a solar sensor-enabled GBA cart without needing a real Boktai ROM. +/// This enables the solar sensor to be used in supported games. +/// Will not contain any SRAM. +/// @param gamecode +/// @param logo The Nintendo logo data embedded in the headers for GBA ROMs and NDS ROMs. +/// Required for the cart to be recognized as a valid GBA cart. +/// Overloads that accept cart objects directly exist as well. +/// If not provided, then it will have to be patched with equivalent data +/// from a real ROM (NDS or GBA) before booting the emulator. +/// @param userdata Optional user data to associate with the cart. +/// @return A CartGameSolarSensor if the ROM was created successfully, +/// or nullptr if any argument is wrong (e.g. an incorrect game code). +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const u8* logo, void* userdata = nullptr) noexcept; +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const NDSCart::CartCommon& cart, void* userdata = nullptr) noexcept; +std::unique_ptr CreateFakeSolarSensorROM(const char* gamecode, const GBACart::CartGame& cart, void* userdata = nullptr) noexcept; + +constexpr const char* BOKTAI_STUB_TITLE = "BOKTAI STUB"; } #endif // GBACART_H diff --git a/src/NDS.cpp b/src/NDS.cpp index 1023d3c0af..7d4535655a 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -539,6 +539,26 @@ void NDS::Reset() void NDS::Start() { Running = true; + + if (ConsoleType != 0) + return; + + auto* ndscart = NDSCartSlot.GetCart(); + if (!ndscart) + return; + + if (auto* cart = GBACartSlot.GetCart(); cart && cart->Type() == GBACart::CartType::GameSolarSensor) + { // If we have a solar sensor cart inserted... + auto& solarcart = *static_cast(cart); + GBACart::GBAHeader& header = solarcart.GetHeader(); + if (strncmp(header.Title, GBACart::BOKTAI_STUB_TITLE, sizeof(header.Title)) == 0) { + // If this is a stub Boktai cart (so we can use the sensor without a full ROM)... + + // ...then copy the Nintendo logo data from the NDS ROM into the stub GBA ROM. + // Otherwise, the GBA cart won't be recognized. + memcpy(header.NintendoLogo, ndscart->GetHeader().NintendoLogo, sizeof(header.NintendoLogo)); + } + } } static const char* StopReasonName(Platform::StopReason reason)