diff --git a/Common/System/Request.cpp b/Common/System/Request.cpp index a37ffebed1..9b3a0ce080 100644 --- a/Common/System/Request.cpp +++ b/Common/System/Request.cpp @@ -156,6 +156,49 @@ void RequestManager::Clear() { callbackMap_.clear(); } +static std::mutex g_textInputLock; +static bool g_textInputWanted[(size_t)TextInputUser::COUNT]; + +static void SendUIEvent(UIEventNotification notification) { + g_requestManager.MakeSystemRequest(SystemRequestType::NOTIFY_UI_EVENT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)notification, 0); +} + +void System_SetTextInputWanted(TextInputUser user, bool wanted) { + bool anyWanted = false; + { + std::lock_guard guard(g_textInputLock); + g_textInputWanted[(size_t)user] = wanted; + for (bool w : g_textInputWanted) { + anyWanted = anyWanted || w; + } + } + // GOTFOCUS always goes through, as platforms may need it to show a keyboard again. + if (wanted) { + SendUIEvent(UIEventNotification::TEXT_GOTFOCUS); + } else if (!anyWanted) { + SendUIEvent(UIEventNotification::TEXT_LOSTFOCUS); + } +} + +void System_NotifyUIEvent(UIEventNotification notification) { + switch (notification) { + case UIEventNotification::TEXT_GOTFOCUS: + System_SetTextInputWanted(TextInputUser::UI, true); + break; + case UIEventNotification::TEXT_LOSTFOCUS: + System_SetTextInputWanted(TextInputUser::UI, false); + break; + case UIEventNotification::POPUP_CLOSED: + // Platforms may hide an on-screen keyboard on this, but text input itself goes through the above. + System_SetTextInputWanted(TextInputUser::UI, false); + SendUIEvent(notification); + break; + default: + SendUIEvent(notification); + break; + } +} + void System_CreateGameShortcut(const Path &path, std::string_view title) { g_requestManager.MakeSystemRequest(SystemRequestType::CREATE_GAME_SHORTCUT, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), title, 0); } diff --git a/Common/System/Request.h b/Common/System/Request.h index aec7d1d4b7..cbcd742fbf 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -182,9 +182,17 @@ inline void System_ShareFile(const Path &path, std::string_view mimeType = "") { g_requestManager.MakeSystemRequest(SystemRequestType::SHARE_FILE, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), mimeType, 0); } -inline void System_NotifyUIEvent(UIEventNotification notification) { - g_requestManager.MakeSystemRequest(SystemRequestType::NOTIFY_UI_EVENT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)notification, 0); -} +// TEXT_GOTFOCUS/TEXT_LOSTFOCUS (and POPUP_CLOSED) sent through here count as the regular UI's use of text input. +void System_NotifyUIEvent(UIEventNotification notification); + +// Several users can want text input at the same time (for example type-to-search under the imgui debugger). +// The platform only gets TEXT_LOSTFOCUS once none of them does. +enum class TextInputUser { + UI, + ImDebugger, + COUNT, +}; +void System_SetTextInputWanted(TextInputUser user, bool wanted); inline void System_SetKeepScreenBright(bool keepScreenBright) { g_requestManager.MakeSystemRequest(SystemRequestType::SET_KEEP_SCREEN_BRIGHT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)keepScreenBright); diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 44801d626a..827af1d693 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -151,14 +151,11 @@ static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t & } // Check for boolean/inverse-boolean parameter - these don't take a separate param value. - if (param.type == CmdParamType::Bool && equals(arg, std::string("--") + param.longName)) { + // SetValue() does the inverting, so both get "true". + if ((param.type == CmdParamType::Bool || param.type == CmdParamType::BoolInverse) && equals(arg, std::string("--") + param.longName)) { ParseParamResult result = SetValue(options, param, "true"); i++; // Skip --param return result; - } else if (param.type == CmdParamType::BoolInverse && equals(arg, std::string("--") + param.longName)) { - ParseParamResult result = SetValue(options, param, "false"); - i++; // Skip --param - return result; } // Check for long form with space: --param value @@ -202,6 +199,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode", CmdLineMode::Headless}, {POFF(bench), CmdParamType::Bool, "bench", 'b', "Enable benchmark mode", CmdLineMode::Headless}, {POFF(oldAtrac), CmdParamType::Bool, "old-atrac", '\0', "Use old ATRAC decoder"}, + {POFF(memStickInserted), CmdParamType::BoolInverse, "no-memstick", '\0', "Run with the memory stick removed"}, {POFF(log), CmdParamType::String, "log", '\0', "Output log to FILE", CmdLineMode::Application}, {POFF(enableLogging), CmdParamType::Bool, "log", '\0', "Full log output, not just emulated printfs", CmdLineMode::Headless}, {POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless}, @@ -554,6 +552,10 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.bAutoRun = false; g_Config.bSaveSettings = false; } + if (memStickInserted.has_value()) { + g_Config.bMemStickInserted = memStickInserted.value(); + g_Config.DoNotSaveSetting(&g_Config.bMemStickInserted); + } if (escapeExit.has_value()) { g_Config.bPauseExitsEmulator = escapeExit.value(); } diff --git a/Core/CmdLine.h b/Core/CmdLine.h index e0b7e03148..18e882449e 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -43,6 +43,8 @@ struct CommandLineOptions { std::optional startScreen; std::optional escapeExit; + // Run as if no memory stick were inserted. + std::optional memStickInserted; std::optional pauseMenuExit; // Enables the WebSocket debugger on startup, on this port (0 = pick automatically). diff --git a/Core/Dialog/PSPSaveDialog.cpp b/Core/Dialog/PSPSaveDialog.cpp index b754b27856..53fca2b2a1 100755 --- a/Core/Dialog/PSPSaveDialog.cpp +++ b/Core/Dialog/PSPSaveDialog.cpp @@ -1114,10 +1114,48 @@ void PSPSaveDialog::ExecuteIOAction() { ioThreadStatus = SAVEIO_READY; } +// What a mode that shows no dialog returns without a memory stick, or 0 for one not tested on +// hardware. They all start fine and fail when they run (pspautotests utility/savedata/nomemstick). +static u32 NoMemstickResult(SceUtilitySavedataType mode) { + switch (mode) { + case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD: + return SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_MS; + case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE: + return SCE_UTILITY_SAVEDATA_ERROR_SAVE_NO_MS; + case SCE_UTILITY_SAVEDATA_TYPE_AUTODELETE: + return SCE_UTILITY_SAVEDATA_ERROR_DELETE_NO_MS; + case SCE_UTILITY_SAVEDATA_TYPE_SIZES: + return SCE_UTILITY_SAVEDATA_ERROR_SIZES_NO_MS; + case SCE_UTILITY_SAVEDATA_TYPE_LIST: + case SCE_UTILITY_SAVEDATA_TYPE_FILES: + case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE: + case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA: + case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE: + case SCE_UTILITY_SAVEDATA_TYPE_READDATA: + case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE: + case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA: + case SCE_UTILITY_SAVEDATA_TYPE_ERASESECURE: + case SCE_UTILITY_SAVEDATA_TYPE_ERASE: + case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA: + case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE: + return SCE_UTILITY_SAVEDATA_ERROR_RW_NO_MEMSTICK; + default: + return 0; + } +} + void PSPSaveDialog::ExecuteNotVisibleIOAction() { auto &result = ioRequest_.common.result; SceUtilitySavedataType utilityMode = (SceUtilitySavedataType)(u32)ioRequest_.mode; + if (MemoryStick_State() != PSP_MEMORYSTICK_STATE_INSERTED) { + const u32 noMemstickResult = NoMemstickResult(utilityMode); + if (noMemstickResult != 0) { + result = noMemstickResult; + return; + } + } + switch (utilityMode) { case SCE_UTILITY_SAVEDATA_TYPE_LOAD: // Only load and exit case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD: @@ -1138,16 +1176,10 @@ void PSPSaveDialog::ExecuteNotVisibleIOAction() { result = ioParam_.GetFilesList(&ioRequest_, requestAddr, ioListSaveDirName_); break; case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE: - { - bool sizeResult = ioParam_.GetSize(&ioRequest_); - // TODO: According to JPCSP, should test/verify this part but seems edge casey. - if (MemoryStick_State() != PSP_MEMORYSTICK_STATE_INSERTED) { - result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_MEMSTICK; - } else if (sizeResult) { - result = 0; - } else { - result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA; - } + if (ioParam_.GetSize(&ioRequest_)) { + result = 0; + } else { + result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA; } break; case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA: diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index f89c8edcde..4ce8b4fa44 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -946,10 +946,22 @@ static void __IoSchedSync(FileNode *f, int fd, int usec) { f->hasAsyncResult = false; } +// With the memory stick out, there's no device behind ms0: (pspautotests utility/savedata/nomemstick). +static bool IsRemovedMemstickPath(const char *path) { + if (MemoryStick_State() == PSP_MEMORYSTICK_STATE_INSERTED) { + return false; + } + return startsWithNoCase(path, "ms0:") || startsWithNoCase(path, "fatms0:"); +} + static u32 sceIoGetstat(const char *filename, u32 addr) { // TODO: Improve timing (although this seems normally slow..) int usec = 1000; + if (IsRemovedMemstickPath(filename)) { + return hleDelayResult(hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_NODEV, "memory stick removed"), "io getstat", usec); + } + // A real PSP refuses to stat the root of a volume - io/stat records sceIoGetstat("ms0:/") // coming back as an invalid argument rather than describing the directory. const char *colon = strchr(filename, ':'); @@ -1573,6 +1585,10 @@ static FileNode *__IoOpen(int &error, const char *filename, int flags, int mode) error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND; return nullptr; } + if (IsRemovedMemstickPath(filename)) { + error = SCE_KERNEL_ERROR_NODEV; + return nullptr; + } int access = FILEACCESS_NONE; if (flags & PSP_O_RDONLY) @@ -2549,6 +2565,10 @@ static u32 sceIoDopen(const char *path) { return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND, "nullptr file not found"); } + if (IsRemovedMemstickPath(path)) { + return hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_NODEV, "memory stick removed"); + } + double startTime = time_now_d(); bool listingExists = false; diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index 628d4882bc..6d368c3a95 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -721,10 +721,14 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF Path outFilename = dest / zippedName.substr(info.stripChars); bool isDir = zippedName.empty() || zippedName.back() == '/'; - if (!isDir && zippedName.find('/') != std::string::npos) { - outFilename = dest / zippedName.substr(info.stripChars, zippedName.rfind('/') - info.stripChars); - } else if (!isDir) { - outFilename = dest; + if (!isDir) { + // stripChars can reach past the last slash (save states strip their whole directory). + size_t lastSlash = zippedName.rfind('/'); + if (lastSlash != std::string::npos && lastSlash > (size_t)info.stripChars) { + outFilename = dest / zippedName.substr(info.stripChars, lastSlash - info.stripChars); + } else { + outFilename = dest; + } } Path outPath(outFilename); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index d797a7df55..206fa9e53c 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -108,6 +108,9 @@ static int g_audioFramesPerBuffer = 0; static bool g_textFocusChanged; static bool g_textFocus; +// Set when SDL_StartTextInput fails, so we generate KeyInputFlags::CHAR from key events ourselves. +// SDL 3.4.0 to 3.4.4 refuse text input on Wayland compositors without zwp_text_input_v3 (like WSLg's Weston). +static bool g_textInputFallback; double g_audioStartTime = 0.0; // Window state to be transferred to the main SDL thread. @@ -1016,7 +1019,6 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string g_textFocus = true; g_textFocusChanged = true; break; - case UIEventNotification::POPUP_CLOSED: case UIEventNotification::TEXT_LOSTFOCUS: g_textFocus = false; g_textFocusChanged = true; @@ -1523,6 +1525,17 @@ static void ProcessSDLEvent(SDL_Window *window, const SDL_Event &event, InputSta #endif case SDL_EVENT_KEY_DOWN: { + if (g_textInputFallback && g_textFocus && !(event.key.mod & (SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI))) { + // Layout-aware, with shift applied. Repeats are wanted here, like real text input. + SDL_Keycode c = SDL_GetKeyFromScancode(event.key.scancode, event.key.mod, false); + if (c >= 0x20 && c != 0x7F && !(c & SDLK_SCANCODE_MASK)) { + KeyInput key; + key.flags = KeyInputFlags::CHAR; + key.unicodeChar = c; + key.deviceId = DEVICE_ID_KEYBOARD; + NativeKey(key); + } + } if (event.key.repeat > 0) { break; } @@ -1848,7 +1861,10 @@ void UpdateTextFocus(SDL_Window *window) { if (g_textFocusChanged) { DEBUG_LOG(Log::System, "Updating text focus: %d", g_textFocus); if (g_textFocus) { - SDL_StartTextInput(window); + if (!SDL_StartTextInput(window) && !g_textInputFallback) { + WARN_LOG(Log::System, "SDL_StartTextInput failed (%s), generating text from key events instead. Update SDL, or set SDL_VIDEO_DRIVER=x11, for full text input.", SDL_GetError()); + g_textInputFallback = true; + } } else { SDL_StopTextInput(window); } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index f841e9e236..d7a2155dd9 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -212,7 +212,7 @@ static std::vector g_inputEventQueue; static std::unique_ptr imDebugger_; static ImCommand imCmd_{}; // needed to buffer commands in case imgui wasn't created yet. static bool imguiInited_ = false; -static bool lastImguiEnabled_ = false; +static bool imguiWantTextInput_ = false; static ImGuiContext *ctx_ = nullptr; class GlobalListener : public ControlListener { @@ -297,20 +297,7 @@ static void RunAchievementsOverrideUpdate(std::function func) { } void runImDebugger(Draw::DrawContext *draw) { - bool lastImguiEnabled_ = false; // temp - if (lastImguiEnabled_ && g_Config.bShowImDebugger) { -#if !defined(MOBILE_DEVICE) - // On mobile devices (specifically iOS) we don't want to pop the keyboard - // on activating imgui. Instead, we should do it when a text edit field in imgui gets focus, - // although we'll still have ugly overlap problems. - System_NotifyUIEvent(UIEventNotification::TEXT_GOTFOCUS); -#endif - VERBOSE_LOG(Log::System, "activating keyboard"); - } else if (lastImguiEnabled_ && !g_Config.bShowImDebugger) { - System_NotifyUIEvent(UIEventNotification::TEXT_LOSTFOCUS); - VERBOSE_LOG(Log::System, "deactivating keyboard"); - } - lastImguiEnabled_ = g_Config.bShowImDebugger; + bool wantTextInput = false; if (g_Config.bShowImDebugger) { if (!imguiInited_) { // TODO: Do this only on demand. @@ -383,10 +370,20 @@ void runImDebugger(Draw::DrawContext *draw) { // Convert to drawlists. ImGui::Render(); + wantTextInput = io.WantTextInput; } else { uiContext->SetOverrideScreenFrame(nullptr); SetOverrideScreenFrame(nullptr); } + + // Enable text input (needed for KeyInputFlags::CHAR events) only while an imgui text field is active. + // On mobile devices (specifically iOS) this would pop the keyboard, with ugly overlap problems. +#if !defined(MOBILE_DEVICE) + if (wantTextInput != imguiWantTextInput_) { + System_SetTextInputWanted(TextInputUser::ImDebugger, wantTextInput); + imguiWantTextInput_ = wantTextInput; + } +#endif } void renderImDebugger(Draw::DrawContext *draw) { @@ -1569,9 +1566,11 @@ bool NativeKey(const KeyInput &key) { // Filtering, detailed rules needed for good imgui behavior without having to ask the screen about what to do. InputMode inputMode = g_screenManager->PassInputToMapper(); + bool imguiHasKeyboard = false; if (g_Config.bShowImDebugger && imguiInited_) { if (ImGui::GetIO().WantCaptureKeyboard) { inputMode &= ~InputMode::Keyboard; + imguiHasKeyboard = true; } if (ImGui::GetIO().WantCaptureMouse) { inputMode &= ~InputMode::Mouse; @@ -1579,21 +1578,21 @@ bool NativeKey(const KeyInput &key) { } bool passKeyThrough = false; - if (inputMode != InputMode::None) { - if ((inputMode & InputMode::ImDebuggerToggle) && (key.flags & (KeyInputFlags::UP | KeyInputFlags::DOWN))) { - InputMapping mapping(key.deviceId, key.keyCode); - std::vector pspButtons; - bool mappingFound = KeyMap::InputMappingToPspButton(mapping, &pspButtons); - if (mappingFound) { - for (auto b : pspButtons) { - if (b == VIRTKEY_TOGGLE_DEBUGGER || b == VIRTKEY_PAUSE) { - // TRUE - passKeyThrough = true; - } + // The debugger toggle also goes through while imgui has the keyboard, so it can always be closed again. + if (((inputMode & InputMode::ImDebuggerToggle) || imguiHasKeyboard) && (key.flags & (KeyInputFlags::UP | KeyInputFlags::DOWN))) { + InputMapping mapping(key.deviceId, key.keyCode); + std::vector pspButtons; + bool mappingFound = KeyMap::InputMappingToPspButton(mapping, &pspButtons); + if (mappingFound) { + for (auto b : pspButtons) { + if (b == VIRTKEY_TOGGLE_DEBUGGER || (b == VIRTKEY_PAUSE && (inputMode & InputMode::ImDebuggerToggle))) { + passKeyThrough = true; } } } + } + if (inputMode != InputMode::None) { if (key.deviceId == DEVICE_ID_MOUSE) { if (inputMode & InputMode::Mouse) { passKeyThrough = true; diff --git a/Windows/MainWindow.cpp b/Windows/MainWindow.cpp index 3cf7f60693..073ec990f9 100644 --- a/Windows/MainWindow.cpp +++ b/Windows/MainWindow.cpp @@ -165,6 +165,13 @@ namespace MainWindow { static bool hasFocus = true; static bool g_keepScreenBright = false; + // The simulated double-click that toggles fullscreen. Only two clicks on the running game count, so + // focus loss, a click outside the client area, or a UI state change in between resets it. + static double lastMouseDownTime = 0.0; + static float lastMouseDownX = -1.0f; + static float lastMouseDownY = -1.0f; + static GlobalUIState lastMouseDownUIState = UISTATE_MENU; + static bool disasmMapLoadPending = false; static bool memoryMapLoadPending = false; static bool g_wasMinimized = false; @@ -825,6 +832,7 @@ namespace MainWindow { trapMouse = true; } if (wParam == WA_INACTIVE) { + lastMouseDownTime = 0.0; System_PostUIMessage(UIMessage::LOST_FOCUS); WindowsRawInput::LoseFocus(); g_InputManager.LoseFocus(); @@ -1208,27 +1216,31 @@ namespace MainWindow { SetCapture(hWnd); // Simulate doubleclick, doesn't work with RawInput enabled - static double lastMouseDownTime; - static float lastMouseDownX = -1.0f; - static float lastMouseDownY = -1.0f; const double now = time_now_d(); - if ((now - lastMouseDownTime) < 0.001 * GetDoubleClickTime()) { + const GlobalUIState uiState = GetUIState(); + if ((now - lastMouseDownTime) < 0.001 * GetDoubleClickTime() && uiState == lastMouseDownUIState) { const float dx = lastMouseDownX - x; const float dy = lastMouseDownY - y; const float distSq = dx * dx + dy * dy; - if (distSq < 3.0f*3.0f && !g_Config.bShowTouchControls && !g_Config.bShowImDebugger && !g_Config.bMouseControl && GetUIState() == UISTATE_INGAME && g_Config.bFullscreenOnDoubleclick) { + if (distSq < 3.0f*3.0f && !g_Config.bShowTouchControls && !g_Config.bShowImDebugger && !g_Config.bMouseControl && uiState == UISTATE_INGAME && g_Config.bFullscreenOnDoubleclick) { g_Config.bFullScreen = !g_Config.bFullScreen; SendApplyFullscreenState(); } lastMouseDownTime = 0.0; } else { lastMouseDownTime = now; + lastMouseDownUIState = uiState; } lastMouseDownX = x; lastMouseDownY = y; } break; + case WM_NCLBUTTONDOWN: + // The title bar or the window's buttons, so not part of a double-click on the game. + lastMouseDownTime = 0.0; + return DefWindowProc(hWnd, message, wParam, lParam); + case WM_MOUSEMOVE: if (!touchHandler.hasTouch() || (GetMessageExtraInfo() & MOUSEEVENTF_MASK_PLUS_PENTOUCH) != MOUSEEVENTF_FROMTOUCH_NOPEN) diff --git a/pspautotests b/pspautotests index e7bc66349a..3066d2f076 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit e7bc66349a521e8590d4fcc6dfc42894a891e962 +Subproject commit 3066d2f076e00a7d8dca2975bc898ca486e597bc diff --git a/test.py b/test.py index a5b5ff31cf..9b90b6acce 100755 --- a/test.py +++ b/test.py @@ -512,6 +512,7 @@ tests_good = [ "utility/savedata/filelist", "utility/savedata/getsize", "utility/savedata/makedata", + "utility/savedata/nomemstick", "utility/systemparam/systemparam", "umd/api/api", "umd/callbacks/umd", @@ -656,6 +657,13 @@ tests_next = [ ] +# Tests that need extra headless flags, so they run in a headless process of their own. The flags +# have to match how the .expected was recorded. +test_flags = { + "utility/savedata/nomemstick": ["--no-memstick"], +} + + # These are the tests we ignore (not important, or impossible to run) tests_ignored = [ "kirk/kirk", @@ -773,7 +781,11 @@ def main(): if skipped: print("Skipping %d known failures on %s: %s" % (len(skipped), skip_arch, ", ".join(skipped))) - returncode = run_tests(tests, args) + plain = [t for t in tests if t not in test_flags] + returncode = run_tests(plain, args) + for test in tests: + if test in test_flags: + returncode = run_tests([test], args + test_flags[test]) or returncode if teamcity: return 0 return returncode diff --git a/unittest/TestZipSlip.cpp b/unittest/TestZipSlip.cpp index 40a6da70d8..3f3621a787 100644 --- a/unittest/TestZipSlip.cpp +++ b/unittest/TestZipSlip.cpp @@ -80,6 +80,49 @@ static bool TestZipSlipExtraction() { return true; } +// Save state zips are detected with stripChars past the last slash, since the whole +// directory is stripped. The files must land directly in the destination. +static bool TestSaveStateZipExtraction() { + // Absolute, since CreateFullPath does nothing with relative paths. + Path tempRoot = File::GetCurDirectory() / "unittest_zip_savestate_test"; + File::DeleteDirRecursively(tempRoot); + EXPECT_TRUE(File::CreateDir(tempRoot)); + + Path destDir = tempRoot / "dest"; + EXPECT_TRUE(File::CreateDir(destDir)); + + Path zipPath = tempRoot / "states.zip"; + int errorp = 0; + zip_t *z = zip_open(zipPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorp); + EXPECT_TRUE(z != nullptr); + static const char *const contents = "data"; + EXPECT_TRUE(zip_dir_add(z, "SAVE/", ZIP_FL_ENC_UTF_8) >= 0); + EXPECT_TRUE(zip_dir_add(z, "SAVE/PPSSPP_STATE/", ZIP_FL_ENC_UTF_8) >= 0); + for (const char *name : { "SAVE/PPSSPP_STATE/UCES01401_1.00_1.jpg", "SAVE/PPSSPP_STATE/UCES01401_1.00_1.ppst" }) { + zip_source_t *source = zip_source_buffer(z, contents, strlen(contents), 0); + EXPECT_TRUE(source != nullptr); + EXPECT_TRUE(zip_file_add(z, name, source, ZIP_FL_ENC_UTF_8) >= 0); + } + EXPECT_TRUE(zip_close(z) == 0); + + z = zip_open(zipPath.c_str(), 0, &errorp); + EXPECT_TRUE(z != nullptr); + + ZipFileInfo info; + DetectZipFileContents(z, &info); + EXPECT_EQ_INT((int)info.contents, (int)ZipFileContents::SAVE_STATES); + + GameManager manager; + EXPECT_TRUE(manager.ExtractZipContents(z, destDir, info, true)); + zip_close(z); + + EXPECT_TRUE(File::Exists(destDir / "UCES01401_1.00_1.jpg") && !File::IsDirectory(destDir / "UCES01401_1.00_1.jpg")); + EXPECT_TRUE(File::Exists(destDir / "UCES01401_1.00_1.ppst") && !File::IsDirectory(destDir / "UCES01401_1.00_1.ppst")); + + File::DeleteDirRecursively(tempRoot); + return true; +} + static bool TestIsSafePathComponent() { EXPECT_TRUE(IsSafePathComponent("ULUS10041")); EXPECT_TRUE(IsSafePathComponent("NPJH-50465")); @@ -106,5 +149,7 @@ bool TestZipSlip() { return false; if (!TestZipSlipExtraction()) return false; + if (!TestSaveStateZipExtraction()) + return false; return true; } diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 3a55f8191a..51d92272a3 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3029,6 +3029,21 @@ bool TestCmdLine() { EXPECT_EQ_INT(options.timeoutWall.value_or(0), 4); EXPECT_EQ_INT(options.timeoutEmulated.value_or(0), 5); } + // Inverse booleans: --windowed turns fullscreen off, and --no-memstick removes the stick. + { + const char *argv[] = { + "ppsspp", + "--windowed", + "--no-memstick", + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + options.Parse(argc, argv, CmdLineMode::Application); + EXPECT_TRUE(options.fullscreen.has_value()); + EXPECT_FALSE(options.fullscreen.value_or(true)); + EXPECT_TRUE(options.memStickInserted.has_value()); + EXPECT_FALSE(options.memStickInserted.value_or(true)); + } // Test GL version override { const char *argv[] = {