From 97f16be342bcf4dc2980fe1127f61114f9adea03 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 13:06:28 +0300 Subject: [PATCH] fix(savedata): keep progress dialogs running until closed Opening a save progress dialog immediately reported FINISHED. Skyrim waited for RUNNING before writing SAVEDATA.DAT and never resumed. Keep progress-bar mode running through polling and progress updates until Close, retaining the existing automatic responses for other dialog modes. Add lifecycle regression coverage for repeated polling, completion, reopening, result metadata and ordinary selection dialogs. The test failed on the previous implementation and passes with the fix on Fedora 44 Release. Correcting the live dialog state recovered a blocked Skyrim save without restarting its session. Written with AI assistance. --- .../CMakeLists.txt | 15 ++++ .../libSceSaveDataDialog.native/Export.cpp | 3 +- .../SaveDataDialog.hpp | 3 + core/libs/tests/GuestSaveDataDialog.cpp | 78 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt create mode 100644 core/libs/tests/GuestSaveDataDialog.cpp diff --git a/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt b/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt new file mode 100644 index 000000000..ddbc456db --- /dev/null +++ b/core/libs/prx/libSceSaveDataDialog.native/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceSaveDataDialog.native SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSaveDataDialog.native PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSaveDataDialog.native) + +if(BUILD_TESTING) + add_test_executable(guest_savedata_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataDialog.cpp) + target_include_directories(guest_savedata_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_dialog_tests PRIVATE libSceSaveDataDialog.native) + configure_windows_unwind(guest_savedata_dialog_tests) + add_test(NAME guest_savedata_dialog COMMAND guest_savedata_dialog_tests) + if(WIN32) + set_tests_properties(guest_savedata_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index 0835cdff4..3c834f7d9 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -69,7 +69,8 @@ int APS5_VABI sceSaveDataDialogOpen(const void* param) { } } } - g_status = SAVE_DATA_DIALOG_STATUS_FINISHED; + g_status = g_mode == SAVE_DATA_DIALOG_MODE_PROGRESS_BAR + ? SAVE_DATA_DIALOG_STATUS_RUNNING : SAVE_DATA_DIALOG_STATUS_FINISHED; return SAVE_DATA_DIALOG_OK; } diff --git a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp index 907db674a..3259b8d48 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp +++ b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp @@ -12,8 +12,11 @@ constexpr int SAVE_DATA_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000D); constexpr int SAVE_DATA_DIALOG_STATUS_NONE = 0; constexpr int SAVE_DATA_DIALOG_STATUS_INITIALIZED = 1; +constexpr int SAVE_DATA_DIALOG_STATUS_RUNNING = 2; constexpr int SAVE_DATA_DIALOG_STATUS_FINISHED = 3; +constexpr int SAVE_DATA_DIALOG_MODE_PROGRESS_BAR = 5; + constexpr int SAVE_DATA_DIALOG_RESULT_OK = 0; constexpr int SAVE_DATA_DIALOG_BUTTON_ID_OK = 1; diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp new file mode 100644 index 000000000..5d8e06371 --- /dev/null +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -0,0 +1,78 @@ +#include "prx/libSceSaveDataDialog.native/SaveDataDialog.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataDialogInitialize(); +int APS5_VABI sceSaveDataDialogOpen(const void* param); +int APS5_VABI sceSaveDataDialogGetStatus(); +int APS5_VABI sceSaveDataDialogUpdateStatus(); +int APS5_VABI sceSaveDataDialogIsReadyToDisplay(); +int APS5_VABI sceSaveDataDialogProgressBarInc(int target, std::uint32_t delta); +int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, std::uint32_t rate); +int APS5_VABI sceSaveDataDialogClose(const void* param); +int APS5_VABI sceSaveDataDialogGetResult(void* result); +int APS5_VABI sceSaveDataDialogTerminate(); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static void RequireRunning() { + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_RUNNING, + "save progress must remain running until closed"); + Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_RUNNING, + "polling save progress must not finish the dialog"); + Require(sceSaveDataDialogIsReadyToDisplay() == 1, "save progress must be ready"); +} + +int main() { + Require(sceSaveDataDialogInitialize() == 0, "initialize"); + + SaveDataDirName names[2]{{}, {"SDU1"}}; + SaveDataDialogItems items{}; + items.dir_names = names; + items.dir_names_num = 2; + int context = 42; + SaveDataDialogParam param{}; + param.size = sizeof(param); + param.mode = SAVE_DATA_DIALOG_MODE_PROGRESS_BAR; + param.items = &items; + param.user_data = &context; + + for (int save = 0; save < 2; ++save) { + Require(sceSaveDataDialogOpen(¶m) == 0, "open save progress"); + for (int poll = 0; poll < 8; ++poll) RequireRunning(); + Require(sceSaveDataDialogOpen(¶m) == SAVE_DATA_DIALOG_ERROR_INVALID_STATE, + "opening another dialog must not interrupt an active save"); + Require(sceSaveDataDialogProgressBarSetValue(0, 25) == 0, "set progress"); + Require(sceSaveDataDialogProgressBarInc(0, 25) == 0, "increment progress"); + RequireRunning(); + Require(sceSaveDataDialogProgressBarSetValue(0, 100) == 0, "complete progress"); + RequireRunning(); + Require(sceSaveDataDialogClose(nullptr) == 0, "close save progress"); + Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, + "closing save progress must finish it"); + SaveDataDirName selected{}; + SaveDataDialogResult result{}; + result.dir_name = &selected; + Require(sceSaveDataDialogGetResult(&result) == 0, "read completed result"); + Require(result.mode == param.mode && result.result == SAVE_DATA_DIALOG_RESULT_OK && + result.user_data == &context && std::strcmp(selected.data, "SDU1") == 0, + "save result must preserve its mode, directory and caller context"); + } + + param.mode = 1; + Require(sceSaveDataDialogOpen(¶m) == 0, "open selection dialog"); + Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, + "selection dialog must retain the existing automatic response"); + Require(sceSaveDataDialogTerminate() == 0, "terminate"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 084451064..4fd66b20b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -11,7 +11,7 @@ ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: -- [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) +- [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. - [libSceMsgDialog.native](../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog - [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged