diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 344990ae2bf5..ff34dac7de67 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -36,21 +36,32 @@ constexpr double micros = 1000000.0; constexpr double nanos = 1000000000.0; + #if PPSSPP_PLATFORM(WINDOWS) +constexpr int64_t UNIX_TIME_START = 0x019DB1DED53E8000; //January 1, 1970 (start of Unix epoch) in "ticks" +constexpr double TICKS_PER_SECOND = 10000000; //a tick is 100ns + static LARGE_INTEGER frequency; static double frequencyMult; static LARGE_INTEGER startTime; +static LARGE_INTEGER startFileTime; HANDLE Timer; int SchedulerPeriodMs = 10; INT64 QpcPerSecond; void TimeInit() { + FILETIME ft; + GetSystemTimeAsFileTime(&ft); //returns ticks in UTC + // Copy the low and high parts of FILETIME into a LARGE_INTEGER + startFileTime.LowPart = ft.dwLowDateTime; + startFileTime.HighPart = ft.dwHighDateTime; + QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&startTime); QpcPerSecond = frequency.QuadPart; - frequencyMult = 1.0 / static_cast(frequency.QuadPart); + frequencyMult = 1.0 / frequency.QuadPart; // The timer will be automatically deleted on process destruction. Don't need to CloseHandle. Timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); @@ -85,9 +96,6 @@ double from_time_raw_relative(uint64_t raw_time) { } double time_now_unix_utc() { - const int64_t UNIX_TIME_START = 0x019DB1DED53E8000; //January 1, 1970 (start of Unix epoch) in "ticks" - const double TICKS_PER_SECOND = 10000000; //a tick is 100ns - FILETIME ft; GetSystemTimeAsFileTime(&ft); //returns ticks in UTC // Copy the low and high parts of FILETIME into a LARGE_INTEGER @@ -98,6 +106,15 @@ double time_now_unix_utc() { return (double)(li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND; } +// Adds the timestamp to startTime, and converts to seconds from the unix epoch. +double time_to_unix_utc(double timestamp) { + // Copy the low and high parts of FILETIME into a LARGE_INTEGER + LARGE_INTEGER li; + li.LowPart = startFileTime.LowPart; + li.HighPart = startFileTime.HighPart; + return (double)(li.QuadPart - UNIX_TIME_START + static_cast(timestamp * TICKS_PER_SECOND)) / TICKS_PER_SECOND; +} + void yield() { YieldProcessor(); } @@ -227,6 +244,10 @@ double time_now_unix_utc() { return time_now_raw(); } +double time_to_unix_utc(double t) { + return (double)tv.tv_sec + (double)tv.tv_usec * (1.0 / micros) + t; +} + Instant::Instant() { struct timeval tv; gettimeofday(&tv, nullptr); diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index 84fe433ee5b5..6f1a30a14c46 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -16,6 +16,7 @@ double from_time_raw_relative(uint64_t raw_time); // Seconds, Unix UTC time double time_now_unix_utc(); +double time_to_unix_utc(double timeNowSeconds); // Sleep for milliseconds. Does not necessarily have millisecond granularity, especially on Windows. // Requires a "reason" since sleeping generally should be very sparingly used. This diff --git a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp index ce53edda9df8..2cbab16c991e 100644 --- a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp +++ b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp @@ -248,14 +248,16 @@ static void GenericStreamBuffer(DebuggerRequest &req, std::functionT("Failed to load state. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system."), ""); } } } @@ -451,7 +448,7 @@ int g_screenshotFailures; if (g_Config.sStateLoadUndoGame != gamePrefix) { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Error: load undo state is from a different game")); + callback(Status::FAILURE, sy->T("Error: load undo state is from a different game"), ""); } return false; } @@ -463,7 +460,7 @@ int g_screenshotFailures; } else { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system."), ""); } return false; } @@ -480,7 +477,7 @@ int g_screenshotFailures; Path shot = GenerateSaveSlotPath(gamePrefix, slot, SCREENSHOT_EXTENSION); std::string prefix(gamePrefix); - auto renameCallback = [fn, fnUndo, prefix, slot, callback](Status status, std::string_view message) { + auto renameCallback = [fn, fnUndo, prefix, slot, callback](Status status, std::string_view message, std::string_view metadata) { if (status != Status::FAILURE) { if (g_Config.bEnableStateUndo) { DeleteIfExists(fnUndo); @@ -494,7 +491,7 @@ int g_screenshotFailures; File::Rename(fn.WithExtraExtension(".tmp"), fn); } if (callback) { - callback(status, message); + callback(status, message, ""); } }; // Let's also create a screenshot. @@ -508,7 +505,7 @@ int g_screenshotFailures; } else { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Failed to save state. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to save state. Error in the file system."), ""); } } Rescan(gamePrefix); @@ -678,8 +675,7 @@ int g_screenshotFailures; return oldestSlot; } - std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) { - std::string fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + static std::string GetSaveFileDateAsString(const std::string &fn) { auto iter = g_files.find(fn); if (iter == g_files.end()) { return ""; @@ -706,6 +702,11 @@ int g_screenshotFailures; return buf; } + std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) { + std::string fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + return GetSaveFileDateAsString(fn); + } + std::vector Flush() { std::lock_guard guard(mutex); std::vector copy = g_pendingOperations; @@ -714,7 +715,7 @@ int g_screenshotFailures; return copy; } - bool HandleLoadFailure(bool wasRewinding) { + static bool HandleLoadFailure(bool wasRewinding, std::string *metadata) { if (wasRewinding) { WARN_LOG(Log::SaveState, "HandleLoadFailure - trying a rewind state."); // Okay, first, let's give the next rewind state a shot - maybe we can at least not reset entirely. @@ -722,7 +723,7 @@ int g_screenshotFailures; CChunkFileReader::Error result; do { std::string errorString; - result = rewindStates.Restore(&errorString); + result = rewindStates.Restore(&errorString, metadata); } while (result == CChunkFileReader::ERROR_BROKEN_STATE); if (result == CChunkFileReader::ERROR_NONE) { @@ -813,6 +814,7 @@ int g_screenshotFailures; CChunkFileReader::Error result; Status callbackResult; std::string callbackMessage; + std::string callbackMetadata; std::string title; auto sc = GetI18NCategory(I18NCat::SCREEN); @@ -824,12 +826,14 @@ int g_screenshotFailures; switch (op.type) { case OperationType::Load: - INFO_LOG(Log::SaveState, "Loading state from '%s'", op.filename.c_str()); + INFO_LOG(Log::SaveState, "Loading state from '%s'", op.path.c_str()); // Use the state's latest version as a guess for saveStateInitialGitVersion. - result = CChunkFileReader::Load(op.filename, &saveStateInitialGitVersion, state, &errorString); + result = CChunkFileReader::Load(op.path, &saveStateInitialGitVersion, state, &errorString); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = op.slot != LOAD_UNDO_SLOT ? sc->T("Loaded State") : sc->T("State load undone"); callbackResult = TriggerLoadWarnings(callbackMessage); + callbackMetadata = SaveState::GetSaveFileDateAsString(op.path.GetFilename()); + hasLoadedState = true; Core_ResetException(); @@ -849,18 +853,18 @@ int g_screenshotFailures; #endif g_lastSaveTime = time_now_d(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { - HandleLoadFailure(false); + HandleLoadFailure(false, &callbackMetadata); callbackMessage = std::string(i18nLoadFailure) + ": " + errorString; ERROR_LOG(Log::SaveState, "Load state failure: %s", errorString.c_str()); callbackResult = Status::FAILURE; } else { - callbackMessage = sc->T(errorString.c_str(), i18nLoadFailure); + callbackMessage = sc->T(errorString, i18nLoadFailure); callbackResult = Status::FAILURE; } break; case OperationType::Save: - INFO_LOG(Log::SaveState, "Saving state to '%s'", op.filename.c_str()); + INFO_LOG(Log::SaveState, "Saving state to '%s'", op.path.c_str()); title = g_paramSFO.GetValueString("TITLE"); if (title.empty()) { // Homebrew title @@ -868,7 +872,7 @@ int g_screenshotFailures; std::size_t lslash = title.find_last_of('/'); title = title.substr(lslash + 1); } - result = CChunkFileReader::Save(op.filename, title, PPSSPP_GIT_VERSION, state); + result = CChunkFileReader::Save(op.path, title, PPSSPP_GIT_VERSION, state); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = slot_prefix + std::string(sc->T("Saved State")); callbackResult = Status::SUCCESS; @@ -909,7 +913,7 @@ int g_screenshotFailures; case OperationType::Rewind: INFO_LOG(Log::SaveState, "Rewinding to recent savestate snapshot"); - result = rewindStates.Restore(&errorString); + result = rewindStates.Restore(&errorString, &callbackMetadata); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = sc->T("Loaded State"); callbackResult = Status::SUCCESS; @@ -917,7 +921,7 @@ int g_screenshotFailures; Core_ResetException(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { // Cripes. Good news is, we might have more. Let's try those too, better than a reset. - if (HandleLoadFailure(true)) { + if (HandleLoadFailure(true, &callbackMetadata)) { // Well, we did rewind, even if too much... callbackMessage = sc->T("Loaded State"); callbackResult = Status::SUCCESS; @@ -940,7 +944,7 @@ int g_screenshotFailures; } if (op.callback) { - op.callback(callbackResult, callbackMessage); + op.callback(callbackResult, callbackMessage, callbackMetadata); } } if (operations.size()) { diff --git a/Core/SaveState.h b/Core/SaveState.h index c8400bafe69b..ba5ae44f0b00 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -34,7 +34,7 @@ namespace SaveState { WARNING, SUCCESS, }; - typedef std::function Callback; + typedef std::function Callback; static const char * const SCREENSHOT_EXTENSION = "jpg"; diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index 414886d43603..dc7eaf0c7a53 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -1,4 +1,6 @@ #include "Common/Thread/ThreadUtil.h" +#include "Common/Data/Text/I18n.h" +#include "Common/StringUtils.h" #include "Core/SaveState.h" #include "Core/SaveStateRewind.h" #include "Core/Core.h" @@ -33,16 +35,18 @@ CChunkFileReader::Error StateRingbuffer::Save() { } else err = SaveToRam(buffer_); - if (err == CChunkFileReader::ERROR_NONE) - ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]); - else + if (err == CChunkFileReader::ERROR_NONE) { + ScheduleCompress(&states_[n].stateBuffer, compressBuffer, &bases_[base_]); + states_[n].savedTime = time_now_d(); + } else { states_[n].clear(); + } baseMapping_[n] = base_; return err; } -CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) { +CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std::string *metadata) { std::lock_guard guard(lock_); // No valid states left. @@ -53,9 +57,20 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) { if (states_[n].empty()) return CChunkFileReader::ERROR_BAD_FILE; + auto pa = GetI18NCategory(I18NCat::PAUSE); + static std::vector buffer; - LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]); + LockedDecompress(buffer, states_[n].stateBuffer, bases_[baseMapping_[n]]); CChunkFileReader::Error error = LoadFromRam(buffer, errorString); + *metadata = pa->T("Rewind"); + + if (states_[n].savedTime) { + auto di = GetI18NCategory(I18NCat::DIALOG); + metadata->append(" ("); + metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), static_cast(time_now_d() - states_[n].savedTime))); + metadata->append(")"); + } + rewindLastTime_ = time_now_d(); return error; } @@ -99,16 +114,13 @@ void StateRingbuffer::LockedDecompress(std::vector &result, const std::vecto result.clear(); result.reserve(base.size()); auto basePos = base.begin(); - for (size_t i = 0; i < compressed.size(); ) - { - if (compressed[i] == 0) - { + for (size_t i = 0; i < compressed.size(); ) { + if (compressed[i] == 0) { ++i; int blockSize = std::min(BLOCK_SIZE, (int)(base.size() - result.size())); result.insert(result.end(), basePos, basePos + blockSize); basePos += blockSize; - } else - { + } else { ++i; int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i)); result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize); @@ -167,4 +179,8 @@ void StateRingbuffer::NotifyState() { rewindLastTime_ = time_now_d(); } +double StateRingbuffer::NextStateTimestamp() const { + return rewindLastTime_ + g_Config.iRewindSnapshotInterval; +} + } // namespace SaveState diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h index a20837ddd3b8..16eddec11860 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -28,7 +28,7 @@ class StateRingbuffer { } CChunkFileReader::Error Save(); - CChunkFileReader::Error Restore(std::string *errorString); + CChunkFileReader::Error Restore(std::string *errorString, std::string *metadata); void ScheduleCompress(std::vector *result, const std::vector *state, const std::vector *base); void Compress(std::vector &result, const std::vector &state, const std::vector &base); void LockedDecompress(std::vector &result, const std::vector &compressed, const std::vector &base); @@ -41,6 +41,8 @@ class StateRingbuffer { void Process(); void NotifyState(); + double NextStateTimestamp() const; + private: const int BLOCK_SIZE = 8192; const int REWIND_NUM_STATES = 20; @@ -49,11 +51,19 @@ class StateRingbuffer { typedef std::vector StateBuffer; + struct RewindState { + StateBuffer stateBuffer; + double savedTime; + + bool empty() const { return stateBuffer.empty(); } + void clear() { stateBuffer.clear(); savedTime = 0.0; } + }; + int first_ = 0; int next_ = 0; int size_; - std::vector states_; + std::vector states_; StateBuffer bases_[2]; std::vector baseMapping_; std::mutex lock_; diff --git a/Qt/mainwindow.cpp b/Qt/mainwindow.cpp index cbd9fdafce16..c499de440cbe 100644 --- a/Qt/mainwindow.cpp +++ b/Qt/mainwindow.cpp @@ -153,7 +153,7 @@ void MainWindow::openmsAct() QDesktopServices::openUrl(QUrl(memorystick)); } -static void SaveStateActionFinished(SaveState::Status status, std::string_view message) +static void SaveStateActionFinished(SaveState::Status status, std::string_view message, std::string_view metadata) { // TODO: Improve messaging? if (status == SaveState::Status::FAILURE) diff --git a/Tools/langtool/Cargo.lock b/Tools/langtool/Cargo.lock index bbdfbfe047fa..91d519458e23 100644 --- a/Tools/langtool/Cargo.lock +++ b/Tools/langtool/Cargo.lock @@ -1727,9 +1727,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.11.1", "bytes", diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index b294842044ee..67c25d9dace0 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -74,7 +74,6 @@ using namespace std::placeholders; #include "Core/Screenshot.h" #include "UI/ImDebugger/ImDebugger.h" #include "Core/HLE/__sceAudio.h" -// #include "Core/HLE/proAdhoc.h" #include "Core/HW/Display.h" #include "UI/BackgroundAudio.h" @@ -533,12 +532,6 @@ void EmuScreen::dialogFinished(const Screen *dialog, DialogResult result) { lastImguiEnabled_ = false; } -static void AfterSaveStateAction(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } -} - void EmuScreen::focusChanged(ScreenFocusChange focusChange) { Screen::focusChanged(focusChange); @@ -609,7 +602,7 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { if (newGamePath.GetFileExtension() == ".ppst") { // TODO: Should verify that it's for the correct game.... INFO_LOG(Log::Loader, "New game is a save state - just load it."); - SaveState::Load(newGamePath, -1, [](SaveState::Status status, std::string_view message) { + SaveState::Load(newGamePath, -1, [](SaveState::Status status, std::string_view message, std::string_view metadata) { Core_Resume(); System_Notify(SystemNotification::DISASSEMBLY); }); @@ -965,7 +958,7 @@ void EmuScreen::ProcessVKey(VirtKey virtKey) { case VIRTKEY_REWIND: if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { if (SaveState::CanRewind()) { - SaveState::Rewind(&AfterSaveStateAction); + SaveState::Rewind(&ShowMessageAfterSaveStateAction); } else { g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0); } @@ -1058,12 +1051,12 @@ void EmuScreen::ProcessVKey(VirtKey virtKey) { case VIRTKEY_SAVE_STATE: if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction); } break; case VIRTKEY_LOAD_STATE: if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction); } break; case VIRTKEY_PREVIOUS_SLOT: @@ -2053,8 +2046,8 @@ void EmuScreen::AutoLoadSaveState() { } if (g_Config.iAutoLoadSaveState && autoSlot != -1) { - SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) { - AfterSaveStateAction(status, message); + SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message, std::string_view metadata) { + ShowMessageAfterSaveStateAction(status, message, metadata); auto sy = GetI18NCategory(I18NCat::SYSTEM); if (status == SaveState::Status::FAILURE) { autoLoadFailed_ = true; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 913068a78220..ab3b410d3d1b 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -133,6 +133,7 @@ #include "UI/OnScreenDisplay.h" #include "UI/RemoteISOScreen.h" #include "UI/Theme.h" +#include "UI/PauseScreen.h" #include "UI/UIAtlas.h" #if PPSSPP_PLATFORM(UWP) @@ -693,12 +694,7 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_BackgroundAudio.SFX().Init(); if (!boot_filename.empty() && stateToLoad.Valid()) { - SaveState::Load(stateToLoad, -1, [](SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } - }); + SaveState::Load(stateToLoad, -1, &ShowMessageAfterSaveStateAction); } if (g_Config.bAchievementsEnable) { diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 12b9d00a02fb..802145d6c28e 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -76,10 +76,10 @@ void copyDeepLinkForPath(std::string_view filePath); #endif -static void AfterSaveStateAction(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { +void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata) { + if (!message.empty()) { g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); + message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); } } @@ -153,7 +153,7 @@ class ScreenshotViewScreen : public UI::PopupScreen { void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_OK); //OK will close the pause screen as well } } @@ -161,7 +161,7 @@ void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { void ScreenshotViewScreen::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_OK); } } @@ -303,7 +303,7 @@ void SaveSlotView::Draw(UIContext &dc) { void SaveSlotView::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateLoaded.Trigger(e2); @@ -313,7 +313,7 @@ void SaveSlotView::OnLoadState(UI::EventParams &e) { void SaveSlotView::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateSaved.Trigger(e2); @@ -426,7 +426,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); loadUndoButton->SetEnabled(SaveState::HasUndoLoad(saveStatePrefix_)); loadUndoButton->OnClick.Add([this](UI::EventParams &e) { - SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction); + SaveState::UndoLoad(saveStatePrefix_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_CANCEL); }); } @@ -435,7 +435,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); rewindButton->SetEnabled(SaveState::CanRewind()); rewindButton->OnClick.Add([this](UI::EventParams &e) { - SaveState::Rewind(&AfterSaveStateAction); + SaveState::Rewind(&ShowMessageAfterSaveStateAction); TriggerFinish(DR_CANCEL); }); } diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 5cf57ac5f3f4..e7115c445814 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -23,6 +23,7 @@ #include "Common/File/Path.h" #include "Common/UI/UIScreen.h" #include "Common/UI/ViewGroup.h" +#include "Core/SaveState.h" #include "Core/ControlMapper.h" #include "UI/BaseScreens.h" #include "UI/Screen.h" @@ -85,3 +86,4 @@ class GamePauseScreen : public UIBaseDialogScreen, protected ControlListener { }; std::string GetConfirmExitMessage(); +void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata); diff --git a/Windows/MainWindowMenu.cpp b/Windows/MainWindowMenu.cpp index 0c684163d015..89831211ca7d 100644 --- a/Windows/MainWindowMenu.cpp +++ b/Windows/MainWindowMenu.cpp @@ -45,6 +45,7 @@ #include "Core/SaveState.h" #include "Core/Core.h" #include "Core/RetroAchievements.h" +#include "UI/PauseScreen.h" #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION #include "ext/rcheevos/include/rc_client_raintegration.h" @@ -394,10 +395,9 @@ namespace MainWindow { }); } - static void SaveStateActionFinished(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } + static void SaveStateActionFinished(SaveState::Status status, std::string_view message, std::string_view metadata) { + // Reuse the message from the pause screen. + ShowMessageAfterSaveStateAction(status, message, metadata); PostMessage(MainWindow::GetHWND(), WM_USER_SAVESTATE_FINISH, 0, 0); } diff --git a/assets/lang/ar_AE.ini b/assets/lang/ar_AE.ini index 2b8f3bc24bf4..bb192ba9089d 100644 --- a/assets/lang/ar_AE.ini +++ b/assets/lang/ar_AE.ini @@ -400,6 +400,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = منذ %1 ثانية # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/az_AZ.ini b/assets/lang/az_AZ.ini index 32d733ca71d2..837b93b953d1 100644 --- a/assets/lang/az_AZ.ini +++ b/assets/lang/az_AZ.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 saniyə əvvəl # AI translated %d hours = %d saat %d minutes = %d dəqiqə %d ms = %d ms diff --git a/assets/lang/be_BY.ini b/assets/lang/be_BY.ini index b050790e7b0d..93b76d553709 100644 --- a/assets/lang/be_BY.ini +++ b/assets/lang/be_BY.ini @@ -392,6 +392,7 @@ Vertex = Вяршыня VFPU = VFPU [Dialog] +%1 seconds ago = прыкладна %1 секунд таму # AI translated %d hours = %d г %d minutes = %d хв %d ms = %d мс diff --git a/assets/lang/bg_BG.ini b/assets/lang/bg_BG.ini index 1cbbe17765f7..b194e9d71cc8 100644 --- a/assets/lang/bg_BG.ini +++ b/assets/lang/bg_BG.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = преди %1 секунди # AI translated %d hours = %d часове %d minutes = %d минути %d ms = %d мс diff --git a/assets/lang/ca_ES.ini b/assets/lang/ca_ES.ini index 1daf258d4b72..4d35ece24d6c 100644 --- a/assets/lang/ca_ES.ini +++ b/assets/lang/ca_ES.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = fa %1 segons # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/cz_CZ.ini b/assets/lang/cz_CZ.ini index 2d137701cde4..ada38878128c 100644 --- a/assets/lang/cz_CZ.ini +++ b/assets/lang/cz_CZ.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = před %1 sekundami # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/da_DK.ini b/assets/lang/da_DK.ini index 1297c309b595..d70675d1f447 100644 --- a/assets/lang/da_DK.ini +++ b/assets/lang/da_DK.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = for %1 sekunder siden # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/de_DE.ini b/assets/lang/de_DE.ini index 9d3c51975316..a01db48faee1 100644 --- a/assets/lang/de_DE.ini +++ b/assets/lang/de_DE.ini @@ -391,6 +391,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = vor %1 Sekunden # AI translated %d hours = %d Stunden %d minutes = %d Minuten %d ms = %d ms diff --git a/assets/lang/dr_ID.ini b/assets/lang/dr_ID.ini index c0290b7928a0..870df3a4b6c5 100644 --- a/assets/lang/dr_ID.ini +++ b/assets/lang/dr_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 detik yang lalu # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index f60c941f47a7..a2c2c53ee640 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -416,6 +416,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 seconds ago %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/es_ES.ini b/assets/lang/es_ES.ini index e983e385c321..f5e7b6ee1f6b 100644 --- a/assets/lang/es_ES.ini +++ b/assets/lang/es_ES.ini @@ -393,6 +393,7 @@ Vertex = Vértices VFPU = VFPU [Dialog] +%1 seconds ago = hace %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/es_LA.ini b/assets/lang/es_LA.ini index dbf3cb31521a..fdf0c6c5d079 100644 --- a/assets/lang/es_LA.ini +++ b/assets/lang/es_LA.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = hace %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/fa_IR.ini b/assets/lang/fa_IR.ini index 5c8373f8e7b0..4bde39d3463f 100644 --- a/assets/lang/fa_IR.ini +++ b/assets/lang/fa_IR.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = قبل از %1 ثانیه # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d میلی‌ثانیه diff --git a/assets/lang/fi_FI.ini b/assets/lang/fi_FI.ini index 0241885b079d..eabc310cd21f 100644 --- a/assets/lang/fi_FI.ini +++ b/assets/lang/fi_FI.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 sekuntia sitten # AI translated %d hours = %d tuntia %d minutes = %d minuuttia %d ms = %d ms diff --git a/assets/lang/fr_FR.ini b/assets/lang/fr_FR.ini index 8efc6ec288ab..1c61b7280bf8 100644 --- a/assets/lang/fr_FR.ini +++ b/assets/lang/fr_FR.ini @@ -416,6 +416,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = il y a %1 secondes # AI translated %d hours = %d heures %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/gl_ES.ini b/assets/lang/gl_ES.ini index 2b3f1e598a50..c61bac47afbd 100644 --- a/assets/lang/gl_ES.ini +++ b/assets/lang/gl_ES.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = hai %1 segundos # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/gr_EL.ini b/assets/lang/gr_EL.ini index d3ddeba23f7c..80743c9060b4 100644 --- a/assets/lang/gr_EL.ini +++ b/assets/lang/gr_EL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = πριν από %1 δευτερόλεπτα # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/he_IL.ini b/assets/lang/he_IL.ini index 3ade3a70b25d..d1598c727833 100644 --- a/assets/lang/he_IL.ini +++ b/assets/lang/he_IL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = לפני %1 שניות # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/he_IL_invert.ini b/assets/lang/he_IL_invert.ini index 16a70df8f2e6..5e08433a6070 100644 --- a/assets/lang/he_IL_invert.ini +++ b/assets/lang/he_IL_invert.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = לפני %1 שניות # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/hr_HR.ini b/assets/lang/hr_HR.ini index ff5f4551420b..8ab11f19f49a 100644 --- a/assets/lang/hr_HR.ini +++ b/assets/lang/hr_HR.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = prije %1 sekundi # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/hu_HU.ini b/assets/lang/hu_HU.ini index 6384a42a1efe..91574ba6752f 100644 --- a/assets/lang/hu_HU.ini +++ b/assets/lang/hu_HU.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 másodperce # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/id_ID.ini b/assets/lang/id_ID.ini index 50aea553e7fa..4554527ba3c2 100644 --- a/assets/lang/id_ID.ini +++ b/assets/lang/id_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = sejak %1 detik yang lalu # AI translated %d hours = %d jam %d minutes = %d menit %d ms = %d ms diff --git a/assets/lang/it_IT.ini b/assets/lang/it_IT.ini index 93acd4f2920b..3e42a0bcb7e8 100644 --- a/assets/lang/it_IT.ini +++ b/assets/lang/it_IT.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = fa %1 secondi # AI translated %d hours = %d ore %d minutes = %d minuti %d ms = %d ms diff --git a/assets/lang/ja_JP.ini b/assets/lang/ja_JP.ini index 668ede31987c..d9805bdae80c 100644 --- a/assets/lang/ja_JP.ini +++ b/assets/lang/ja_JP.ini @@ -392,6 +392,7 @@ Vertex = 頂点 VFPU = VFPU [Dialog] +%1 seconds ago = %1 秒前 # AI translated %d hours = %d 時間 %d minutes = %d 分 %d ms = %d ms diff --git a/assets/lang/jv_ID.ini b/assets/lang/jv_ID.ini index f9bce51608da..bae709408c2a 100644 --- a/assets/lang/jv_ID.ini +++ b/assets/lang/jv_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = saka %1 detik kepungkur # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ko_KR.ini b/assets/lang/ko_KR.ini index ca855d8a9faa..700f0a77066c 100644 --- a/assets/lang/ko_KR.ini +++ b/assets/lang/ko_KR.ini @@ -392,6 +392,7 @@ Vertex = 꼭짓점 VFPU = VFPU [Dialog] +%1 seconds ago = %1초 전 # AI translated %d hours = %d 시간 %d minutes = %d 분 %d ms = %d ms diff --git a/assets/lang/ku_SO.ini b/assets/lang/ku_SO.ini index 13fdb8d9b018..5f8ea73409be 100644 --- a/assets/lang/ku_SO.ini +++ b/assets/lang/ku_SO.ini @@ -406,6 +406,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = bori %1 çirkan # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/lo_LA.ini b/assets/lang/lo_LA.ini index 7977e1426e82..65ceaefbb5d7 100644 --- a/assets/lang/lo_LA.ini +++ b/assets/lang/lo_LA.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ຕ໭ວບ %1 ວິນາທີ # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/lt-LT.ini b/assets/lang/lt-LT.ini index 80c955f756cb..eb55dcca266a 100644 --- a/assets/lang/lt-LT.ini +++ b/assets/lang/lt-LT.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = prieš %1 sekundes # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ms_MY.ini b/assets/lang/ms_MY.ini index 3e679dc3196b..b404f11ec629 100644 --- a/assets/lang/ms_MY.ini +++ b/assets/lang/ms_MY.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = lebih %1 saat yang lalu # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/nl_NL.ini b/assets/lang/nl_NL.ini index aa0f2a678d8d..23567e00be9a 100644 --- a/assets/lang/nl_NL.ini +++ b/assets/lang/nl_NL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ongeveer %1 seconden geleden # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/no_NO.ini b/assets/lang/no_NO.ini index 740615776976..ab21b7a95b60 100644 --- a/assets/lang/no_NO.ini +++ b/assets/lang/no_NO.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = for %1 sekunder siden # AI translated %d hours = %d timer %d minutes = %d minutter %d ms = %d ms diff --git a/assets/lang/pl_PL.ini b/assets/lang/pl_PL.ini index bde823aae0bc..06962d68ea21 100644 --- a/assets/lang/pl_PL.ini +++ b/assets/lang/pl_PL.ini @@ -392,6 +392,7 @@ Vertex = Wierzchołka VFPU = VFPU [Dialog] +%1 seconds ago = przed %1 sekundami # AI translated %d hours = %d godzin %d minutes = %d minut %d ms = %d milisekund diff --git a/assets/lang/pt_BR.ini b/assets/lang/pt_BR.ini index ac7bf3881117..a4eb698d7f92 100644 --- a/assets/lang/pt_BR.ini +++ b/assets/lang/pt_BR.ini @@ -416,6 +416,7 @@ Vertex = Vértice VFPU = VFPU [Dialog] +%1 seconds ago = há %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/pt_PT.ini b/assets/lang/pt_PT.ini index 27aa60d51a67..bde2c0a51b22 100644 --- a/assets/lang/pt_PT.ini +++ b/assets/lang/pt_PT.ini @@ -416,6 +416,7 @@ Vertex = Vértice VFPU = VFPU [Dialog] +%1 seconds ago = há %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/ro_RO.ini b/assets/lang/ro_RO.ini index 7765f0686b2c..dc6d4f1cd85e 100644 --- a/assets/lang/ro_RO.ini +++ b/assets/lang/ro_RO.ini @@ -393,6 +393,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = acum %1 secunde # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ru_RU.ini b/assets/lang/ru_RU.ini index 54a78243be4f..7178870c16ae 100644 --- a/assets/lang/ru_RU.ini +++ b/assets/lang/ru_RU.ini @@ -392,6 +392,7 @@ Vertex = Вершина VFPU = VFPU [Dialog] +%1 seconds ago = %1 секунду назад # AI translated %d hours = %d ч %d minutes = %d мин %d ms = %d мс diff --git a/assets/lang/sv_SE.ini b/assets/lang/sv_SE.ini index e1d6f6712216..4460c4c2febe 100644 --- a/assets/lang/sv_SE.ini +++ b/assets/lang/sv_SE.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = för %1 sekunder sedan # AI translated %d hours = %d timmar %d minutes = %d minuter %d ms = %d ms diff --git a/assets/lang/tg_PH.ini b/assets/lang/tg_PH.ini index ebc8fc4ad6c6..a82c7b58195d 100644 --- a/assets/lang/tg_PH.ini +++ b/assets/lang/tg_PH.ini @@ -393,6 +393,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ҳоло %1 сония пеш # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/th_TH.ini b/assets/lang/th_TH.ini index c421c0601907..0b806ec06c94 100644 --- a/assets/lang/th_TH.ini +++ b/assets/lang/th_TH.ini @@ -408,6 +408,7 @@ Vulkan = วัลแคน VFPU = VFPU [Dialog] +%1 seconds ago = เมื่อ %1 วินาทีที่แล้ว # AI translated %d hours = %d ชั่วโมง %d minutes = %d นาที %d ms = %d ms diff --git a/assets/lang/tr_TR.ini b/assets/lang/tr_TR.ini index 66a379b7eeb9..ef8f3960aaf6 100644 --- a/assets/lang/tr_TR.ini +++ b/assets/lang/tr_TR.ini @@ -394,6 +394,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 saniye önce # AI translated %d hours = %d saat %d minutes = %d dakika %d ms = %d ms diff --git a/assets/lang/uk_UA.ini b/assets/lang/uk_UA.ini index eda0d05ed883..d3b876b103fe 100644 --- a/assets/lang/uk_UA.ini +++ b/assets/lang/uk_UA.ini @@ -392,6 +392,7 @@ Vertex = Вершина VFPU = VFPU [Dialog] +%1 seconds ago = %1 секунд тому # AI translated %d hours = %d год %d minutes = %d хв %d ms = %d мс diff --git a/assets/lang/vi_VN.ini b/assets/lang/vi_VN.ini index 11873c79bde7..60587210ac09 100644 --- a/assets/lang/vi_VN.ini +++ b/assets/lang/vi_VN.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = trước %1 giây # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/zh_CN.ini b/assets/lang/zh_CN.ini index efed768b1f9f..94f68bb6c9d1 100644 --- a/assets/lang/zh_CN.ini +++ b/assets/lang/zh_CN.ini @@ -392,6 +392,7 @@ Vertex = 顶点着色器 VFPU = VFPU [Dialog] +%1 seconds ago = %1秒前 # AI translated %d hours = %d 小时 %d minutes = %d 分 %d ms = %d 毫秒 diff --git a/assets/lang/zh_TW.ini b/assets/lang/zh_TW.ini index 46c4c1d39191..93ddf851e65d 100644 --- a/assets/lang/zh_TW.ini +++ b/assets/lang/zh_TW.ini @@ -392,6 +392,7 @@ Vertex = 頂點 VFPU = VFPU [Dialog] +%1 seconds ago = %1 秒鐘前 # AI translated %d hours = %d 小時 %d minutes = %d 分 %d ms = %d 毫秒