/* Copyright 2016-2024 melonDS team This file is part of melonDS. melonDS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. melonDS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with melonDS. If not, see http://www.gnu.org/licenses/. */ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ARCHIVE_SUPPORT_ENABLED #include "ArchiveUtil.h" #endif #include "EmuInstance.h" #include "Config.h" #include "Platform.h" #include "Net.h" #include "MPInterface.h" #include "NDS.h" #include "DSi.h" #include "SPI.h" #include "RTC.h" #include "DSi_I2C.h" #include "FreeBIOS.h" #include "main.h" using std::make_unique; using std::pair; using std::string; using std::tie; using std::unique_ptr; using std::wstring_convert; using namespace melonDS; using namespace melonDS::Platform; MainWindow* topWindow = nullptr; const string kWifiSettingsPath = "wfcsettings.bin"; extern Net net; EmuInstance::EmuInstance(int inst) : deleting(false), instanceID(inst), globalCfg(Config::GetGlobalTable()), localCfg(Config::GetLocalTable(inst)) { consoleType = globalCfg.GetInt("Emu.ConsoleType"); ndsSave = nullptr; cartType = -1; baseROMDir = ""; baseROMName = ""; baseAssetName = ""; nextCart = nullptr; changeCart = false; gbaSave = nullptr; gbaCartType = -1; baseGBAROMDir = ""; baseGBAROMName = ""; baseGBAAssetName = ""; nextGBACart = nullptr; changeGBACart = false; cheatFile = nullptr; cheatsOn = localCfg.GetBool("EnableCheats"); doLimitFPS = globalCfg.GetBool("LimitFPS"); double val = globalCfg.GetDouble("TargetFPS"); if (val == 0.0) { Platform::Log(Platform::LogLevel::Error, "Target FPS in config invalid\n"); targetFPS = 60.0; } else targetFPS = val; val = globalCfg.GetDouble("FastForwardFPS"); if (val == 0.0) { Platform::Log(Platform::LogLevel::Error, "Fast-Forward FPS in config invalid\n"); fastForwardFPS = 60.0; } else fastForwardFPS = val; val = globalCfg.GetDouble("SlowmoFPS"); if (val == 0.0) { Platform::Log(Platform::LogLevel::Error, "Slow-Mo FPS in config invalid\n"); slowmoFPS = 60.0; } else slowmoFPS = val; doAudioSync = globalCfg.GetBool("AudioSync"); mpAudioMode = globalCfg.GetInt("MP.AudioMode"); nds = nullptr; //updateConsole(); audioInit(); inputInit(); net.RegisterInstance(instanceID); emuThread = new EmuThread(this); numWindows = 0; mainWindow = nullptr; for (int i = 0; i < kMaxWindows; i++) windowList[i] = nullptr; if (inst == 0) topWindow = nullptr; createWindow(); emuThread->start(); // if any extra windows were saved as enabled, open them for (int i = 1; i < kMaxWindows; i++) { std::string key = "Window" + std::to_string(i) + ".Enabled"; bool enable = localCfg.GetBool(key); if (enable) createWindow(i); } } EmuInstance::~EmuInstance() { deleting = true; deleteAllWindows(); emuThread->emuExit(); emuThread->wait(); delete emuThread; net.UnregisterInstance(instanceID); audioDeInit(); inputDeInit(); if (nds) { saveRTCData(); delete nds; } } std::string EmuInstance::instanceFileSuffix() { if (instanceID == 0) return ""; char suffix[16] = {0}; snprintf(suffix, 15, ".%d", instanceID+1); return suffix; } void EmuInstance::createWindow(int id) { if (numWindows >= kMaxWindows) { // TODO return; } if (id == -1) { for (int i = 0; i < kMaxWindows; i++) { if (windowList[i]) continue; id = i; break; } } if (id == -1) return; if (windowList[id]) return; MainWindow* win = new MainWindow(id, this, topWindow); if (!topWindow) topWindow = win; if (!mainWindow) mainWindow = win; windowList[id] = win; numWindows++; emuThread->attachWindow(win); // if creating a secondary window, we may need to initialize its OpenGL context here if (win->hasOpenGL() && (id != 0)) emuThread->initContext(id); bool enable = (numWindows < kMaxWindows); doOnAllWindows([=](MainWindow* win) { win->actNewWindow->setEnabled(enable); }); } void EmuInstance::deleteWindow(int id, bool close) { if (id >= kMaxWindows) return; MainWindow* win = windowList[id]; if (!win) return; if (win->hasOpenGL()) emuThread->deinitContext(id); emuThread->detachWindow(win); windowList[id] = nullptr; numWindows--; if (topWindow == win) topWindow = nullptr; if (mainWindow == win) mainWindow = nullptr; if (close) win->close(); if (deleting) return; if (numWindows == 0) { // if we closed the last window, delete the instance // if the main window is closed, Qt will take care of closing any secondary windows deleteEmuInstance(instanceID); } else { bool enable = (numWindows < kMaxWindows); doOnAllWindows([=](MainWindow* win) { win->actNewWindow->setEnabled(enable); }); } } void EmuInstance::deleteAllWindows() { for (int i = kMaxWindows-1; i >= 0; i--) deleteWindow(i, true); } void EmuInstance::doOnAllWindows(std::function func, int exclude) { for (int i = 0; i < kMaxWindows; i++) { if (i == exclude) continue; if (!windowList[i]) continue; func(windowList[i]); } } void EmuInstance::saveEnabledWindows() { doOnAllWindows([=](MainWindow* win) { win->saveEnabled(true); }); } void EmuInstance::broadcastCommand(int cmd, QVariant param) { broadcastInstanceCommand(cmd, param, instanceID); } void EmuInstance::handleCommand(int cmd, QVariant& param) { switch (cmd) { case InstCmd_Pause: emuThread->emuPause(false); break; case InstCmd_Unpause: emuThread->emuUnpause(false); break; case InstCmd_UpdateRecentFiles: for (int i = 0; i < kMaxWindows; i++) { if (windowList[i]) windowList[i]->loadRecentFilesMenu(true); } break; /*case InstCmd_UpdateVideoSettings: mainWindow->updateVideoSettings(param.value()); break;*/ } } void EmuInstance::osdAddMessage(unsigned int color, const char* fmt, ...) { if (fmt == nullptr) return; char msg[256]; va_list args; va_start(args, fmt); vsnprintf(msg, 256, fmt, args); va_end(args); for (int i = 0; i < kMaxWindows; i++) { if (windowList[i]) windowList[i]->osdAddMessage(color, msg); } } bool EmuInstance::emuIsActive() { return emuThread->emuIsActive(); } void EmuInstance::emuStop(StopReason reason) { if (reason != StopReason::External) emuThread->emuStop(false); switch (reason) { case StopReason::GBAModeNotSupported: osdAddMessage(0xFFA0A0, "GBA mode not supported"); break; case StopReason::BadExceptionRegion: osdAddMessage(0xFFA0A0, "Internal error"); break; case StopReason::PowerOff: case StopReason::External: osdAddMessage(0xFFC040, "Shutdown"); default: break; } } bool EmuInstance::usesOpenGL() { return globalCfg.GetBool("Screen.UseGL") || (globalCfg.GetInt("3D.Renderer") != renderer3D_Software); } void EmuInstance::initOpenGL(int win) { if (windowList[win]) windowList[win]->initOpenGL(); setVSyncGL(true); } void EmuInstance::deinitOpenGL(int win) { if (windowList[win]) windowList[win]->deinitOpenGL(); } void EmuInstance::setVSyncGL(bool vsync) { int intv; vsync = vsync && globalCfg.GetBool("Screen.VSync"); if (vsync) intv = globalCfg.GetInt("Screen.VSyncInterval"); else intv = 0; for (int i = 0; i < kMaxWindows; i++) { if (windowList[i]) windowList[i]->setGLSwapInterval(intv); } } void EmuInstance::makeCurrentGL() { mainWindow->makeCurrentGL(); } void EmuInstance::drawScreenGL() { for (int i = 0; i < kMaxWindows; i++) { if (windowList[i]) windowList[i]->drawScreenGL(); } } int EmuInstance::lastSep(const std::string& path) { int i = path.length() - 1; while (i >= 0) { if (path[i] == '/' || path[i] == '\\') return i; i--; } return -1; } string EmuInstance::getAssetPath(bool gba, const string& configpath, const string& ext, const string& file = "") { string result; if (configpath.empty()) result = gba ? baseGBAROMDir : baseROMDir; else result = configpath; // cut off trailing slashes for (;;) { int i = result.length() - 1; if (i < 0) break; if (result[i] == '/' || result[i] == '\\') result.resize(i); else break; } if (!result.empty()) result += '/'; if (file.empty()) { std::string& baseName = gba ? baseGBAAssetName : baseAssetName; if (baseName.empty()) result += "firmware"; else result += baseName; } else { result += file; } result += ext; return result; } QString EmuInstance::verifyDSBIOS() { FileHandle* f; long len; f = Platform::OpenLocalFile(globalCfg.GetString("DS.BIOS9Path"), FileMode::Read); if (!f) return "DS ARM9 BIOS was not found or could not be accessed. Check your emu settings."; len = FileLength(f); if (len != 0x1000) { CloseFile(f); return "DS ARM9 BIOS is not a valid BIOS dump."; } CloseFile(f); f = Platform::OpenLocalFile(globalCfg.GetString("DS.BIOS7Path"), FileMode::Read); if (!f) return "DS ARM7 BIOS was not found or could not be accessed. Check your emu settings."; len = FileLength(f); if (len != 0x4000) { CloseFile(f); return "DS ARM7 BIOS is not a valid BIOS dump."; } CloseFile(f); return ""; } QString EmuInstance::verifyDSiBIOS() { FileHandle* f; long len; // TODO: check the first 32 bytes f = Platform::OpenLocalFile(globalCfg.GetString("DSi.BIOS9Path"), FileMode::Read); if (!f) return "DSi ARM9 BIOS was not found or could not be accessed. Check your emu settings."; len = FileLength(f); if (len != 0x10000) { CloseFile(f); return "DSi ARM9 BIOS is not a valid BIOS dump."; } CloseFile(f); f = Platform::OpenLocalFile(globalCfg.GetString("DSi.BIOS7Path"), FileMode::Read); if (!f) return "DSi ARM7 BIOS was not found or could not be accessed. Check your emu settings."; len = FileLength(f); if (len != 0x10000) { CloseFile(f); return "DSi ARM7 BIOS is not a valid BIOS dump."; } CloseFile(f); return ""; } QString EmuInstance::verifyDSFirmware() { FileHandle* f; long len; std::string fwpath = globalCfg.GetString("DS.FirmwarePath"); f = Platform::OpenLocalFile(fwpath, FileMode::Read); if (!f) return "DS firmware was not found or could not be accessed. Check your emu settings."; if (!Platform::CheckFileWritable(fwpath)) return "DS firmware is unable to be written to.\nPlease check file/folder write permissions."; len = FileLength(f); if (len == 0x20000) { // 128KB firmware, not bootable CloseFile(f); // TODO report it somehow? detect in core? return ""; } else if (len != 0x40000 && len != 0x80000) { CloseFile(f); return "DS firmware is not a valid firmware dump."; } CloseFile(f); return ""; } QString EmuInstance::verifyDSiFirmware() { FileHandle* f; long len; std::string fwpath = globalCfg.GetString("DSi.FirmwarePath"); f = Platform::OpenLocalFile(fwpath, FileMode::Read); if (!f) return "DSi firmware was not found or could not be accessed. Check your emu settings."; if (!Platform::CheckFileWritable(fwpath)) return "DSi firmware is unable to be written to.\nPlease check file/folder write permissions."; len = FileLength(f); if (len != 0x20000) { // not 128KB // TODO: check whether those work CloseFile(f); return "DSi firmware is not a valid firmware dump."; } CloseFile(f); return ""; } QString EmuInstance::verifyDSiNAND() { FileHandle* f; long len; std::string nandpath = globalCfg.GetString("DSi.NANDPath"); f = Platform::OpenLocalFile(nandpath, FileMode::ReadWriteExisting); if (!f) return "DSi NAND was not found or could not be accessed. Check your emu settings."; if (!Platform::CheckFileWritable(nandpath)) return "DSi NAND is unable to be written to.\nPlease check file/folder write permissions."; // TODO: some basic checks // check that it has the nocash footer, and all CloseFile(f); return ""; } QString EmuInstance::verifySetup() { QString res; bool extbios = globalCfg.GetBool("Emu.ExternalBIOSEnable"); int console = globalCfg.GetInt("Emu.ConsoleType"); if (extbios) { res = verifyDSBIOS(); if (!res.isEmpty()) return res; } if (console == 1) { res = verifyDSiBIOS(); if (!res.isEmpty()) return res; if (extbios) { res = verifyDSiFirmware(); if (!res.isEmpty()) return res; } res = verifyDSiNAND(); if (!res.isEmpty()) return res; } else { if (extbios) { res = verifyDSFirmware(); if (!res.isEmpty()) return res; } } return ""; } std::string EmuInstance::getEffectiveFirmwareSavePath() { if (!globalCfg.GetBool("Emu.ExternalBIOSEnable")) { return GetLocalFilePath(kWifiSettingsPath); } if (consoleType == 1) { return globalCfg.GetString("DSi.FirmwarePath"); } else { return globalCfg.GetString("DS.FirmwarePath"); } } // Initializes the firmware save manager with the selected firmware image's path // OR the path to the wi-fi settings. void EmuInstance::initFirmwareSaveManager() noexcept { firmwareSave = std::make_unique(getEffectiveFirmwareSavePath() + instanceFileSuffix()); } std::string EmuInstance::getSavestateName(int slot) { std::string ext = ".ml"; ext += (char)('0'+slot); return getAssetPath(false, localCfg.GetString("SavestatePath"), ext); } bool EmuInstance::savestateExists(int slot) { std::string ssfile = getSavestateName(slot); return Platform::FileExists(ssfile); } bool EmuInstance::loadState(const std::string& filename) { Platform::FileHandle* file = Platform::OpenFile(filename, Platform::FileMode::Read); if (file == nullptr) { // If we couldn't open the state file... Platform::Log(Platform::LogLevel::Error, "Failed to open state file \"%s\"\n", filename.c_str()); return false; } std::unique_ptr backup = std::make_unique(Savestate::DEFAULT_SIZE); if (backup->Error) { // If we couldn't allocate memory for the backup... Platform::Log(Platform::LogLevel::Error, "Failed to allocate memory for state backup\n"); Platform::CloseFile(file); return false; } if (!nds->DoSavestate(backup.get()) || backup->Error) { // Back up the emulator's state. If that failed... Platform::Log(Platform::LogLevel::Error, "Failed to back up state, aborting load (from \"%s\")\n", filename.c_str()); Platform::CloseFile(file); return false; } // We'll store the backup once we're sure that the state was loaded. // Now that we know the file and backup are both good, let's load the new state. // Get the size of the file that we opened size_t size = Platform::FileLength(file); // Allocate exactly as much memory as we need for the savestate std::vector buffer(size); if (Platform::FileRead(buffer.data(), size, 1, file) == 0) { // Read the state file into the buffer. If that failed... Platform::Log(Platform::LogLevel::Error, "Failed to read %u-byte state file \"%s\"\n", size, filename.c_str()); Platform::CloseFile(file); return false; } Platform::CloseFile(file); // done with the file now // Get ready to load the state from the buffer into the emulator std::unique_ptr state = std::make_unique(buffer.data(), size, false); if (!nds->DoSavestate(state.get()) || state->Error) { // If we couldn't load the savestate from the buffer... Platform::Log(Platform::LogLevel::Error, "Failed to load state file \"%s\" into emulator\n", filename.c_str()); return false; } // The backup was made and the state was loaded, so we can store the backup now. backupState = std::move(backup); // This will clean up any existing backup assert(backup == nullptr); if (globalCfg.GetBool("Savestate.RelocSRAM") && ndsSave) { previousSaveFile = ndsSave->GetPath(); std::string savefile = filename.substr(lastSep(filename)+1); savefile = getAssetPath(false, localCfg.GetString("SaveFilePath"), ".sav", savefile); savefile += instanceFileSuffix(); ndsSave->SetPath(savefile, true); } savestateLoaded = true; return true; } bool EmuInstance::saveState(const std::string& filename) { Platform::FileHandle* file = Platform::OpenFile(filename, Platform::FileMode::Write); if (file == nullptr) { // If the file couldn't be opened... return false; } Savestate state; if (state.Error) { // If there was an error creating the state (and allocating its memory)... Platform::CloseFile(file); return false; } // Write the savestate to the in-memory buffer nds->DoSavestate(&state); if (state.Error) { Platform::CloseFile(file); return false; } if (Platform::FileWrite(state.Buffer(), state.Length(), 1, file) == 0) { // Write the Savestate buffer to the file. If that fails... Platform::Log(Platform::Error, "Failed to write %d-byte savestate to %s\n", state.Length(), filename.c_str() ); Platform::CloseFile(file); return false; } Platform::CloseFile(file); if (globalCfg.GetBool("Savestate.RelocSRAM") && ndsSave) { std::string savefile = filename.substr(lastSep(filename)+1); savefile = getAssetPath(false, localCfg.GetString("SaveFilePath"), ".sav", savefile); savefile += instanceFileSuffix(); ndsSave->SetPath(savefile, false); } return true; } void EmuInstance::undoStateLoad() { if (!savestateLoaded || !backupState) return; // Rewind the backup state and put it in load mode backupState->Rewind(false); // pray that this works // what do we do if it doesn't??? // but it should work. nds->DoSavestate(backupState.get()); if (ndsSave && (!previousSaveFile.empty())) { ndsSave->SetPath(previousSaveFile, true); } } void EmuInstance::unloadCheats() { cheatFile = nullptr; // cleaned up by unique_ptr nds->AREngine.Cheats.clear(); } void EmuInstance::loadCheats() { unloadCheats(); std::string filename = getAssetPath(false, localCfg.GetString("CheatFilePath"), ".mch"); // TODO: check for error (malformed cheat file, ...) cheatFile = std::make_unique(filename); if (cheatsOn) { nds->AREngine.Cheats = cheatFile->GetCodes(); } else { nds->AREngine.Cheats.clear(); } } std::unique_ptr EmuInstance::loadARM9BIOS() noexcept { if (!globalCfg.GetBool("Emu.ExternalBIOSEnable")) { return std::make_unique(bios_arm9_bin); } string path = globalCfg.GetString("DS.BIOS9Path"); if (FileHandle* f = OpenLocalFile(path, Read)) { std::unique_ptr bios = std::make_unique(); FileRewind(f); FileRead(bios->data(), bios->size(), 1, f); CloseFile(f); Log(Info, "ARM9 BIOS loaded from %s\n", path.c_str()); return bios; } Log(Warn, "ARM9 BIOS not found\n"); return nullptr; } std::unique_ptr EmuInstance::loadARM7BIOS() noexcept { if (!globalCfg.GetBool("Emu.ExternalBIOSEnable")) { return std::make_unique(bios_arm7_bin); } string path = globalCfg.GetString("DS.BIOS7Path"); if (FileHandle* f = OpenLocalFile(path, Read)) { std::unique_ptr bios = std::make_unique(); FileRead(bios->data(), bios->size(), 1, f); CloseFile(f); Log(Info, "ARM7 BIOS loaded from %s\n", path.c_str()); return bios; } Log(Warn, "ARM7 BIOS not found\n"); return nullptr; } std::unique_ptr EmuInstance::loadDSiARM9BIOS() noexcept { string path = globalCfg.GetString("DSi.BIOS9Path"); if (FileHandle* f = OpenLocalFile(path, Read)) { std::unique_ptr bios = std::make_unique(); FileRead(bios->data(), bios->size(), 1, f); CloseFile(f); if (!globalCfg.GetBool("DSi.FullBIOSBoot")) { // herp *(u32*)bios->data() = 0xEAFFFFFE; // overwrites the reset vector // TODO!!!! // hax the upper 32K out of the goddamn DSi // done that :) -pcy } Log(Info, "ARM9i BIOS loaded from %s\n", path.c_str()); return bios; } Log(Warn, "ARM9i BIOS not found\n"); return nullptr; } std::unique_ptr EmuInstance::loadDSiARM7BIOS() noexcept { string path = globalCfg.GetString("DSi.BIOS7Path"); if (FileHandle* f = OpenLocalFile(path, Read)) { std::unique_ptr bios = std::make_unique(); FileRead(bios->data(), bios->size(), 1, f); CloseFile(f); if (!globalCfg.GetBool("DSi.FullBIOSBoot")) { // herp *(u32*)bios->data() = 0xEAFFFFFE; // overwrites the reset vector // TODO!!!! // hax the upper 32K out of the goddamn DSi // done that :) -pcy } Log(Info, "ARM7i BIOS loaded from %s\n", path.c_str()); return bios; } Log(Warn, "ARM7i BIOS not found\n"); return nullptr; } Firmware EmuInstance::generateFirmware(int type) noexcept { // Construct the default firmware... string settingspath; Firmware firmware = Firmware(type); assert(firmware.Buffer() != nullptr); // If using generated firmware, we keep the wi-fi settings on the host disk separately. // Wi-fi access point data includes Nintendo WFC settings, // and if we didn't keep them then the player would have to reset them in each session. // We don't need to save the whole firmware, just the part that may actually change. if (FileHandle* f = OpenLocalFile(kWifiSettingsPath, Read)) {// If we have Wi-fi settings to load... constexpr unsigned TOTAL_WFC_SETTINGS_SIZE = 3 * (sizeof(Firmware::WifiAccessPoint) + sizeof(Firmware::ExtendedWifiAccessPoint)); if (!FileRead(firmware.GetExtendedAccessPointPosition(), TOTAL_WFC_SETTINGS_SIZE, 1, f)) { // If we couldn't read the Wi-fi settings from this file... Log(Warn, "Failed to read Wi-fi settings from \"%s\"; using defaults instead\n", kWifiSettingsPath.c_str()); // The access point and extended access point segments might // be in different locations depending on the firmware revision, // but our generated firmware always keeps them next to each other. // (Extended access points first, then regular ones.) firmware.GetAccessPoints() = { Firmware::WifiAccessPoint(type), Firmware::WifiAccessPoint(), Firmware::WifiAccessPoint(), }; firmware.GetExtendedAccessPoints() = { Firmware::ExtendedWifiAccessPoint(), Firmware::ExtendedWifiAccessPoint(), Firmware::ExtendedWifiAccessPoint(), }; firmware.UpdateChecksums(); CloseFile(f); } } customizeFirmware(firmware, true); // If we don't have Wi-fi settings to load, // then the defaults will have already been populated by the constructor. return firmware; } std::optional EmuInstance::loadFirmware(int type) noexcept { if (!globalCfg.GetBool("Emu.ExternalBIOSEnable")) { // If we're using built-in firmware... if (type == 1) { // TODO: support generating a firmware for DSi mode } else { return generateFirmware(type); } } //const string& firmwarepath = type == 1 ? Config::DSiFirmwarePath : Config::FirmwarePath; string firmwarepath; if (type == 1) firmwarepath = globalCfg.GetString("DSi.FirmwarePath"); else firmwarepath = globalCfg.GetString("DS.FirmwarePath"); Log(Debug, "SPI firmware: loading from file %s\n", firmwarepath.c_str()); FileHandle* file = OpenLocalFile(firmwarepath, Read); if (!file) { Log(Error, "SPI firmware: couldn't open firmware file!\n"); return std::nullopt; } Firmware firmware(file); CloseFile(file); if (!firmware.Buffer()) { Log(Error, "SPI firmware: couldn't read firmware file!\n"); return std::nullopt; } customizeFirmware(firmware, localCfg.GetBool("Firmware.OverrideSettings")); return firmware; } std::optional EmuInstance::loadNAND(const std::array& arm7ibios) noexcept { string path = globalCfg.GetString("DSi.NANDPath"); FileHandle* nandfile = OpenLocalFile(path, ReadWriteExisting); if (!nandfile) return std::nullopt; DSi_NAND::NANDImage nandImage(nandfile, &arm7ibios[0x8308]); if (!nandImage) { Log(Error, "Failed to parse DSi NAND\n"); return std::nullopt; // the NANDImage takes ownership of the FileHandle, no need to clean it up here } // scoped so that mount isn't alive when we move the NAND image to DSi::NANDImage { auto mount = DSi_NAND::NANDMount(nandImage); if (!mount) { Log(Error, "Failed to mount DSi NAND\n"); return std::nullopt; } DSi_NAND::DSiFirmwareSystemSettings settings {}; if (!mount.ReadUserData(settings)) { Log(Error, "Failed to read DSi NAND user data\n"); return std::nullopt; } // override user settings, if needed if (localCfg.GetBool("Firmware.OverrideSettings")) { auto firmcfg = localCfg.GetTable("Firmware"); // we store relevant strings as UTF-8, so we need to convert them to UTF-16 //auto converter = wstring_convert, char16_t>{}; // setting up username auto username = firmcfg.GetQString("Username"); size_t usernameLength = std::min((int) username.length(), 10); memset(&settings.Nickname, 0, sizeof(settings.Nickname)); memcpy(&settings.Nickname, username.utf16(), usernameLength * sizeof(char16_t)); // setting language settings.Language = static_cast(firmcfg.GetInt("Language")); // setting up color settings.FavoriteColor = firmcfg.GetInt("FavouriteColour"); // setting up birthday settings.BirthdayMonth = firmcfg.GetInt("BirthdayMonth"); settings.BirthdayDay = firmcfg.GetInt("BirthdayDay"); // setup message auto message = firmcfg.GetQString("Message"); size_t messageLength = std::min((int) message.length(), 26); memset(&settings.Message, 0, sizeof(settings.Message)); memcpy(&settings.Message, message.utf16(), messageLength * sizeof(char16_t)); // TODO: make other items configurable? } // fix touchscreen coords settings.TouchCalibrationADC1 = {0, 0}; settings.TouchCalibrationPixel1 = {0, 0}; settings.TouchCalibrationADC2 = {255 << 4, 191 << 4}; settings.TouchCalibrationPixel2 = {255, 191}; settings.UpdateHash(); if (!mount.ApplyUserData(settings)) { Log(LogLevel::Error, "Failed to write patched DSi NAND user data\n"); return std::nullopt; } } return nandImage; } constexpr u64 MB(u64 i) { return i * 1024 * 1024; } constexpr u64 imgsizes[] = {0, MB(256), MB(512), MB(1024), MB(2048), MB(4096)}; std::optional EmuInstance::getSDCardArgs(const string& key) noexcept { // key = DSi.SD or DLDI Config::Table sdopt = globalCfg.GetTable(key); if (!sdopt.GetBool("Enable")) return std::nullopt; return FATStorageArgs { sdopt.GetString("ImagePath"), imgsizes[sdopt.GetInt("ImageSize")], sdopt.GetBool("ReadOnly"), sdopt.GetBool("FolderSync") ? std::make_optional(sdopt.GetString("FolderPath")) : std::nullopt }; } std::optional EmuInstance::loadSDCard(const string& key) noexcept { auto args = getSDCardArgs(key); if (!args.has_value()) return std::nullopt; return FATStorage(args.value()); } void EmuInstance::enableCheats(bool enable) { cheatsOn = enable; if (cheatsOn && cheatFile) nds->AREngine.Cheats = cheatFile->GetCodes(); else nds->AREngine.Cheats.clear(); } ARCodeFile* EmuInstance::getCheatFile() { return cheatFile.get(); } void EmuInstance::setBatteryLevels() { if (consoleType == 1) { auto dsi = static_cast(nds); dsi->I2C.GetBPTWL()->SetBatteryLevel(localCfg.GetInt("DSi.Battery.Level")); dsi->I2C.GetBPTWL()->SetBatteryCharging(localCfg.GetBool("DSi.Battery.Charging")); } else { nds->SPI.GetPowerMan()->SetBatteryLevelOkay(localCfg.GetBool("DS.Battery.LevelOkay")); } } void EmuInstance::loadRTCData() { auto file = Platform::OpenLocalFile("rtc.bin", Platform::FileMode::Read); if (file) { RTC::StateData state; Platform::FileRead(&state, sizeof(state), 1, file); Platform::CloseFile(file); nds->RTC.SetState(state); } } void EmuInstance::saveRTCData() { auto file = Platform::OpenLocalFile("rtc.bin", Platform::FileMode::Write); if (file) { RTC::StateData state; nds->RTC.GetState(state); Platform::FileWrite(&state, sizeof(state), 1, file); Platform::CloseFile(file); } } void EmuInstance::setDateTime() { QDateTime hosttime = QDateTime::currentDateTime(); QDateTime time = hosttime.addSecs(localCfg.GetInt64("RTC.Offset")); nds->RTC.SetDateTime(time.date().year(), time.date().month(), time.date().day(), time.time().hour(), time.time().minute(), time.time().second()); } bool EmuInstance::updateConsole() noexcept { // update the console type consoleType = globalCfg.GetInt("Emu.ConsoleType"); // Let's get the cart we want to use; // if we want to keep the cart, we'll eject it from the existing console first. std::unique_ptr nextndscart; if (!changeCart) { // If we want to keep the existing cart (if any)... nextndscart = nds ? nds->EjectCart() : nullptr; } else { nextndscart = std::move(nextCart); changeCart = false; } if (auto* cartsd = dynamic_cast(nextndscart.get())) { // LoadDLDISDCard will return nullopt if the SD card is disabled; // SetSDCard will accept nullopt, which means no SD card cartsd->SetSDCard(getSDCardArgs("DLDI")); } std::unique_ptr nextgbacart; if (!changeGBACart) { nextgbacart = nds ? nds->EjectGBACart() : nullptr; } else { nextgbacart = std::move(nextGBACart); changeGBACart = false; } auto arm9bios = loadARM9BIOS(); if (!arm9bios) return false; auto arm7bios = loadARM7BIOS(); if (!arm7bios) return false; auto firmware = loadFirmware(consoleType); if (!firmware) return false; #ifdef JIT_ENABLED Config::Table jitopt = globalCfg.GetTable("JIT"); JITArgs _jitargs { static_cast(jitopt.GetInt("MaxBlockSize")), jitopt.GetBool("LiteralOptimisations"), jitopt.GetBool("BranchOptimisations"), jitopt.GetBool("FastMemory"), }; auto jitargs = jitopt.GetBool("Enable") ? std::make_optional(_jitargs) : std::nullopt; #else std::optional jitargs = std::nullopt; #endif #ifdef GDBSTUB_ENABLED Config::Table gdbopt = localCfg.GetTable("Gdb"); GDBArgs _gdbargs { static_cast(gdbopt.GetInt("ARM7.Port")), static_cast(gdbopt.GetInt("ARM9.Port")), gdbopt.GetBool("ARM7.BreakOnStartup"), gdbopt.GetBool("ARM9.BreakOnStartup"), }; auto gdbargs = gdbopt.GetBool("Enabled") ? std::make_optional(_gdbargs) : std::nullopt; #else std::optional gdbargs = std::nullopt; #endif NDSArgs ndsargs { std::move(arm9bios), std::move(arm7bios), std::move(*firmware), jitargs, static_cast(globalCfg.GetInt("Audio.BitDepth")), static_cast(globalCfg.GetInt("Audio.Interpolation")), gdbargs, }; NDSArgs* args = &ndsargs; std::optional dsiargs = std::nullopt; if (consoleType == 1) { auto arm7ibios = loadDSiARM7BIOS(); if (!arm7ibios) return false; auto arm9ibios = loadDSiARM9BIOS(); if (!arm9ibios) return false; auto nand = loadNAND(*arm7ibios); if (!nand) return false; auto sdcard = loadSDCard("DSi.SD"); DSiArgs _dsiargs { std::move(ndsargs), std::move(arm9ibios), std::move(arm7ibios), std::move(*nand), std::move(sdcard), globalCfg.GetBool("DSi.FullBIOSBoot"), }; dsiargs = std::move(_dsiargs); args = &(*dsiargs); } renderLock.lock(); if ((!nds) || (consoleType != nds->ConsoleType)) { if (nds) { saveRTCData(); delete nds; } if (consoleType == 1) nds = new DSi(std::move(dsiargs.value()), this); else nds = new NDS(std::move(ndsargs), this); nds->Reset(); loadRTCData(); //emuThread->updateVideoRenderer(); // not actually needed? } else { nds->SetARM7BIOS(*args->ARM7BIOS); nds->SetARM9BIOS(*args->ARM9BIOS); nds->SetFirmware(std::move(args->Firmware)); nds->SetJITArgs(args->JIT); nds->SetGdbArgs(args->GDB); nds->SPU.SetInterpolation(args->Interpolation); nds->SPU.SetDegrade10Bit(args->BitDepth); if (consoleType == 1) { DSi* dsi = (DSi*)nds; DSiArgs& _dsiargs = *dsiargs; dsi->SetFullBIOSBoot(_dsiargs.FullBIOSBoot); dsi->ARM7iBIOS = *_dsiargs.ARM7iBIOS; dsi->ARM9iBIOS = *_dsiargs.ARM9iBIOS; dsi->SetNAND(std::move(_dsiargs.NANDImage)); dsi->SetSDCard(std::move(_dsiargs.DSiSDCard)); // We're moving the optional, not the card // (inserting std::nullopt here is okay, it means no card) } } // loads the carts later -- to be sure that everything else is initialized nds->SetNDSCart(std::move(nextndscart)); if (consoleType == 1) nds->EjectGBACart(); else nds->SetGBACart(std::move(nextgbacart)); renderLock.unlock(); loadCheats(); return true; } void EmuInstance::reset() { updateConsole(); if (consoleType == 1) ejectGBACart(); nds->Reset(); setBatteryLevels(); setDateTime(); if ((cartType != -1) && ndsSave) { std::string oldsave = ndsSave->GetPath(); std::string newsave = getAssetPath(false, localCfg.GetString("SaveFilePath"), ".sav"); newsave += instanceFileSuffix(); if (oldsave != newsave) ndsSave->SetPath(newsave, false); } if ((gbaCartType != -1) && gbaSave) { std::string oldsave = gbaSave->GetPath(); std::string newsave = getAssetPath(true, localCfg.GetString("SaveFilePath"), ".sav"); newsave += instanceFileSuffix(); if (oldsave != newsave) gbaSave->SetPath(newsave, false); } initFirmwareSaveManager(); if (firmwareSave) { std::string oldsave = firmwareSave->GetPath(); string newsave; if (globalCfg.GetBool("Emu.ExternalBIOSEnable")) { if (consoleType == 1) newsave = globalCfg.GetString("DSi.FirmwarePath") + instanceFileSuffix(); else newsave = globalCfg.GetString("DS.FirmwarePath") + instanceFileSuffix(); } else { newsave = GetLocalFilePath(kWifiSettingsPath + instanceFileSuffix()); } if (oldsave != newsave) { // If the player toggled the ConsoleType or ExternalBIOSEnable... firmwareSave->SetPath(newsave, true); } } if (!baseROMName.empty()) { if (globalCfg.GetBool("Emu.DirectBoot") || nds->NeedsDirectBoot()) { nds->SetupDirectBoot(baseROMName); } } nds->Start(); } bool EmuInstance::bootToMenu(QString& errorstr) { // Keep whatever cart is in the console, if any. if (!updateConsole()) { // Try to update the console, but keep the existing cart. If that fails... errorstr = "Failed to boot the firmware."; return false; } // BIOS and firmware files are loaded, patched, and installed in UpdateConsole if (nds->NeedsDirectBoot()) { errorstr = "This firmware is not bootable."; return false; } initFirmwareSaveManager(); nds->Reset(); setBatteryLevels(); setDateTime(); return true; } u32 EmuInstance::decompressROM(const u8* inContent, const u32 inSize, unique_ptr& outContent) { u64 realSize = ZSTD_getFrameContentSize(inContent, inSize); const u32 maxSize = 0x40000000; if (realSize == ZSTD_CONTENTSIZE_ERROR || (realSize > maxSize && realSize != ZSTD_CONTENTSIZE_UNKNOWN)) { return 0; } if (realSize != ZSTD_CONTENTSIZE_UNKNOWN) { auto newOutContent = make_unique(realSize); u64 decompressed = ZSTD_decompress(newOutContent.get(), realSize, inContent, inSize); if (ZSTD_isError(decompressed)) { outContent = nullptr; return 0; } outContent = std::move(newOutContent); return realSize; } else { ZSTD_DStream* dStream = ZSTD_createDStream(); ZSTD_initDStream(dStream); ZSTD_inBuffer inBuf = { .src = inContent, .size = inSize, .pos = 0 }; const u32 startSize = 1024 * 1024 * 16; u8* partialOutContent = (u8*) malloc(startSize); ZSTD_outBuffer outBuf = { .dst = partialOutContent, .size = startSize, .pos = 0 }; size_t result; do { result = ZSTD_decompressStream(dStream, &outBuf, &inBuf); if (ZSTD_isError(result)) { ZSTD_freeDStream(dStream); free(outBuf.dst); return 0; } // if result == 0 and not inBuf.pos < inBuf.size, go again to let zstd flush everything. if (result == 0) continue; if (outBuf.pos == outBuf.size) { outBuf.size *= 2; if (outBuf.size > maxSize) { ZSTD_freeDStream(dStream); free(outBuf.dst); return 0; } outBuf.dst = realloc(outBuf.dst, outBuf.size); } } while (inBuf.pos < inBuf.size); outContent = make_unique(outBuf.pos); memcpy(outContent.get(), outBuf.dst, outBuf.pos); ZSTD_freeDStream(dStream); free(outBuf.dst); return outBuf.size; } } void EmuInstance::clearBackupState() { if (backupState != nullptr) { backupState = nullptr; } } pair, string> EmuInstance::generateDefaultFirmware() { // Construct the default firmware... string settingspath; std::unique_ptr firmware = std::make_unique(consoleType); assert(firmware->Buffer() != nullptr); // Try to open the instanced Wi-fi settings, falling back to the regular Wi-fi settings if they don't exist. // We don't need to save the whole firmware, just the part that may actually change. std::string wfcsettingspath = kWifiSettingsPath; settingspath = wfcsettingspath + instanceFileSuffix(); FileHandle* f = Platform::OpenLocalFile(settingspath, FileMode::Read); if (!f) { settingspath = wfcsettingspath; f = Platform::OpenLocalFile(settingspath, FileMode::Read); } // If using generated firmware, we keep the wi-fi settings on the host disk separately. // Wi-fi access point data includes Nintendo WFC settings, // and if we didn't keep them then the player would have to reset them in each session. if (f) { // If we have Wi-fi settings to load... constexpr unsigned TOTAL_WFC_SETTINGS_SIZE = 3 * (sizeof(Firmware::WifiAccessPoint) + sizeof(Firmware::ExtendedWifiAccessPoint)); // The access point and extended access point segments might // be in different locations depending on the firmware revision, // but our generated firmware always keeps them next to each other. // (Extended access points first, then regular ones.) if (!FileRead(firmware->GetExtendedAccessPointPosition(), TOTAL_WFC_SETTINGS_SIZE, 1, f)) { // If we couldn't read the Wi-fi settings from this file... Platform::Log(Platform::LogLevel::Warn, "Failed to read Wi-fi settings from \"%s\"; using defaults instead\n", wfcsettingspath.c_str()); firmware->GetAccessPoints() = { Firmware::WifiAccessPoint(consoleType), Firmware::WifiAccessPoint(), Firmware::WifiAccessPoint(), }; firmware->GetExtendedAccessPoints() = { Firmware::ExtendedWifiAccessPoint(), Firmware::ExtendedWifiAccessPoint(), Firmware::ExtendedWifiAccessPoint(), }; } firmware->UpdateChecksums(); CloseFile(f); } // If we don't have Wi-fi settings to load, // then the defaults will have already been populated by the constructor. return std::make_pair(std::move(firmware), std::move(wfcsettingspath)); } bool EmuInstance::parseMacAddress(void* data) { const std::string mac_in = localCfg.GetString("Firmware.MAC"); u8* mac_out = (u8*)data; int o = 0; u8 tmp = 0; for (int i = 0; i < 18; i++) { char c = mac_in[i]; if (c == '\0') break; int n; if (c >= '0' && c <= '9') n = c - '0'; else if (c >= 'a' && c <= 'f') n = c - 'a' + 10; else if (c >= 'A' && c <= 'F') n = c - 'A' + 10; else continue; if (!(o & 1)) tmp = n; else mac_out[o >> 1] = n | (tmp << 4); o++; if (o >= 12) return true; } return false; } void EmuInstance::customizeFirmware(Firmware& firmware, bool overridesettings) noexcept { if (overridesettings) { auto ¤tData = firmware.GetEffectiveUserData(); auto firmcfg = localCfg.GetTable("Firmware"); // setting up username auto username = firmcfg.GetQString("Username"); if (!username.isEmpty()) { // If the frontend defines a username, take it. If not, leave the existing one. size_t usernameLength = std::min((int) username.length(), 10); currentData.NameLength = usernameLength; memcpy(currentData.Nickname, username.utf16(), usernameLength * sizeof(char16_t)); } auto language = static_cast(firmcfg.GetInt("Language")); if (language != Firmware::Language::Reserved) { // If the frontend specifies a language (rather than using the existing value)... currentData.Settings &= ~Firmware::Language::Reserved; // ..clear the existing language... currentData.Settings |= language; // ...and set the new one. } // setting up color u8 favoritecolor = firmcfg.GetInt("FavouriteColour"); if (favoritecolor != 0xFF) { currentData.FavoriteColor = favoritecolor; } u8 birthmonth = firmcfg.GetInt("BirthdayMonth"); if (birthmonth != 0) { // If the frontend specifies a birth month (rather than using the existing value)... currentData.BirthdayMonth = birthmonth; } u8 birthday = firmcfg.GetInt("BirthdayDay"); if (birthday != 0) { // If the frontend specifies a birthday (rather than using the existing value)... currentData.BirthdayDay = birthday; } // setup message auto message = firmcfg.GetQString("Message"); if (!message.isEmpty()) { size_t messageLength = std::min((int) message.length(), 26); currentData.MessageLength = messageLength; memcpy(currentData.Message, message.data(), messageLength * sizeof(char16_t)); } } MacAddress mac; bool rep = false; auto& header = firmware.GetHeader(); memcpy(&mac, header.MacAddr.data(), sizeof(MacAddress)); if (overridesettings) { MacAddress configuredMac; rep = parseMacAddress(&configuredMac); rep &= (configuredMac != MacAddress()); if (rep) { mac = configuredMac; } } if (instanceID > 0) { rep = true; mac[3] += instanceID; mac[4] += instanceID*0x44; mac[5] += instanceID*0x10; } if (rep) { mac[0] &= 0xFC; // ensure the MAC isn't a broadcast MAC header.MacAddr = mac; header.UpdateChecksum(); } firmware.UpdateChecksums(); } // Loads ROM data without parsing it. Works for GBA and NDS ROMs. bool EmuInstance::loadROMData(const QStringList& filepath, std::unique_ptr& filedata, u32& filelen, string& basepath, string& romname) noexcept { if (filepath.empty()) return false; if (int num = filepath.count(); num == 1) { // regular file std::string filename = filepath.at(0).toStdString(); Platform::FileHandle* f = Platform::OpenFile(filename, FileMode::Read); if (!f) return false; long len = Platform::FileLength(f); if (len > 0x40000000) { Platform::CloseFile(f); return false; } Platform::FileRewind(f); filedata = make_unique(len); size_t nread = Platform::FileRead(filedata.get(), (size_t)len, 1, f); Platform::CloseFile(f); if (nread != 1) { filedata = nullptr; return false; } filelen = (u32)len; if (filename.length() > 4 && filename.substr(filename.length() - 4) == ".zst") { filelen = decompressROM(filedata.get(), len, filedata); if (filelen > 0) { filename = filename.substr(0, filename.length() - 4); } else { filedata = nullptr; filelen = 0; basepath = ""; romname = ""; return false; } } int pos = lastSep(filename); if(pos != -1) basepath = filename.substr(0, pos); romname = filename.substr(pos+1); return true; } #ifdef ARCHIVE_SUPPORT_ENABLED else if (num == 2) { // file inside archive s32 lenread = Archive::ExtractFileFromArchive(filepath.at(0), filepath.at(1), filedata, &filelen); if (lenread < 0) return false; if (!filedata) return false; if (lenread != filelen) { filedata = nullptr; return false; } std::string std_archivepath = filepath.at(0).toStdString(); basepath = std_archivepath.substr(0, lastSep(std_archivepath)); std::string std_romname = filepath.at(1).toStdString(); romname = std_romname.substr(lastSep(std_romname)+1); return true; } #endif else return false; } QString EmuInstance::getSavErrorString(std::string& filepath, bool gba) { std::string console = gba ? "GBA" : "DS"; std::string err1 = "Unable to write to "; std::string err2 = " save.\nPlease check file/folder write permissions.\n\nAttempted to Access:\n"; err1 += console + err2 + filepath; return QString::fromStdString(err1); } bool EmuInstance::loadROM(QStringList filepath, bool reset, QString& errorstr) { unique_ptr filedata = nullptr; u32 filelen; std::string basepath; std::string romname; if (!loadROMData(filepath, filedata, filelen, basepath, romname)) { errorstr = "Failed to load the DS ROM."; return false; } ndsSave = nullptr; baseROMDir = basepath; baseROMName = romname; baseAssetName = romname.substr(0, romname.rfind('.')); u32 savelen = 0; std::unique_ptr savedata = nullptr; std::string savname = getAssetPath(false, localCfg.GetString("SaveFilePath"), ".sav"); std::string origsav = savname; savname += instanceFileSuffix(); FileHandle* sav = Platform::OpenFile(savname, FileMode::Read); if (!sav) { if (!Platform::CheckFileWritable(origsav)) { errorstr = getSavErrorString(origsav, false); return false; } sav = Platform::OpenFile(origsav, FileMode::Read); } else if (!Platform::CheckFileWritable(savname)) { errorstr = getSavErrorString(savname, false); return false; } if (sav) { savelen = (u32)Platform::FileLength(sav); FileRewind(sav); savedata = std::make_unique(savelen); FileRead(savedata.get(), savelen, 1, sav); CloseFile(sav); } NDSCart::NDSCartArgs cartargs { // Don't load the SD card itself yet, because we don't know if // the ROM is homebrew or not. // So this is the card we *would* load if the ROM were homebrew. .SDCard = getSDCardArgs("DLDI"), .SRAM = std::move(savedata), .SRAMLength = savelen, }; auto cart = NDSCart::ParseROM(std::move(filedata), filelen, this, std::move(cartargs)); if (!cart) { // If we couldn't parse the ROM... errorstr = "Failed to load the DS ROM."; return false; } if (reset) { nextCart = std::move(cart); changeCart = true; if (!updateConsole()) { errorstr = "Failed to load the DS ROM."; return false; } initFirmwareSaveManager(); nds->Reset(); if (globalCfg.GetBool("Emu.DirectBoot") || nds->NeedsDirectBoot()) { // If direct boot is enabled or forced... nds->SetupDirectBoot(romname); } setBatteryLevels(); setDateTime(); } else { if (emuIsActive()) { nds->SetNDSCart(std::move(cart)); loadCheats(); } else { nextCart = std::move(cart); changeCart = true; } } cartType = 0; ndsSave = std::make_unique(savname); return true; // success } void EmuInstance::ejectCart() { ndsSave = nullptr; if (emuIsActive()) { nds->EjectCart(); unloadCheats(); } else { nextCart = nullptr; changeCart = true; } cartType = -1; baseROMDir = ""; baseROMName = ""; baseAssetName = ""; } bool EmuInstance::cartInserted() { return cartType != -1; } QString EmuInstance::cartLabel() { if (cartType == -1) return "(none)"; QString ret = QString::fromStdString(baseROMName); int maxlen = 32; if (ret.length() > maxlen) ret = ret.left(maxlen-6) + "..." + ret.right(3); return ret; } bool EmuInstance::loadGBAROM(QStringList filepath, QString& errorstr) { if (consoleType == 1) { errorstr = "The DSi doesn't have a GBA slot."; return false; } unique_ptr filedata = nullptr; u32 filelen; std::string basepath; std::string romname; if (!loadROMData(filepath, filedata, filelen, basepath, romname)) { errorstr = "Failed to load the GBA ROM."; return false; } gbaSave = nullptr; baseGBAROMDir = basepath; baseGBAROMName = romname; baseGBAAssetName = romname.substr(0, romname.rfind('.')); u32 savelen = 0; std::unique_ptr savedata = nullptr; std::string savname = getAssetPath(true, localCfg.GetString("SaveFilePath"), ".sav"); std::string origsav = savname; savname += instanceFileSuffix(); FileHandle* sav = Platform::OpenFile(savname, FileMode::Read); if (!sav) { if (!Platform::CheckFileWritable(origsav)) { errorstr = getSavErrorString(origsav, true); return false; } sav = Platform::OpenFile(origsav, FileMode::Read); } else if (!Platform::CheckFileWritable(savname)) { errorstr = getSavErrorString(savname, true); return false; } if (sav) { savelen = (u32)FileLength(sav); if (savelen > 0) { FileRewind(sav); savedata = std::make_unique(savelen); FileRead(savedata.get(), savelen, 1, sav); } CloseFile(sav); } auto cart = GBACart::ParseROM(std::move(filedata), filelen, std::move(savedata), savelen, this); if (!cart) { errorstr = "Failed to load the GBA ROM."; return false; } gbaCartType = 0; if (emuIsActive()) { nds->SetGBACart(std::move(cart)); gbaSave = std::make_unique(savname); } else { nextGBACart = std::move(cart); changeGBACart = true; } return true; } void EmuInstance::loadGBAAddon(int type, QString& errorstr) { if (consoleType == 1) return; auto cart = GBACart::LoadAddon(type, this); if (!cart) { errorstr = "Failed to load the GBA addon."; return; } if (emuIsActive()) { nds->SetGBACart(std::move(cart)); } else { nextGBACart = std::move(cart); changeGBACart = true; } gbaSave = nullptr; gbaCartType = type; baseGBAROMDir = ""; baseGBAROMName = ""; baseGBAAssetName = ""; } void EmuInstance::ejectGBACart() { gbaSave = nullptr; if (emuIsActive()) { nds->EjectGBACart(); } else { nextGBACart = nullptr; changeGBACart = true; } gbaCartType = -1; baseGBAROMDir = ""; baseGBAROMName = ""; baseGBAAssetName = ""; } bool EmuInstance::gbaCartInserted() { return gbaCartType != -1; } QString EmuInstance::gbaAddonName(int addon) { switch (addon) { case GBAAddon_RumblePak: return "Rumble Pak"; case GBAAddon_RAMExpansion: return "Memory expansion"; case GBAAddon_MotionPak: return "Motion Pak"; } return "???"; } QString EmuInstance::gbaCartLabel() { if (consoleType == 1) return "none (DSi)"; if (gbaCartType == 0) { QString ret = QString::fromStdString(baseGBAROMName); int maxlen = 32; if (ret.length() > maxlen) ret = ret.left(maxlen-6) + "..." + ret.right(3); return ret; } else if (gbaCartType != -1) { return gbaAddonName(gbaCartType); } return "(none)"; } void EmuInstance::romIcon(const u8 (&data)[512], const u16 (&palette)[16], u32 (&iconRef)[32*32]) { u32 paletteRGBA[16]; for (int i = 0; i < 16; i++) { u8 r = ((palette[i] >> 0) & 0x1F) * 255 / 31; u8 g = ((palette[i] >> 5) & 0x1F) * 255 / 31; u8 b = ((palette[i] >> 10) & 0x1F) * 255 / 31; u8 a = i ? 255 : 0; paletteRGBA[i] = r | (g << 8) | (b << 16) | (a << 24); } int count = 0; for (int ytile = 0; ytile < 4; ytile++) { for (int xtile = 0; xtile < 4; xtile++) { for (int ypixel = 0; ypixel < 8; ypixel++) { for (int xpixel = 0; xpixel < 8; xpixel++) { u8 pal_index = count % 2 ? data[count/2] >> 4 : data[count/2] & 0x0F; iconRef[ytile*256 + ypixel*32 + xtile*8 + xpixel] = paletteRGBA[pal_index]; count++; } } } } } #define SEQ_FLIPV(i) ((i & 0b1000000000000000) >> 15) #define SEQ_FLIPH(i) ((i & 0b0100000000000000) >> 14) #define SEQ_PAL(i) ((i & 0b0011100000000000) >> 11) #define SEQ_BMP(i) ((i & 0b0000011100000000) >> 8) #define SEQ_DUR(i) ((i & 0b0000000011111111) >> 0) void EmuInstance::animatedROMIcon(const u8 (&data)[8][512], const u16 (&palette)[8][16], const u16 (&sequence)[64], u32 (&animatedIconRef)[64][32*32], std::vector &animatedSequenceRef) { for (int i = 0; i < 64; i++) { if (!sequence[i]) break; romIcon(data[SEQ_BMP(sequence[i])], palette[SEQ_PAL(sequence[i])], animatedIconRef[i]); u32* frame = animatedIconRef[i]; if (SEQ_FLIPH(sequence[i])) { for (int x = 0; x < 32; x++) { for (int y = 0; y < 32/2; y++) { std::swap(frame[x * 32 + y], frame[x * 32 + (32 - 1 - y)]); } } } if (SEQ_FLIPV(sequence[i])) { for (int x = 0; x < 32/2; x++) { for (int y = 0; y < 32; y++) { std::swap(frame[x * 32 + y], frame[(32 - 1 - x) * 32 + y]); } } } for (int j = 0; j < SEQ_DUR(sequence[i]); j++) animatedSequenceRef.push_back(i); } }