From 90cea302b9f15ecafaba0694656b7d3c5f103afa Mon Sep 17 00:00:00 2001 From: LunaMoo Date: Mon, 26 Oct 2026 11:39:48 +0100 Subject: [PATCH] HOME button quit screen, with the firmware's art when available As on a PSP: pressing HOME in a game shows "Do you want to quit the game?" over it. The game keeps running but gets no input while it's up (sceCtrl samples are blanked). Left/right picks Yes or No, the confirm button (our Confirmation button setting, or what compat forces) chooses, the other face button or HOME closes it, and Yes/No can be tapped. The volume keys change the game volume a step at a time, shown on the volume bar. Yes runs the game's registered exit callback first, like the PSP does, showing "Please wait..." meanwhile. Then, if the XMB launched the game, the XMB is booted again; otherwise it's back to the game browser, like Exit to menu. If the callback hasn't ended the game in 10 s, it's quit anyway. Not in the XMB itself, the PS1 emulator (its own HOME menu) or GE dumps. It lives in UI/ImposeHome.cpp; EmuScreen only feeds it input and frames and draws it over the PSP display. With a firmware dump installed, it's drawn with the firmware's own art from flash0:/kd/resource/impose.rsc, which is what impose.prx draws it from: prerendered text for each system language (the question, Yes/No normal and highlighted, Enter/Back, Please wait...), the button icons, the lines, the volume bar, the battery and the clock glyphs. The format is documented in ImposeHome.cpp. Without one, the same screen is drawn with our font and simple shapes at the same positions. Where impose.rsc doesn't say where things go, the layout follows impose_02g.prx (6.61), decrypted and disassembled: - The screen is filled with 0x9CE7 (#393939); the images are opaque. - Lines at (80, 96) and (80, 175); impose.prx grows them from the middle when it opens, which isn't done here. - Volume bar: the speaker at (28, 223), 30 steps from x 62, 12 apart. It shows our game volume in the PSP's 30 steps. - Button guide: the cross icon always left, the circle right, "Enter" and "Back" next to whichever confirms and cancels. - Status bar: the battery at (450, 5) (60% and up full, 30% two bars, 12% one, then empty), the clock ending next to it at y 5: "M/D H:MM", or "D/M" for the DD/MM/YYYY format, no leading zero on the month, day or hour, AM/PM for the 12-hour clock. The clock is the emulated PSP's local time (__RtcGetCurrentLocalTime, new), in the system's date and time formats. The battery is this device's, since the emulated PSP's is always full; a device without one shows it full. For that, Windows now reports -1 for SYSPROP_BATTERY_PERCENTAGE without a battery (was 100), as SDL already did; SDL polls the battery even when the status overlay doesn't show it, and the overlay leaves it out when there's none. Core side: __CtrlSetGameInputBlocked; __KernelCanExitToVSH and __KernelRequestExitToVSH to quit to the XMB from outside a syscall; and __KernelInvokeRegisteredExitCallback(toVSH), so a callback that returns without exiting ends in the XMB rather than powering down. New strings: "Do you want to quit the game?", "Please wait..." (Dialog). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NB244iFpvm14JH2FbwHXeQ --- Core/HLE/sceCtrl.cpp | 19 + Core/HLE/sceCtrl.h | 4 + Core/HLE/sceKernelModule.cpp | 38 ++ Core/HLE/sceKernelModule.h | 7 + Core/HLE/sceKernelThread.cpp | 17 +- Core/HLE/sceKernelThread.h | 5 +- Core/HLE/sceRtc.cpp | 13 + Core/HLE/sceRtc.h | 4 + SDL/SDLMain.cpp | 7 +- UI/CMakeLists.txt | 2 + UI/DebugOverlay.cpp | 6 +- UI/EmuScreen.cpp | 25 +- UI/EmuScreen.h | 5 + UI/ImposeHome.cpp | 884 ++++++++++++++++++++++++++++++ UI/ImposeHome.h | 78 +++ UI/UI.vcxproj | 2 + UI/UI.vcxproj.filters | 6 + UWP/UI_UWP/UI_UWP.vcxproj | 2 + UWP/UI_UWP/UI_UWP.vcxproj.filters | 6 + Windows/main.cpp | 6 +- android/jni/Android.mk | 1 + assets/lang/ar_AE.ini | 2 + assets/lang/az_AZ.ini | 2 + assets/lang/be_BY.ini | 2 + assets/lang/bg_BG.ini | 2 + assets/lang/ca_ES.ini | 2 + assets/lang/cz_CZ.ini | 2 + assets/lang/da_DK.ini | 2 + assets/lang/de_DE.ini | 2 + assets/lang/dr_ID.ini | 2 + assets/lang/en_US.ini | 2 + assets/lang/es_ES.ini | 2 + assets/lang/es_LA.ini | 2 + assets/lang/fa_IR.ini | 2 + assets/lang/fi_FI.ini | 2 + assets/lang/fr_FR.ini | 2 + assets/lang/gl_ES.ini | 2 + assets/lang/gr_EL.ini | 2 + assets/lang/he_IL.ini | 2 + assets/lang/hr_HR.ini | 2 + assets/lang/hu_HU.ini | 2 + assets/lang/id_ID.ini | 2 + assets/lang/it_IT.ini | 2 + assets/lang/ja_JP.ini | 2 + assets/lang/jv_ID.ini | 2 + assets/lang/km_KH.ini | 2 + assets/lang/ko_KR.ini | 2 + assets/lang/ku_SO.ini | 2 + assets/lang/lo_LA.ini | 2 + assets/lang/lt-LT.ini | 2 + assets/lang/ms_MY.ini | 2 + assets/lang/nl_NL.ini | 2 + assets/lang/nn_NO.ini | 2 + assets/lang/no_NO.ini | 2 + assets/lang/pl_PL.ini | 2 + assets/lang/pt_BR.ini | 2 + assets/lang/pt_PT.ini | 2 + assets/lang/ro_RO.ini | 2 + assets/lang/ru_RU.ini | 2 + assets/lang/sv_SE.ini | 2 + assets/lang/tg_PH.ini | 2 + assets/lang/th_TH.ini | 2 + assets/lang/tr_TR.ini | 2 + assets/lang/uk_UA.ini | 2 + assets/lang/vi_VN.ini | 2 + assets/lang/zh_CN.ini | 2 + assets/lang/zh_TW.ini | 2 + 67 files changed, 1217 insertions(+), 12 deletions(-) create mode 100644 UI/ImposeHome.cpp create mode 100644 UI/ImposeHome.h diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp index bbf85eb094..c623d2b368 100644 --- a/Core/HLE/sceCtrl.cpp +++ b/Core/HLE/sceCtrl.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -109,6 +110,10 @@ static u32 emuRapidFireFrames = 0; static bool emuRapidFireToggle = true; static u32 emuRapidFireInterval = 5; +// Set while a system overlay (the HOME button's quit screen) owns the controls: the game then reads +// no buttons and centered sticks, as impose does on a PSP. Host-side peeks still see the real state. +static std::atomic gameInputBlocked{ false }; + // These buttons are not affected by rapid fire (neither is analog.) const u32 CTRL_EMU_RAPIDFIRE_MASK = CTRL_UP | CTRL_DOWN | CTRL_LEFT | CTRL_RIGHT; @@ -139,6 +144,11 @@ static void __CtrlUpdateLatch() ctrlBufs[ctrlBuf].analog[0][1] = (u8)Clamp(rY + 128, 0.0f, 255.0f); } + if (gameInputBlocked) { + buttons = 0; + memset(ctrlBufs[ctrlBuf].analog, CTRL_ANALOG_CENTER, sizeof(ctrlBufs[ctrlBuf].analog)); + } + ctrlBufs[ctrlBuf].buttons = buttons; u32 changed = buttons ^ ctrlOldButtons; @@ -171,6 +181,14 @@ static int __CtrlResetLatch() return oldBufs; } +void __CtrlSetGameInputBlocked(bool blocked) { + gameInputBlocked = blocked; +} + +bool __CtrlIsGameInputBlocked() { + return gameInputBlocked; +} + u32 __CtrlPeekButtons() { std::lock_guard guard(ctrlMutex); @@ -429,6 +447,7 @@ void __CtrlInit() { // Start with everything released. latch.btnRelease = 0xffffffff; + gameInputBlocked = false; memset(&ctrlCurrent, 0, sizeof(ctrlCurrent)); memset(ctrlCurrent.analog, CTRL_ANALOG_CENTER, sizeof(ctrlCurrent.analog)); analogEnabled = false; diff --git a/Core/HLE/sceCtrl.h b/Core/HLE/sceCtrl.h index 768c1f8f2d..274606abe3 100644 --- a/Core/HLE/sceCtrl.h +++ b/Core/HLE/sceCtrl.h @@ -90,6 +90,10 @@ bool __CtrlGetRapidFire(); // For use by internal UI like MsgDialog u32 __CtrlPeekButtons(); +// While set, the game sees no buttons and centered sticks (a system overlay such as the HOME +// button's quit screen has the controls). __CtrlPeekButtons still returns the real state. +void __CtrlSetGameInputBlocked(bool blocked); +bool __CtrlIsGameInputBlocked(); // Exposed so sceVshBridge can reuse it directly for vshCtrlReadBufferPositive, matching JPCSP. int sceCtrlReadBufferPositive(u32 ctrlDataPtr, u32 nBufs); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index acb606db20..596f1c9a55 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2762,6 +2762,44 @@ static std::vector VshmainReturnArgs(const std::vector &launchArgs) { return args; } +// For a game the XMB booted, or loaded in place under its own session: the boot that takes it back +// to the XMB. False otherwise (or for the XMB itself, or the PS1 emulator, which has its own HOME menu +// with its own way out). +static bool GetExitToVSHRequest(BootRequest *request) { + if (g_runningVSH || PopsBootActive()) { + return false; + } + const std::optional &launchedFromVSH = PSP_CoreParameter().launchedFromVSH; + if (launchedFromVSH) { + request->fileToStart = launchedFromVSH->vshmain; + request->vshmainArgs = VshmainReturnArgs(launchedFromVSH->vshmainArgs); + } else if (PSP_CoreParameter().fileToStart.GetFilename() == "vshmain.prx") { + // Loaded in place: the XMB's own boot file is what we started from. + request->fileToStart = PSP_CoreParameter().fileToStart; + request->vshmainArgs = VshmainReturnArgs(g_vshmainArgs); + } else { + return false; + } + request->mountIso = __UmdGetPath(); + return true; +} + +bool __KernelCanExitToVSH() { + BootRequest request; + return GetExitToVSHRequest(&request); +} + +bool __KernelRequestExitToVSH() { + BootRequest request; + if (!GetExitToVSHRequest(&request)) { + return false; + } + INFO_LOG(Log::sceModule, "Quitting the game: back to the XMB"); + // Stops the core; the frontend then boots the XMB like anything else. + PSP_RequestBoot(request); + return true; +} + bool __KernelExitToVSH(MIPSState *mips) { // Normally the XMB launched this game by booting it, so it boots the XMB again. const std::optional &launchedFromVSH = PSP_CoreParameter().launchedFromVSH; diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 6ab8d38759..1ab357464d 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -248,6 +248,13 @@ bool __KernelIsRunningVSH(); // What sceKernelExitGame does in a game the XMB launched, as on a PSP: back to the XMB, by booting it // again. False if the XMB didn't launch this. bool __KernelExitToVSH(MIPSState *mips); +// Whether the running game can quit back to the XMB, the way the HOME button's quit screen does it: +// the XMB launched it (and it's not the PS1 emulator, whose HOME menu is its own). +bool __KernelCanExitToVSH(); +// Quits the running game back to the XMB from outside a syscall (the host's HOME screen, or after +// the game's exit callback returned): asks the frontend to boot the XMB, which stops the core. +// False if __KernelCanExitToVSH() is. +bool __KernelRequestExitToVSH(); // Exposed so sceVshBridge can reuse it directly for vshKernelLoadModuleBufferVSH, matching JPCSP. SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr); bool __KernelLoadExecFromBuffer(MIPSState *mips, const u8 *data, size_t size, u32 paramPtr, std::string *error_string); diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index 92b21feec2..1e4d3308f4 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -280,6 +280,10 @@ public: g_exitCallbackPending = false; // The callback may have already powered down by calling sceKernelExitGame - that's fine, // Core_Stop is idempotent. Either way, the host will see CORE_POWERDOWN and tear the game down. + // Quitting from the HOME screen goes back to the XMB instead, as on a PSP. + if (toVSH && coreState != CORE_POWERDOWN && __KernelRequestExitToVSH()) { + return; + } Core_Stop(); } @@ -288,14 +292,22 @@ public: } void DoState(PointerWrap &p) override { - auto s = p.Section("ActionAfterExitCallback", 1); + auto s = p.Section("ActionAfterExitCallback", 1, 2); if (!s) return; + if (s >= 2) { + Do(p, toVSH); + } else { + toVSH = false; + } if (p.mode == p.MODE_READ) { // The dispatch is still in flight in the loaded state (see __KernelThreadingDoState.) g_exitCallbackPending = true; } } + + // Set when the HOME button's quit screen asked for the exit: then it ends in the XMB. + bool toVSH = false; }; u32 PSPThread::GetMissingErrorCode() { @@ -4006,7 +4018,7 @@ int sceKernelRegisterExitCallback(SceUID cbId) { // be dispatched (none registered, registering thread dead, etc.) - the caller should then power down // immediately. While true is returned, __KernelIsExitCallbackPending() will report true until the // callback returns (via the chained ActionAfterExitCallback). -bool __KernelInvokeRegisteredExitCallback() { +bool __KernelInvokeRegisteredExitCallback(bool toVSH) { if (g_exitCallbackPending) { WARN_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: already in progress"); return true; @@ -4030,6 +4042,7 @@ bool __KernelInvokeRegisteredExitCallback() { } ActionAfterExitCallback *action = (ActionAfterExitCallback *)__KernelCreateAction(actionAfterExitCallback); + action->toVSH = toVSH; // Exit callbacks on real PSP receive (arg1, arg2, common) where arg1/arg2 are zero for HOME-button exit. const u32 args[] = { 0, 0, cb->nc.commonArgument }; diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index 5cf6b2d67c..28accae99f 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -533,8 +533,9 @@ int sceKernelRegisterExitCallback(SceUID cbId); // Dispatch the exit callback registered via sceKernelRegisterExitCallback on its registering thread, // so the game has a chance to clean up before we shut down. Returns false if no callback can be // dispatched (none registered, the thread is gone, etc.) - in that case, the host should proceed -// to power down immediately. -bool __KernelInvokeRegisteredExitCallback(); +// to power down immediately. With toVSH (the HOME button's quit screen), a callback that returns +// without exiting itself ends in the XMB rather than powering down. +bool __KernelInvokeRegisteredExitCallback(bool toVSH = false); // True while a previously-invoked exit callback hasn't yet returned. The host should keep running // emulation while this is true, and only tear down after it goes false (or after a timeout). bool __KernelIsExitCallbackPending(); diff --git a/Core/HLE/sceRtc.cpp b/Core/HLE/sceRtc.cpp index 828575f834..89314c8316 100644 --- a/Core/HLE/sceRtc.cpp +++ b/Core/HLE/sceRtc.cpp @@ -322,6 +322,19 @@ static u32 sceRtcGetCurrentClock(u32 pspTimePtr, int tz) { return hleLogDebug(Log::sceRtc, 0); } +bool __RtcGetCurrentLocalTime(ScePspDateTime *t) { + PSPTimeval tv; + __RtcTimeOfDay(&tv); + time_t sec = (time_t)tv.tv_sec; + const tm *local = localtime(&sec); + if (!local) { + return false; + } + __RtcTmToPspTime(*t, local); + t->microsecond = tv.tv_usec; + return true; +} + static u32 sceRtcGetCurrentClockLocalTime(u32 pspTimePtr) { auto pt = PSPPointer::Create(pspTimePtr); diff --git a/Core/HLE/sceRtc.h b/Core/HLE/sceRtc.h index 38978d38cc..7aa5f048a8 100644 --- a/Core/HLE/sceRtc.h +++ b/Core/HLE/sceRtc.h @@ -48,3 +48,7 @@ struct ScePspDateTime { s16_le second; u32_le microsecond; }; + +// The emulated PSP's clock, in local time - what sceRtcGetCurrentClockLocalTime gives the game. +// For host UI that shows the PSP's time. False if the date is out of range. +bool __RtcGetCurrentLocalTime(ScePspDateTime *t); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 806c4d38bf..ce74862ba7 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -129,7 +129,8 @@ struct WindowState { static WindowState g_windowState; #if !PPSSPP_PLATFORM(MAC) -static int g_batteryPercent = 0; +// -1 until polled, or without a battery. +static int g_batteryPercent = -1; #endif #if PPSSPP_PLATFORM(MAC) @@ -2456,10 +2457,8 @@ int main(int argc, char *argv[]) { } }); + // Always polled: the status overlay shows it, and so does the HOME screen. auto kick_battery_poll_thread = [&last_battery_poll, &battery_poll_thread_sema] { - if (!(g_Config.iShowStatusFlags & (int)ShowStatusFlags::BATTERY_PERCENT)) { - return; - } double now = time_now_d(); if (now - last_battery_poll >= 5.0) { last_battery_poll = now; diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 8bdcee94b7..f56f6c16a3 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -43,6 +43,8 @@ list(APPEND UISource DisplayLayoutScreen.h EmuScreen.h EmuScreen.cpp + ImposeHome.h + ImposeHome.cpp GameInfoCache.h GameInfoCache.cpp IAPScreen.cpp diff --git a/UI/DebugOverlay.cpp b/UI/DebugOverlay.cpp index fe4ba4b781..b67e030b2d 100644 --- a/UI/DebugOverlay.cpp +++ b/UI/DebugOverlay.cpp @@ -551,8 +551,10 @@ void DrawFPS(UIContext *ctx, const Bounds &bounds) { if (System_GetPropertyBool(SYSPROP_CAN_READ_BATTERY_PERCENTAGE)) { if (g_Config.iShowStatusFlags & (int)ShowStatusFlags::BATTERY_PERCENT) { const int percentage = System_GetPropertyInt(SYSPROP_BATTERY_PERCENTAGE); - // Just plain append battery. Add linebreak? - w.F(" Battery: %d%%", percentage); + // Just plain append battery. Add linebreak? Nothing without a battery. + if (percentage >= 0) { + w.F(" Battery: %d%%", percentage); + } } } diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index dbe7db9b33..2e67b4206a 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -86,6 +86,7 @@ using namespace std::placeholders; #include "UI/MainScreen.h" #include "UI/Background.h" #include "UI/EmuScreen.h" +#include "UI/ImposeHome.h" #include "UI/DevScreens.h" #include "UI/GameInfoCache.h" #include "UI/BaseScreens.h" @@ -162,7 +163,7 @@ void EmuScreen::SetPSPAnalog(int iInternalScreenRotation, int stick, float x, fl } EmuScreen::EmuScreen(const Path &filename, const Path &mountIso, std::optional umdAutoboot) - : gamePath_(filename), mountIso_(mountIso), umdAutoboot_(umdAutoboot) { + : gamePath_(filename), mountIso_(mountIso), umdAutoboot_(umdAutoboot), imposeHome_(std::make_unique()) { saveStateSlot_ = SaveState::GetCurrentSlot(); g_controlMapper.AddListener(this); @@ -1194,6 +1195,10 @@ bool EmuScreen::key(const KeyInput &key) { bool EmuScreen::touch(const TouchInput &touch) { System_Notify(SystemNotification::ACTIVITY); + if (imposeHome_->IsOpen() && imposeHome_->Touch(touch)) { + return true; + } + bool ignoreGamepad = false; if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) { @@ -1377,6 +1382,7 @@ void EmuScreen::CreateViews() { void EmuScreen::deviceLost() { // A boot in progress only creates the GPU from PSP_InitUpdate, which doesn't run while the device is lost. UIScreen::deviceLost(); + imposeHome_->DeviceLost(); } void EmuScreen::deviceRestored(Draw::DrawContext *draw) { @@ -1439,6 +1445,12 @@ void EmuScreen::update() { RecreateViews(); } + // The HOME button's quit screen. + const bool gameRunning = PSP_IsInited() && !bootPending_ && coreState == CORE_RUNNING_CPU && !PSP_HasBootRequest(); + if (imposeHome_->Update(gameRunning, IsOnTop()) == ImposeHomeScreen::Action::LEAVE_GAME) { + leaveGame(); + } + using namespace UI; // This is where views are recreated. @@ -1984,6 +1996,8 @@ ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { } bool EmuScreen::hasVisibleUI() { + if (imposeHome_->IsOpen()) + return true; // Regular but uncommon UI. if (saveStatePreview_->GetVisibility() != UI::V_GONE || loadingSpinner_->GetVisibility() == UI::V_VISIBLE || saveStateSpinner_->GetVisibility() == UI::V_VISIBLE) return true; @@ -2020,6 +2034,15 @@ void EmuScreen::renderUI() { root_->Draw(*ctx); } + if (imposeHome_->IsOpen() && PSP_IsInited()) { + // Over the PSP screen, wherever the layout puts it. + const DisplayLayoutConfig &config = GameLayoutConfig(); + FRect frame = GetScreenFrame(config.bIgnoreScreenInsets, g_display.pixel_xres, g_display.pixel_yres); + FRect rc; + CalculateDisplayOutputRect(config, &rc, 480.0f, 272.0f, frame, config.iInternalScreenRotation); + imposeHome_->Draw(*ctx, Bounds(rc.x * g_display.dpi_scale_x, rc.y * g_display.dpi_scale_y, rc.w * g_display.dpi_scale_x, rc.h * g_display.dpi_scale_y)); + } + if (PSP_IsInited()) { if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::CONTROL) { DrawControlMapperOverlay(ctx, GetLayoutBounds(*ctx), g_controlMapper); diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index ca8f919bee..f61cdbbf66 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include #include @@ -36,6 +37,7 @@ struct AxisInput; struct DisplayLayoutConfig; class AsyncImageFileView; +class ImposeHomeScreen; class ChatMenu; class ImDebugger; struct ImCommand; @@ -171,6 +173,9 @@ private: DeviceOrientation gameOutputOrientation_ = DeviceOrientation::Landscape; uint32_t clearColor_ = 0; + + // The HOME button's quit screen (as impose draws it on a PSP). + std::unique_ptr imposeHome_; }; bool MustRunBehind(); diff --git a/UI/ImposeHome.cpp b/UI/ImposeHome.cpp new file mode 100644 index 0000000000..03cc160359 --- /dev/null +++ b/UI/ImposeHome.cpp @@ -0,0 +1,884 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// This program 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 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/File/FileUtil.h" +#include "Common/GPU/thin3d.h" +#include "Common/Input/InputState.h" +#include "Common/Log.h" +#include "Common/Render/DrawBuffer.h" +#include "Common/System/System.h" +#include "Common/TimeUtil.h" +#include "Common/UI/Context.h" +#include "Common/UI/View.h" +#include "Core/Config.h" +#include "Core/ConfigValues.h" +#include "Core/Core.h" +#include "Core/System.h" +#include "Core/HLE/PopsBoot.h" +#include "Core/HLE/sceCtrl.h" +#include "Core/HLE/sceKernelModule.h" +#include "Core/HLE/sceKernelThread.h" +#include "Core/HLE/sceRtc.h" +#include "Core/HLE/sceUtility.h" +#include "UI/ImposeHome.h" + +// The artwork of the PSP's own HOME screen ("Do you want to quit the game?"), from the installed +// firmware's flash0:/kd/resource/impose.rsc. impose.prx has no font, so every string is a +// prerendered bitmap, one set per system language, each with its place on the 480x272 screen. +// +// impose.rsc, as of 6.61 (all little-endian u32 unless noted): +// 0x00 palette count (3), language block count (12), shared image count (21) +// 0x0C offsets from the start of the file: the palettes, then the language blocks (in PSP +// system language order), then the shared images +// palette: 16 u16 colors, ABGR1555. 0 is normal, 1 is the highlighted button, 2 a dimmer +// highlight. +// language block: image count (10), the circle button icon's x, y, the cross button +// icon's x, y, padding to 0x44, then per image: palette, byte size, x, y, width, height, +// offset of its 4bpp pixels (low nibble first) from 0x44 into the block. The images are +// the question, "Enter", "Back", "Please wait...", then Yes and No, each in palette 0, 1 +// and 2. +// shared image: byte size, then ABGR1555 pixels, no dimensions (impose.prx knows them). +// The ones used here: 0 the battery (28x17, four levels stacked, full first), 2 and 3 the +// circle and cross button icons (16x16), 8 the line above and below the question (328x9), +// 16 a volume step, set and unset (8x24 each), 18 the speaker (32x28, then muted and +// dimmed versions), 10 the digits (16x15 each, stacked), 12 "/" and +// ":" (12x15 each), 14 "AM" and "PM" (32x16 each). 11, 13 and 15 are the same shifted by +// a pixel, since impose.prx copies 32 bits at a time and needs them for odd x. The rest are +// for impose's other overlays (volume, brightness, mute, hold). +// +// The status bar at the top, the lines and the volume bar aren't placed by impose.rsc: impose.prx +// draws them with these images, at the positions DrawStatusBar and DrawFrame use (read from +// impose_02g.prx, 6.61). The same goes for the button guide: the cross icon is always on the left +// and the circle on the right, and "Enter" and "Back" go next to whichever confirms and cancels. +class ImposeArt { +public: + enum class Item { + QUESTION, + ENTER, + BACK, + PLEASE_WAIT, + YES, + YES_SELECTED, + YES_DIM, + NO, + NO_SELECTED, + NO_DIM, + CIRCLE_ICON, + CROSS_ICON, + BATTERY_FULL, // Then 2 bars, 1 bar, empty. + DIGIT_0 = BATTERY_FULL + 4, + SLASH = DIGIT_0 + 10, + COLON, + AM, + PM, + LINE, + VOLUME_SET, + VOLUME_UNSET, + SPEAKER, + COUNT, + }; + + ~ImposeArt(); + + // Reads impose.rsc from the installed firmware (g_Config.nandRootDirectory) for this PSP + // system language. Only touches the file on the first call or when the language changes. + // False if there's no firmware or the file isn't the expected format. + bool Load(int pspLanguage); + // Where an item goes on the PSP screen (480x272), mapped onto screen. + Bounds ItemBounds(Item item, const Bounds &screen) const; + // The button guide: where the circle or cross icon goes, and where "Enter" or "Back" goes + // when its button is the circle or the cross. + Bounds IconBounds(bool circle, const Bounds &screen) const; + Bounds LabelBounds(Item label, bool circle, const Bounds &screen) const; + void Draw(UIContext &dc, Item item, const Bounds &where); + + // The date and time at the top right, as impose.prx draws them, with the battery to their + // right (batteryPercent < 0: none, as on a PSP running off the adapter with its battery out). dayFirst is the DD/MM/YYYY date format, twelveHour the 12-hour clock. + void DrawStatusBar(UIContext &dc, const Bounds &screen, int month, int day, int hour, int minute, bool dayFirst, bool twelveHour, int batteryPercent); + // The lines above and below the question, and the volume bar (volume 0-30) when volume >= 0. + void DrawFrame(UIContext &dc, const Bounds &screen, int volume); + // The same text, for drawing it without the firmware's glyphs: "12/31 9:05" or "31/12 9:05 PM". + static std::string ClockText(int month, int day, int hour, int minute, bool dayFirst, bool twelveHour); + // Which battery level impose.prx shows for a charge (0 full .. 3 empty). + static int BatteryLevel(int percent); + + void DeviceLost(); + + // The color behind everything on the screen, which the text bitmaps are drawn on (ABGR8888). + static constexpr uint32_t BACKGROUND = 0xFF393939; + +private: + struct Sprite { + int atlasY = 0; + int w = 0; + int h = 0; + int x = 0; // On the PSP screen. + int y = 0; + }; + + bool Parse(const std::vector &file, int pspLanguage); + Draw::Texture *GetTexture(Draw::DrawContext *draw); + + bool tried_ = false; + bool loaded_ = false; + int language_ = -1; + Sprite sprites_[(int)Item::COUNT]{}; + int circleIconX_ = 0, circleIconY_ = 0; + int crossIconX_ = 0, crossIconY_ = 0; + // All the sprites, stacked vertically, RGBA8888. + std::vector atlas_; + int atlasW_ = 0; + int atlasH_ = 0; + Draw::Texture *texture_ = nullptr; +}; + +namespace { + +constexpr int PSP_W = 480; +constexpr int PSP_H = 272; +constexpr uint32_t NUM_PALETTES = 3; +constexpr uint32_t NUM_LANGUAGES = 12; +constexpr uint32_t NUM_TEXT_IMAGES = 10; +constexpr uint32_t TEXT_RECORDS = 0x44; +constexpr int BATTERY_SHARED_IMAGE = 0; +constexpr int CIRCLE_SHARED_IMAGE = 2; +constexpr int CROSS_SHARED_IMAGE = 3; +constexpr int LINE_SHARED_IMAGE = 8; +constexpr int DIGITS_SHARED_IMAGE = 10; +constexpr int VOLUME_SHARED_IMAGE = 16; +constexpr int SPEAKER_SHARED_IMAGE = 18; +constexpr int SEPARATORS_SHARED_IMAGE = 12; +constexpr int AMPM_SHARED_IMAGE = 14; +constexpr int ICON_SIZE = 16; + +uint32_t ABGR1555ToRGBA(uint16_t c) { + uint32_t r = ((c >> 0) & 31) * 255 / 31; + uint32_t g = ((c >> 5) & 31) * 255 / 31; + uint32_t b = ((c >> 10) & 31) * 255 / 31; + uint32_t a = (c & 0x8000) ? 255 : 0; + return r | (g << 8) | (b << 16) | (a << 24); +} + +// Bounds-checked reads from the file. +struct Reader { + const std::vector &data; + bool ok = true; + + uint32_t U32(size_t offset) { + if (offset + 4 > data.size()) { + ok = false; + return 0; + } + uint32_t v; + memcpy(&v, &data[offset], 4); + return v; + } + uint16_t U16(size_t offset) { + if (offset + 2 > data.size()) { + ok = false; + return 0; + } + return (uint16_t)(data[offset] | (data[offset + 1] << 8)); + } + bool Has(size_t offset, size_t size) { + if (offset > data.size() || size > data.size() - offset) { + ok = false; + } + return ok; + } +}; + +} // namespace + +ImposeArt::~ImposeArt() { + DeviceLost(); +} + +void ImposeArt::DeviceLost() { + if (texture_) { + texture_->Release(); + texture_ = nullptr; + } +} + +bool ImposeArt::Load(int pspLanguage) { + if (pspLanguage < 0 || pspLanguage >= (int)NUM_LANGUAGES) { + pspLanguage = 1; // English. + } + if (tried_ && language_ == pspLanguage) { + return loaded_; + } + tried_ = true; + language_ = pspLanguage; + loaded_ = false; + DeviceLost(); + + if (g_Config.nandRootDirectory.empty()) { + return false; + } + const Path path = g_Config.nandRootDirectory / "flash0/kd/resource/impose.rsc"; + std::string contents; + if (!File::ReadBinaryFileToString(path, &contents)) { + INFO_LOG(Log::System, "No %s, drawing the HOME screen ourselves", path.c_str()); + return false; + } + std::vector file(contents.begin(), contents.end()); + loaded_ = Parse(file, pspLanguage); + if (loaded_) { + INFO_LOG(Log::System, "HOME screen: using the firmware's artwork from %s", path.c_str()); + } else { + WARN_LOG(Log::System, "HOME screen: %s isn't in the expected format, drawing it ourselves", path.c_str()); + } + return loaded_; +} + +bool ImposeArt::Parse(const std::vector &file, int pspLanguage) { + Reader r{ file }; + const uint32_t numPalettes = r.U32(0); + const uint32_t numLanguages = r.U32(4); + const uint32_t numShared = r.U32(8); + if (!r.ok || numPalettes != NUM_PALETTES || numLanguages < NUM_LANGUAGES || numShared <= SPEAKER_SHARED_IMAGE || numShared > 256) { + return false; + } + auto tableEntry = [&](uint32_t index) { return r.U32(0x0C + 4 * index); }; + + uint32_t palettes[NUM_PALETTES][16]; + for (uint32_t p = 0; p < NUM_PALETTES; p++) { + const uint32_t offset = tableEntry(p); + for (int i = 0; i < 16; i++) { + palettes[p][i] = ABGR1555ToRGBA(r.U16(offset + 2 * i)); + } + } + + // Decoded sprites, before they go into the atlas. + struct Decoded { + int w = 0, h = 0, x = 0, y = 0; + std::vector pixels; + }; + Decoded decoded[(int)Item::COUNT]; + + const uint32_t block = tableEntry(NUM_PALETTES + pspLanguage); + if (!r.ok || r.U32(block) < NUM_TEXT_IMAGES) { + return false; + } + circleIconX_ = (int)r.U32(block + 4); + circleIconY_ = (int)r.U32(block + 8); + crossIconX_ = (int)r.U32(block + 12); + crossIconY_ = (int)r.U32(block + 16); + if (circleIconX_ > PSP_W - ICON_SIZE || circleIconY_ > PSP_H - ICON_SIZE || crossIconX_ > PSP_W - ICON_SIZE || crossIconY_ > PSP_H - ICON_SIZE) { + return false; + } + + for (uint32_t i = 0; i < NUM_TEXT_IMAGES; i++) { + const uint32_t rec = block + TEXT_RECORDS + 28 * i; + const uint32_t palette = r.U32(rec + 0); + const uint32_t size = r.U32(rec + 4); + const uint32_t x = r.U32(rec + 8); + const uint32_t y = r.U32(rec + 12); + const uint32_t w = r.U32(rec + 16); + const uint32_t h = r.U32(rec + 20); + const uint32_t offset = r.U32(rec + 24); + if (!r.ok || palette >= NUM_PALETTES || w == 0 || h == 0 || x + w > PSP_W || y + h > PSP_H || (w * h + 1) / 2 != size) { + return false; + } + const size_t pixels = block + TEXT_RECORDS + (size_t)offset; + if (!r.Has(pixels, size)) { + return false; + } + Decoded &d = decoded[i]; + d.w = (int)w; + d.h = (int)h; + d.x = (int)x; + d.y = (int)y; + d.pixels.resize(w * h); + for (uint32_t p = 0; p < w * h; p++) { + const uint8_t byte = file[pixels + p / 2]; + d.pixels[p] = palettes[palette][(p & 1) ? (byte >> 4) : (byte & 0xF)]; + } + } + + // Shared images, cut into the frames they hold (stacked vertically). + struct SharedImage { + int index; + int w, h; // Of one frame. + int frames; + Item first; + }; + static const SharedImage sharedImages[] = { + { BATTERY_SHARED_IMAGE, 28, 17, 4, Item::BATTERY_FULL }, + { CIRCLE_SHARED_IMAGE, ICON_SIZE, ICON_SIZE, 1, Item::CIRCLE_ICON }, + { CROSS_SHARED_IMAGE, ICON_SIZE, ICON_SIZE, 1, Item::CROSS_ICON }, + { DIGITS_SHARED_IMAGE, 16, 15, 10, Item::DIGIT_0 }, + { SEPARATORS_SHARED_IMAGE, 12, 15, 2, Item::SLASH }, + { AMPM_SHARED_IMAGE, 32, 16, 2, Item::AM }, + { LINE_SHARED_IMAGE, 328, 9, 1, Item::LINE }, + { VOLUME_SHARED_IMAGE, 8, 24, 2, Item::VOLUME_SET }, + { SPEAKER_SHARED_IMAGE, 32, 28, 3, Item::SPEAKER }, + }; + for (const SharedImage &img : sharedImages) { + if ((uint32_t)img.index >= numShared) { + return false; + } + const uint32_t offset = tableEntry(NUM_PALETTES + numLanguages + img.index); + const uint32_t frameBytes = (uint32_t)(img.w * img.h * 2); + // The speaker has more frames than we use (only the first). + const int frames = img.first == Item::SPEAKER ? 1 : img.frames; + if (!r.ok || r.U32(offset) != frameBytes * img.frames || !r.Has(offset + 4, frameBytes * img.frames)) { + return false; + } + for (int f = 0; f < frames; f++) { + Decoded &d = decoded[(int)img.first + f]; + d.w = img.w; + d.h = img.h; + d.pixels.resize(img.w * img.h); + const uint32_t frame = offset + 4 + frameBytes * f; + for (int p = 0; p < img.w * img.h; p++) { + d.pixels[p] = ABGR1555ToRGBA(r.U16(frame + 2 * p)); + } + } + } + if (!r.ok) { + return false; + } + + // Stack them into one texture, a pixel apart so linear filtering doesn't bleed. + atlasW_ = 0; + atlasH_ = 0; + for (const Decoded &d : decoded) { + atlasW_ = std::max(atlasW_, d.w); + atlasH_ += d.h + 1; + } + atlas_.assign((size_t)atlasW_ * atlasH_, 0); + int y = 0; + for (int i = 0; i < (int)Item::COUNT; i++) { + const Decoded &d = decoded[i]; + for (int row = 0; row < d.h; row++) { + memcpy(&atlas_[(size_t)(y + row) * atlasW_], &d.pixels[(size_t)row * d.w], d.w * 4); + } + sprites_[i] = Sprite{ y, d.w, d.h, d.x, d.y }; + y += d.h + 1; + } + return true; +} + +Draw::Texture *ImposeArt::GetTexture(Draw::DrawContext *draw) { + if (!texture_ && loaded_) { + Draw::TextureDesc desc{}; + desc.type = Draw::TextureType::LINEAR2D; + desc.format = Draw::DataFormat::R8G8B8A8_UNORM; + desc.width = atlasW_; + desc.height = atlasH_; + desc.depth = 1; + desc.mipLevels = 1; + desc.generateMips = false; + desc.tag = "impose_home"; + desc.initData.push_back((const uint8_t *)atlas_.data()); + texture_ = draw->CreateTexture(desc); + } + return texture_; +} + +Bounds ImposeArt::ItemBounds(Item item, const Bounds &screen) const { + const Sprite &s = sprites_[(int)item]; + const float sx = screen.w / PSP_W; + const float sy = screen.h / PSP_H; + return Bounds(screen.x + s.x * sx, screen.y + s.y * sy, s.w * sx, s.h * sy); +} + +Bounds ImposeArt::IconBounds(bool circle, const Bounds &screen) const { + const float sx = screen.w / PSP_W; + const float sy = screen.h / PSP_H; + const int x = circle ? circleIconX_ : crossIconX_; + const int y = circle ? circleIconY_ : crossIconY_; + return Bounds(screen.x + x * sx, screen.y + y * sy, ICON_SIZE * sx, ICON_SIZE * sy); +} + +Bounds ImposeArt::LabelBounds(Item label, bool circle, const Bounds &screen) const { + const Sprite &s = sprites_[(int)label]; + const float sx = screen.w / PSP_W; + const float sy = screen.h / PSP_H; + const int x = (circle ? circleIconX_ : crossIconX_) + ICON_SIZE; + return Bounds(screen.x + x * sx, screen.y + s.y * sy, s.w * sx, s.h * sy); +} + +void ImposeArt::Draw(UIContext &dc, Item item, const Bounds &where) { + Draw::Texture *texture = GetTexture(dc.GetDrawContext()); + if (!texture) { + return; + } + const Sprite &s = sprites_[(int)item]; + dc.Flush(); + dc.GetDrawContext()->BindTexture(0, texture); + const float u1 = 0.0f; + const float u2 = (float)s.w / atlasW_; + const float v1 = (float)s.atlasY / atlasH_; + const float v2 = (float)(s.atlasY + s.h) / atlasH_; + dc.Draw()->DrawTexRect(where, u1, v1, u2, v2, 0xFFFFFFFF); + dc.Flush(); + dc.RebindTexture(); +} + +namespace { + +// The status bar's glyphs, as impose.prx lays them out: what to draw and how far to move on. +enum class Glyph { DIGIT, SLASH, COLON, SPACE, THIN_SPACE, AM, PM }; + +struct StatusGlyph { + Glyph glyph; + int digit; +}; + +// The firmware builds the string the same way: no leading zero on the month, the day or the hour, +// a 5 pixel space after the date and at the end, a 1 pixel one before AM/PM. +std::vector StatusBarGlyphs(int month, int day, int hour, int minute, bool dayFirst, bool twelveHour) { + std::vector glyphs; + auto number = [&](int n, bool leadingZero) { + if (n / 10 != 0 || leadingZero) { + glyphs.push_back({ Glyph::DIGIT, (n / 10) % 10 }); + } + glyphs.push_back({ Glyph::DIGIT, n % 10 }); + }; + number(dayFirst ? day : month, false); + glyphs.push_back({ Glyph::SLASH, 0 }); + number(dayFirst ? month : day, false); + glyphs.push_back({ Glyph::SPACE, 0 }); + bool pm = false; + if (twelveHour) { + pm = hour >= 12; + hour %= 12; + if (hour == 0) { + hour = 12; + } + } + number(hour, false); + glyphs.push_back({ Glyph::COLON, 0 }); + number(minute, true); + if (twelveHour) { + glyphs.push_back({ Glyph::THIN_SPACE, 0 }); + glyphs.push_back({ pm ? Glyph::PM : Glyph::AM, 0 }); + } + glyphs.push_back({ Glyph::SPACE, 0 }); + return glyphs; +} + +int GlyphAdvance(const StatusGlyph &g) { + switch (g.glyph) { + case Glyph::DIGIT: return g.digit == 1 ? 10 : (g.digit == 2 ? 11 : 12); + case Glyph::SLASH: return 7; + case Glyph::COLON: return 6; + case Glyph::SPACE: return 5; + case Glyph::THIN_SPACE: return 1; + case Glyph::AM: + case Glyph::PM: return 29; + } + return 0; +} + +} // namespace + +int ImposeArt::BatteryLevel(int percent) { + // impose.prx's thresholds. + if (percent >= 60) + return 0; + if (percent >= 30) + return 1; + if (percent >= 12) + return 2; + return 3; +} + +std::string ImposeArt::ClockText(int month, int day, int hour, int minute, bool dayFirst, bool twelveHour) { + std::string text; + for (const StatusGlyph &g : StatusBarGlyphs(month, day, hour, minute, dayFirst, twelveHour)) { + switch (g.glyph) { + case Glyph::DIGIT: text += (char)('0' + g.digit); break; + case Glyph::SLASH: text += '/'; break; + case Glyph::COLON: text += ':'; break; + case Glyph::SPACE: text += ' '; break; + case Glyph::THIN_SPACE: break; + case Glyph::AM: text += " AM"; break; + case Glyph::PM: text += " PM"; break; + } + } + while (!text.empty() && text.back() == ' ') { + text.pop_back(); + } + return text; +} + +void ImposeArt::DrawStatusBar(UIContext &dc, const Bounds &screen, int month, int day, int hour, int minute, bool dayFirst, bool twelveHour, int batteryPercent) { + const float sx = screen.w / PSP_W; + const float sy = screen.h / PSP_H; + auto at = [&](Item item, int x, int y) { + const Sprite &s = sprites_[(int)item]; + Draw(dc, item, Bounds(screen.x + x * sx, screen.y + y * sy, s.w * sx, s.h * sy)); + }; + + // Everything is at y 5. The battery sits at x 450, and the clock ends where it starts, or at + // the screen's edge without one. + const int y = 5; + int right = PSP_W; + if (batteryPercent >= 0) { + right = 450; + at((Item)((int)Item::BATTERY_FULL + BatteryLevel(batteryPercent)), 450, y); + } + + const std::vector glyphs = StatusBarGlyphs(month, day, hour, minute, dayFirst, twelveHour); + int width = 0; + for (const StatusGlyph &g : glyphs) { + width += GlyphAdvance(g); + } + int x = right - width; + for (const StatusGlyph &g : glyphs) { + switch (g.glyph) { + case Glyph::DIGIT: at((Item)((int)Item::DIGIT_0 + g.digit), x, y); break; + case Glyph::SLASH: at(Item::SLASH, x, y); break; + case Glyph::COLON: at(Item::COLON, x, y); break; + case Glyph::AM: at(Item::AM, x, y); break; + case Glyph::PM: at(Item::PM, x, y); break; + default: break; + } + x += GlyphAdvance(g); + } +} + +void ImposeArt::DrawFrame(UIContext &dc, const Bounds &screen, int volume) { + const float sx = screen.w / PSP_W; + const float sy = screen.h / PSP_H; + auto at = [&](Item item, int x, int y) { + const Sprite &s = sprites_[(int)item]; + Draw(dc, item, Bounds(screen.x + x * sx, screen.y + y * sy, s.w * sx, s.h * sy)); + }; + // Where impose.prx leaves them once its opening animation (the lines growing from the + // middle) is done. + at(Item::LINE, 80, 96); + at(Item::LINE, 80, 175); + if (volume >= 0) { + at(Item::SPEAKER, 28, 223); + for (int i = 0; i < 30; i++) { + at(i < volume ? Item::VOLUME_SET : Item::VOLUME_UNSET, 62 + 12 * i, 223); + } + } +} + +// The HOME button's quit screen. On a PSP, impose draws it over a running game: the game keeps +// going but stops getting input, and Yes runs the game's exit callback (or just ends it) and goes +// back to the XMB, showing "Please wait..." meanwhile. Here it goes back to the XMB when the XMB +// launched the game, otherwise to our game browser, like Exit to menu. With a firmware dump +// installed it uses the firmware's own artwork (see ImposeArt), otherwise it's drawn the same +// way with our font. + +// Whether HOME brings up the quit screen: in any game, but not in the XMB itself (where HOME does +// nothing on a PSP), the PS1 emulator (which has its own HOME menu) or a GE dump. +static bool Available() { + return !__KernelIsRunningVSH() && !PopsBootActive() && PSP_CoreParameter().fileType != IdentifiedFileType::PPSSPP_GE_DUMP; +} + +static uint32_t ConfirmButton() { + // The system's confirm button, as impose reports it (see __ImposeInit). + const bool circle = PSP_CoreParameter().compat.flags().ForceCircleButtonConfirm || g_Config.iButtonPreference == PSP_SYSTEMPARAM_BUTTON_CIRCLE; + return circle ? CTRL_CIRCLE : CTRL_CROSS; +} + +// Our game volume (0-100) as the PSP's volume steps (0-30), which the volume bar shows. +static int VolumeStep() { + return (std::clamp(g_Config.iGameVolume, 0, VOLUMEHI_FULL) * 30 + VOLUMEHI_FULL / 2) / VOLUMEHI_FULL; +} + +void ImposeHomeScreen::Open() { + INFO_LOG(Log::System, "HOME: showing the quit screen"); + state_ = State::ASKING; + yes_ = false; + __CtrlSetGameInputBlocked(true); +} + +void ImposeHomeScreen::Close() { + if (state_ != State::CLOSED) { + state_ = State::CLOSED; + __CtrlSetGameInputBlocked(false); + } +} + +void ImposeHomeScreen::Choose(bool quit) { + if (!quit || coreState != CORE_RUNNING_CPU) { + Close(); + return; + } + // Like a PSP: the game's exit callback first, so it can clean up (and usually calls + // sceKernelExitGame itself). Without one, the game is just ended. The game still gets no input + // meanwhile, and "Please wait..." stays up until it's gone. + state_ = State::QUITTING; + if (!__KernelIsExitCallbackPending() && __KernelInvokeRegisteredExitCallback(__KernelCanExitToVSH())) { + INFO_LOG(Log::System, "HOME: quitting, running the game's exit callback"); + deadline_ = time_now_d() + 10.0; + } else { + FinishQuit(); + } +} + +void ImposeHomeScreen::FinishQuit() { + // Back to the XMB if it launched the game, otherwise to the game browser. + if (__KernelRequestExitToVSH()) { + return; + } + INFO_LOG(Log::System, "HOME: quitting to the game browser"); + Close(); + pendingAction_ = Action::LEAVE_GAME; +} + +ImposeHomeScreen::Action ImposeHomeScreen::Update(bool running, bool onTop) { + // A quit chosen by touch since the last update. + if (pendingAction_ != Action::NONE) { + return TakeAction(); + } + if (!running) { + // Paused in the debugger, say, or on the way out: keep showing "Please wait..." while a + // quit is under way, otherwise let go. + if (state_ == State::ASKING || !PSP_IsInited()) { + Close(); + } + lastButtons_ = 0; + if (!PSP_IsInited()) { + deadline_ = -1.0; + } + return Action::NONE; + } + + // A game whose exit callback never gets round to exiting: quit it anyway. + if (deadline_ >= 0.0 && time_now_d() > deadline_) { + deadline_ = -1.0; + WARN_LOG(Log::System, "HOME: the exit callback didn't end the game, quitting it"); + FinishQuit(); + return TakeAction(); + } + + const uint32_t buttons = __CtrlPeekButtons(); + const uint32_t pressed = buttons & ~lastButtons_; + lastButtons_ = buttons; + + if (!onTop) { + // The pause menu or such is up. Leave the quit screen as it was. + return Action::NONE; + } + + switch (state_) { + case State::CLOSED: + if ((pressed & CTRL_HOME) && deadline_ < 0.0 && Available()) { + Open(); + } + break; + case State::ASKING: + { + const uint32_t confirm = ConfirmButton(); + const uint32_t cancel = confirm == CTRL_CIRCLE ? CTRL_CROSS : CTRL_CIRCLE; + if (pressed & (CTRL_LEFT | CTRL_RIGHT)) { + yes_ = !yes_; + } + // The volume keys move the volume bar a step, which is our game volume. + if (pressed & (CTRL_VOL_UP | CTRL_VOL_DOWN)) { + const int step = std::clamp(VolumeStep() + ((pressed & CTRL_VOL_UP) ? 1 : -1), 0, 30); + g_Config.iGameVolume = (step * VOLUMEHI_FULL + 15) / 30; + } + if (pressed & confirm) { + Choose(yes_); + } else if (pressed & (cancel | CTRL_HOME)) { + Choose(false); + } + break; + } + case State::QUITTING: + break; + } + return TakeAction(); +} + +ImposeHomeScreen::Action ImposeHomeScreen::TakeAction() { + Action action = pendingAction_; + pendingAction_ = Action::NONE; + return action; +} + +bool ImposeHomeScreen::Touch(const TouchInput &touch) { + if ((touch.flags & TouchInputFlags::DOWN) && state_ == State::ASKING) { + if (yesBounds_.Contains(touch.x, touch.y)) { + yes_ = true; + Choose(true); + } else if (noBounds_.Contains(touch.x, touch.y)) { + yes_ = false; + Choose(false); + } + } + // Let releases through, so nothing on the touch controls stays held. Everything else is ours. + return !(touch.flags & TouchInputFlags::UP); +} + +void ImposeHomeScreen::Draw(UIContext &dc, const Bounds &screen) { + using Item = ImposeArt::Item; + + const float sx = screen.w / 480.0f; + const float sy = screen.h / 272.0f; + // A rectangle on the PSP screen (480x272). + auto PSPRect = [&](float x, float y, float w, float h) { return Bounds(screen.x + x * sx, screen.y + y * sy, w * sx, h * sy); }; + + const bool asking = state_ == State::ASKING; + const bool circleConfirm = ConfirmButton() == CTRL_CIRCLE; + + // impose covers the whole screen with this, which its text bitmaps are drawn on. + dc.Flush(); + dc.RebindTexture(); + dc.FillRect(UI::Drawable(ImposeArt::BACKGROUND), screen); + + // The status bar: the emulated PSP's date and time, and the battery - this device's, since the + // PSP's is always full. A device without one (a desktop) shows it full, like what games are told. + ScePspDateTime now{}; + const bool haveTime = __RtcGetCurrentLocalTime(&now); + const bool dayFirst = g_Config.iDateFormat == PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY; + const bool twelveHour = g_Config.iTimeFormat == PSP_SYSTEMPARAM_TIME_FORMAT_12HR; + int batteryPercent = 100; + if (System_GetPropertyBool(SYSPROP_CAN_READ_BATTERY_PERCENTAGE)) { + const int64_t percent = System_GetPropertyInt(SYSPROP_BATTERY_PERCENTAGE); + if (percent >= 0 && percent <= 100) { + batteryPercent = (int)percent; + } + } + + // The volume bar shows our game volume, in the PSP's 30 steps. + const int volume = VolumeStep(); + + if (!art_) { + art_ = std::make_unique(); + } + if (art_->Load(GetPSPLanguage())) { + ImposeArt &art = *art_; + if (haveTime) { + art.DrawStatusBar(dc, screen, now.month, now.day, now.hour, now.minute, dayFirst, twelveHour, batteryPercent); + } + yesBounds_ = art.ItemBounds(Item::YES, screen); + noBounds_ = art.ItemBounds(Item::NO, screen); + art.DrawFrame(dc, screen, asking ? volume : -1); + if (asking) { + art.Draw(dc, Item::QUESTION, art.ItemBounds(Item::QUESTION, screen)); + art.Draw(dc, yes_ ? Item::YES_SELECTED : Item::YES, yesBounds_); + art.Draw(dc, yes_ ? Item::NO : Item::NO_SELECTED, noBounds_); + // Cross on the left, circle on the right, the labels by their buttons. + art.Draw(dc, Item::CROSS_ICON, art.IconBounds(false, screen)); + art.Draw(dc, Item::CIRCLE_ICON, art.IconBounds(true, screen)); + art.Draw(dc, Item::ENTER, art.LabelBounds(Item::ENTER, circleConfirm, screen)); + art.Draw(dc, Item::BACK, art.LabelBounds(Item::BACK, !circleConfirm, screen)); + } else { + art.Draw(dc, Item::PLEASE_WAIT, art.ItemBounds(Item::PLEASE_WAIT, screen)); + } + dc.Flush(); + return; + } + + // No firmware: the same screen with our font, at the firmware's positions (English layout). + auto di = GetI18NCategory(I18NCat::DIALOG); + const UI::Theme &theme = dc.GetTheme(); + dc.SetFontStyle(theme.uiFont); + auto FitText = [&](std::string_view text, const Bounds &bounds, uint32_t color, int align) { + // Scale the font to the bitmap's height, then squeeze if the translation is longer. + float w, h; + dc.MeasureText(theme.uiFont, 1.0f, 1.0f, text, &w, &h); + float scale = h > 0.0f ? bounds.h / h : 1.0f; + if (w * scale > bounds.w && (align & ALIGN_HCENTER)) { + scale = std::max(bounds.w / w, scale * 0.5f); + } + dc.SetFontScale(scale, scale); + const float x = (align & ALIGN_HCENTER) ? bounds.centerX() : bounds.x; + dc.DrawText(text, x, bounds.centerY(), color, (align & ALIGN_HCENTER) ? ALIGN_CENTER : (ALIGN_LEFT | ALIGN_VCENTER)); + }; + + // Status bar: the clock right-aligned at the top, the battery as its level in quarters. + float clockRight = 480.0f; + if (batteryPercent >= 0) { + clockRight = 450.0f; + const Bounds battery = PSPRect(452, 6, 24, 15); + const int level = 3 - ImposeArt::BatteryLevel(batteryPercent); // Bars, 0-3. + dc.FillRect(UI::Drawable(0xFFFFFFFF), Bounds(battery.x, battery.y, battery.w, battery.h)); + dc.FillRect(UI::Drawable(ImposeArt::BACKGROUND), Bounds(battery.x + sx, battery.y + sy, battery.w - 2 * sx, battery.h - 2 * sy)); + for (int i = 0; i < level; i++) { + dc.FillRect(UI::Drawable(0xFFFFFFFF), PSPRect(455 + 6.5f * i, 9, 5, 9)); + } + } + if (haveTime) { + const std::string clock = ImposeArt::ClockText(now.month, now.day, now.hour, now.minute, dayFirst, twelveHour); + float w, h; + dc.MeasureText(theme.uiFont, 1.0f, 1.0f, clock, &w, &h); + const float scale = h > 0.0f ? 15.0f * sy / h : 1.0f; + dc.SetFontScale(scale, scale); + dc.DrawText(clock, screen.x + (clockRight - 6.0f) * sx, screen.y + 12.5f * sy, 0xFFFFFFFF, ALIGN_RIGHT | ALIGN_VCENTER); + } + + // The lines above and below the question. + dc.FillRect(UI::Drawable(0xFF9C9C9C), PSPRect(80, 99, 328, 1.5f)); + dc.FillRect(UI::Drawable(0xFF9C9C9C), PSPRect(80, 178, 328, 1.5f)); + + yesBounds_ = PSPRect(190, 146, 32, 18); + noBounds_ = PSPRect(258, 146, 32, 18); + if (asking) { + // Volume bar: the speaker, then a bar per step set and a dot per step not. + const Bounds speaker = PSPRect(28, 223, 32, 28); + dc.Draw()->DrawImage(ImageID(volume > 0 ? "I_SPEAKER" : "I_SPEAKER_OFF"), speaker.centerX(), speaker.y + 12.0f * sy, 22.0f * sy / 32.0f, 0xFFFFFFFF, ALIGN_CENTER); + for (int i = 0; i < 30; i++) { + if (i < volume) { + dc.FillRect(UI::Drawable(0xFFFFFFFF), PSPRect(64.5f + 12 * i, 228, 3, 14)); + } else { + dc.FillRect(UI::Drawable(0xFFC5C5C5), PSPRect(65 + 12 * i, 239, 2, 2)); + } + } + + FitText(di->T("Do you want to quit the game?"), PSPRect(40, 112, 400, 19), 0xFFFFFFFF, ALIGN_HCENTER); + const Bounds &selected = yes_ ? yesBounds_ : noBounds_; + dc.FillRect(UI::Drawable(0xFFA4A4A4), selected); + FitText(di->T("Yes"), yesBounds_, 0xFFFFFFFF, ALIGN_HCENTER); + FitText(di->T("No"), noBounds_, 0xFFFFFFFF, ALIGN_HCENTER); + + // Button guide, as the firmware has it: cross on the left, circle on the right, each + // labelled with what it does. + const float iconScale = 16.0f * sy / 32.0f; // Atlas button icons are about 32 high. + const Bounds crossIcon = PSPRect(188, 254, 16, 16); + const Bounds circleIcon = PSPRect(266, 254, 16, 16); + dc.Draw()->DrawImage(ImageID("I_CROSS"), crossIcon.centerX(), crossIcon.centerY(), iconScale, 0xFFFFFFFF, ALIGN_CENTER); + FitText(circleConfirm ? di->T("Back") : di->T("Enter"), PSPRect(204, 254, 60, 14), 0xFFFFFFFF, ALIGN_LEFT); + dc.Draw()->DrawImage(ImageID("I_CIRCLE"), circleIcon.centerX(), circleIcon.centerY(), iconScale, 0xFFFFFFFF, ALIGN_CENTER); + FitText(circleConfirm ? di->T("Enter") : di->T("Back"), PSPRect(282, 254, 60, 14), 0xFFFFFFFF, ALIGN_LEFT); + } else { + FitText(di->T("Please wait..."), PSPRect(120, 128, 240, 16), 0xFFFFFFFF, ALIGN_HCENTER); + } + + dc.SetFontScale(1.0f, 1.0f); + dc.Flush(); +} + +ImposeHomeScreen::ImposeHomeScreen() {} + +ImposeHomeScreen::~ImposeHomeScreen() { + Close(); +} + +void ImposeHomeScreen::DeviceLost() { + if (art_) { + art_->DeviceLost(); + } +} diff --git a/UI/ImposeHome.h b/UI/ImposeHome.h new file mode 100644 index 0000000000..5469841a63 --- /dev/null +++ b/UI/ImposeHome.h @@ -0,0 +1,78 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// This program 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 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include + +#include "Common/Math/geom2d.h" + +class UIContext; +struct TouchInput; +class ImposeArt; + +// The PSP's HOME screen ("Do you want to quit the game?"), which impose.prx draws over a running +// game. Owned by EmuScreen, which feeds it input and frames and draws it over the PSP display. +// See ImposeHome.cpp for how it follows the firmware, and the firmware art it uses when there is +// a firmware dump. +class ImposeHomeScreen { +public: + ImposeHomeScreen(); + ~ImposeHomeScreen(); + + // What EmuScreen has to do after an update. + enum class Action { + NONE, + LEAVE_GAME, // Quit to the game browser, like Exit to menu. + }; + + // Once a frame. running: the game is running (booted, CPU running, no reboot pending). + // onTop: EmuScreen is the top screen, so the HOME screen may take input. + Action Update(bool running, bool onTop); + // Touch input while open. True if it took it. + bool Touch(const TouchInput &touch); + // screen: where the PSP's 480x272 display is, in UI coordinates. + void Draw(UIContext &dc, const Bounds &screen); + bool IsOpen() const { return state_ != State::CLOSED; } + void Close(); + void DeviceLost(); + +private: + enum class State { + CLOSED, + ASKING, // Yes / No. + QUITTING, // "Please wait...", while the game's exit callback runs. + }; + + void Open(); + void Choose(bool quit); + void FinishQuit(); + Action TakeAction(); + + State state_ = State::CLOSED; + bool yes_ = false; // Which option is highlighted. + uint32_t lastButtons_ = 0; + // After choosing Yes, while the game's exit callback runs: when to stop waiting and quit anyway. + double deadline_ = -1.0; + Action pendingAction_ = Action::NONE; + // Where Yes and No were last drawn, for touch. + Bounds yesBounds_{}; + Bounds noBounds_{}; + // The firmware's own artwork, when a firmware dump is installed. + std::unique_ptr art_; +}; diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 2e30350ef2..7138c7ca5c 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -42,6 +42,7 @@ + @@ -114,6 +115,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index ee9c3dc0b1..9400ae2055 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -4,6 +4,9 @@ + + Screens + Screens @@ -195,6 +198,9 @@ + + Screens + Screens diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index d8c493d884..a0fd6a2cb5 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -101,6 +101,7 @@ + @@ -167,6 +168,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index 2f3d51c119..07c875bb9b 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -59,6 +59,9 @@ Screens + + Screens + Screens @@ -242,6 +245,9 @@ Screens + + Screens + Screens diff --git a/Windows/main.cpp b/Windows/main.cpp index 17e84ad7c5..827e7b01d0 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -401,9 +401,13 @@ int64_t System_GetPropertyInt(SystemProperty prop) { { SYSTEM_POWER_STATUS status{}; if (GetSystemPowerStatus(&status)) { + // -1 without a battery (a desktop), like SDL_GetPowerInfo. + if (status.BatteryFlag == 128 || status.BatteryFlag == 255) { + return -1; + } return status.BatteryLifePercent < 255 ? status.BatteryLifePercent : 100; } else { - return 100; + return -1; } } default: diff --git a/android/jni/Android.mk b/android/jni/Android.mk index e6c86ecbb4..166990834d 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1054,6 +1054,7 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/DriverManagerScreen.cpp \ $(SRC)/UI/DisplayLayoutScreen.cpp \ $(SRC)/UI/EmuScreen.cpp \ + $(SRC)/UI/ImposeHome.cpp \ $(SRC)/UI/GameBrowser.cpp \ $(SRC)/UI/MainScreen.cpp \ $(SRC)/UI/TabbedDialogScreen.cpp \ diff --git a/assets/lang/ar_AE.ini b/assets/lang/ar_AE.ini index 7cf36b6395..acd4436f1c 100644 --- a/assets/lang/ar_AE.ini +++ b/assets/lang/ar_AE.ini @@ -469,6 +469,7 @@ Details = معلومات Disable All = ‎عطل الكل Disabled = معطل Dismiss = ‎إلغاء +Do you want to quit the game? = Do you want to quit the game? Done! = !تم Download = ‎تحميل Dumps = التفريغات # AI translated @@ -518,6 +519,7 @@ Old savedata detected = ‎بيانات حفظ قديمة قد إكتشفت Options = ‎إعدادات Password = كلمة السر Playing on TV = اللعب على التلفاز # AI translated +Please wait... = Please wait... Remove = إزالة Rename state = إعادة تسمية الحالة # AI translated Reset = ‎إستعادة diff --git a/assets/lang/az_AZ.ini b/assets/lang/az_AZ.ini index ac2ab919fd..bdd315d6d9 100644 --- a/assets/lang/az_AZ.ini +++ b/assets/lang/az_AZ.ini @@ -460,6 +460,7 @@ Details = Ayrıntılar Disable All = Hamısını bağla Disabled = Bağlanıb Dismiss = Rədd et +Do you want to quit the game? = Do you want to quit the game? Done! = Oldu! Download = Endir Dumps = Boşalışlar @@ -509,6 +510,7 @@ Old savedata detected = Köhnə qorunuş tapıldı Options = Seçimlər Password = Keçid sözü Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Qaldır Rename state = Durumu yenidən adlandır # AI translated Reset = Sıfırla diff --git a/assets/lang/be_BY.ini b/assets/lang/be_BY.ini index ae0566a016..24b04e5c70 100644 --- a/assets/lang/be_BY.ini +++ b/assets/lang/be_BY.ini @@ -459,6 +459,7 @@ Details = Падрабязнасці Disable All = Disable all Disabled = Disabled Dismiss = Адхіліць +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Спампаваць Dumps = Dumps @@ -508,6 +509,7 @@ Old savedata detected = Old savedata detected Options = Опцыі Password = Пароль Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Выдаліць Rename state = Перайменаваць стан # AI translated Reset = Скінуць diff --git a/assets/lang/bg_BG.ini b/assets/lang/bg_BG.ini index 08940fb988..4b44124220 100644 --- a/assets/lang/bg_BG.ini +++ b/assets/lang/bg_BG.ini @@ -461,6 +461,7 @@ Details = Детайли Disable All = Деактивиране на всички Disabled = Деактивирано Dismiss = Откажи +Do you want to quit the game? = Do you want to quit the game? Done! = Готово! Download = Свали Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Old savedata detected Options = Опции Password = Парола Playing on TV = Играе се на телевизора # AI translated +Please wait... = Please wait... Remove = Премахване Rename state = Преименувай състоянието # AI translated Reset = Нулиране diff --git a/assets/lang/ca_ES.ini b/assets/lang/ca_ES.ini index 07df1172d5..c097054dff 100644 --- a/assets/lang/ca_ES.ini +++ b/assets/lang/ca_ES.ini @@ -461,6 +461,7 @@ Details = Detalls Disable All = Desactivar tot Disabled = Disabled Dismiss = Descartar +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Descarregar Dumps = Volcats @@ -510,6 +511,7 @@ Old savedata detected = S'han detectar dades del joc antigues. Options = Opcions Password = Password Playing on TV = Jugant a la TV # AI translated +Please wait... = Please wait... Remove = Borrar Rename state = Reanomena l'estat # AI translated Reset = Reiniciar diff --git a/assets/lang/cz_CZ.ini b/assets/lang/cz_CZ.ini index 7da47b0c7c..f92b9e5c72 100644 --- a/assets/lang/cz_CZ.ini +++ b/assets/lang/cz_CZ.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Později +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Stáhnout Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Zjištěna stará uložená data Options = Volby Password = Password Playing on TV = Hra běží na televizi # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Přejmenovat stav # AI translated Reset = Resetovat diff --git a/assets/lang/da_DK.ini b/assets/lang/da_DK.ini index 3830edabf4..bb8c915cb9 100644 --- a/assets/lang/da_DK.ini +++ b/assets/lang/da_DK.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Afvis +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Download Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Gamle savedata fundet Options = Optioner Password = Password Playing on TV = Spiller på tv'et # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Omdøb spil-status # AI translated Reset = Nulstil diff --git a/assets/lang/de_DE.ini b/assets/lang/de_DE.ini index d7a849db71..114ba06c2b 100644 --- a/assets/lang/de_DE.ini +++ b/assets/lang/de_DE.ini @@ -460,6 +460,7 @@ Details = Details Disable All = Alle deaktivieren Disabled = Deaktiviert Dismiss = Schließen +Do you want to quit the game? = Do you want to quit the game? Done! = Fertig! Download = Herunterladen Dumps = Dumps @@ -509,6 +510,7 @@ Old savedata detected = Alte Speicherdaten entdeckt Options = Optionen Password = Passwort Playing on TV = Spiel läuft auf dem Fernseher # AI translated +Please wait... = Please wait... Remove = Entfernen Rename state = Spielstand umbenennen # AI translated Reset = Zurücksetzen diff --git a/assets/lang/dr_ID.ini b/assets/lang/dr_ID.ini index 1dbaf390f1..aa9682acbc 100644 --- a/assets/lang/dr_ID.ini +++ b/assets/lang/dr_ID.ini @@ -459,6 +459,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Dismiss +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Download Dumps = Dumps @@ -508,6 +509,7 @@ Old savedata detected = Old savedata detected Options = Options Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Remove Rename state = Rename state Reset = Reset diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 732dc12087..b081b7c98d 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -485,6 +485,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Dismiss +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Download Dumps = Dumps @@ -534,6 +535,7 @@ Old savedata detected = Old savedata detected Options = Options Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Remove Rename state = Rename state Reset = Reset diff --git a/assets/lang/es_ES.ini b/assets/lang/es_ES.ini index d75448906b..46207b44e1 100644 --- a/assets/lang/es_ES.ini +++ b/assets/lang/es_ES.ini @@ -462,6 +462,7 @@ Details = Detalles Disable All = Desactivar todo Disabled = Desactivado Dismiss = Cancelar +Do you want to quit the game? = Do you want to quit the game? Done! = ¡Hecho! Download = Descargar Dumps = Volcados @@ -511,6 +512,7 @@ Old savedata detected = Encontrado guardado previo. Options = Opciones Password = Contraseña Playing on TV = Jugando en la TV # AI translated +Please wait... = Please wait... Remove = Borrar Rename state = Renombrar estado # AI translated Reset = Restablecer diff --git a/assets/lang/es_LA.ini b/assets/lang/es_LA.ini index 15cbef2247..aab3bfa250 100644 --- a/assets/lang/es_LA.ini +++ b/assets/lang/es_LA.ini @@ -461,6 +461,7 @@ Details = Detalles Disable All = Deshabilitar todo Disabled = Deshabilitado Dismiss = Cancelar +Do you want to quit the game? = Do you want to quit the game? Done! = ¡Hecho! Download = Descargar Dumps = Volcados/dumpeos @@ -510,6 +511,7 @@ Old savedata detected = Se han detectado datos del juego antiguos. Options = Opciones Password = Contraseña Playing on TV = Jugando en la TV # AI translated +Please wait... = Please wait... Remove = Remover Rename state = Renombrar estado # AI translated Reset = Reiniciar diff --git a/assets/lang/fa_IR.ini b/assets/lang/fa_IR.ini index 8d6930a534..a7c88b1c58 100644 --- a/assets/lang/fa_IR.ini +++ b/assets/lang/fa_IR.ini @@ -460,6 +460,7 @@ Details = جزئیات Disable All = غیر فعال کردن همه؟ Disabled = Disabled Dismiss = رد کردن +Do you want to quit the game? = Do you want to quit the game? Done! = انجام شد! Download = بارگیری Dumps = Dumps @@ -509,6 +510,7 @@ Old savedata detected = Old savedata detected Options = تنظیمات Password = رمز عبور Playing on TV = Playing on TV +Please wait... = Please wait... Remove = حذف Rename state = تغییر نام وضعیت # AI translated Reset = بازیابی diff --git a/assets/lang/fi_FI.ini b/assets/lang/fi_FI.ini index d736daf5ee..7d36431b9b 100644 --- a/assets/lang/fi_FI.ini +++ b/assets/lang/fi_FI.ini @@ -461,6 +461,7 @@ Details = Yksityiskohdat Disable All = Poista kaikki käytöstä Disabled = Poistettu käytöstä Dismiss = Hylkää +Do you want to quit the game? = Do you want to quit the game? Done! = Valmis! Download = Lataa Dumps = Dumppaukset @@ -510,6 +511,7 @@ Old savedata detected = Vanha tallennustieto havaittu Options = Asetukset Password = Salasana Playing on TV = Pelataan televisiossa # AI translated +Please wait... = Please wait... Remove = Poista Rename state = Nimeä tila uudelleen # AI translated Reset = Nollaa diff --git a/assets/lang/fr_FR.ini b/assets/lang/fr_FR.ini index ad63e4e33c..0b52184b36 100644 --- a/assets/lang/fr_FR.ini +++ b/assets/lang/fr_FR.ini @@ -485,6 +485,7 @@ Details = Détails Disable All = Tout désactiver Disabled = Désactivé Dismiss = Ignorer +Do you want to quit the game? = Do you want to quit the game? Done! = Terminé! Download = Télécharger Dumps = Dumps @@ -534,6 +535,7 @@ Old savedata detected = Ancienne sauvegarde détectée Options = Options Password = Password Playing on TV = Jeu en cours sur la TV # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Renommer l'état # AI translated Reset = Remise à 0 diff --git a/assets/lang/gl_ES.ini b/assets/lang/gl_ES.ini index 28b72d2a84..8cec42e9b6 100644 --- a/assets/lang/gl_ES.ini +++ b/assets/lang/gl_ES.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Cancelar +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Descargar Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Detectados datos de gardado antigos. Options = Options Password = Password Playing on TV = Xogando na TV # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Renomear estado # AI translated Reset = Reiniciar diff --git a/assets/lang/gr_EL.ini b/assets/lang/gr_EL.ini index 807dbe8343..3e7856c828 100644 --- a/assets/lang/gr_EL.ini +++ b/assets/lang/gr_EL.ini @@ -461,6 +461,7 @@ Details = Details Disable All = ΑΠΕΝΕΡΓΟΠΟΙΗΣΗ ΟΛΩΝ Disabled = Disabled Dismiss = Απόρριψη +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Κατέβασμα Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Εντοπίστηκαν παλαιά αρχεία απ Options = ΕΠΙΛΟΓΕΣ Password = Password Playing on TV = Παίζεται στην τηλεόραση # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Μετονομασία κατάστασης # AI translated Reset = ΕΠΑΝΑΦΟΡΑ diff --git a/assets/lang/he_IL.ini b/assets/lang/he_IL.ini index 48a3a04748..bcb0be8e10 100644 --- a/assets/lang/he_IL.ini +++ b/assets/lang/he_IL.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Dismiss +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Download Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Old savedata detected Options = Options Password = Password Playing on TV = משחק בטלוויזיה # AI translated +Please wait... = Please wait... Remove = Remove Rename state = שנה שם למצב # AI translated Reset = Reset diff --git a/assets/lang/hr_HR.ini b/assets/lang/hr_HR.ini index f5145762cd..ffd16eb080 100644 --- a/assets/lang/hr_HR.ini +++ b/assets/lang/hr_HR.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Isključi sve Disabled = Disabled Dismiss = Zanemari +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Instaliraj Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Uočena stara savedata Options = Opcije Password = Password Playing on TV = Igra se na TV-u # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Preimenuj state # AI translated Reset = Reset diff --git a/assets/lang/hu_HU.ini b/assets/lang/hu_HU.ini index 2d440b1c8d..8b6ff63ce0 100644 --- a/assets/lang/hu_HU.ini +++ b/assets/lang/hu_HU.ini @@ -461,6 +461,7 @@ Details = Részletek Disable All = Összes kikapcsolása Disabled = Kikapcsolva Dismiss = Elvetés +Do you want to quit the game? = Do you want to quit the game? Done! = Kész! Download = Letöltés Dumps = Kiírt fájlok @@ -510,6 +511,7 @@ Old savedata detected = Régi mentés észlelve Options = Opciók Password = Password Playing on TV = Játék a tévén # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Állapot átnevezése # AI translated Reset = Visszaállít diff --git a/assets/lang/id_ID.ini b/assets/lang/id_ID.ini index 256a37b267..e62cac9403 100644 --- a/assets/lang/id_ID.ini +++ b/assets/lang/id_ID.ini @@ -461,6 +461,7 @@ Details = Rincian Disable All = Nonaktifkan semua Disabled = Disabled Dismiss = Abaikan +Do you want to quit the game? = Do you want to quit the game? Done! = Selesai! Download = Unduh Dumps = Buang @@ -510,6 +511,7 @@ Old savedata detected = Simpanan data lama terdeteksi Options = Pilihan Password = Kata sandi Playing on TV = Bermain di TV # AI translated +Please wait... = Please wait... Remove = Menghapus Rename state = Ganti nama savestate # AI translated Reset = Atur ulang diff --git a/assets/lang/it_IT.ini b/assets/lang/it_IT.ini index 6188a84875..c836174f92 100644 --- a/assets/lang/it_IT.ini +++ b/assets/lang/it_IT.ini @@ -461,6 +461,7 @@ Details = Dettagli Disable All = Disattiva tutto Disabled = Disabilitato Dismiss = Ignora +Do you want to quit the game? = Do you want to quit the game? Done! = Fatto! Download = Scarica Dumps = Dump @@ -510,6 +511,7 @@ Old savedata detected = Rilevati vecchi dati salvati Options = Opzioni Password = Password Playing on TV = Gioco in corso sulla TV # AI translated +Please wait... = Please wait... Remove = Rimuovi Rename state = Rinomina stato # AI translated Reset = Reset diff --git a/assets/lang/ja_JP.ini b/assets/lang/ja_JP.ini index 05949e6447..c61aada65a 100644 --- a/assets/lang/ja_JP.ini +++ b/assets/lang/ja_JP.ini @@ -461,6 +461,7 @@ Details = 詳細 Disable All = 全て無効にする Disabled = 無効 Dismiss = 無視 +Do you want to quit the game? = Do you want to quit the game? Done! = 完了! Download = ダウンロード Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = 古いセーブデータを検出しました Options = オプション Password = パスワード Playing on TV = テレビでプレイ中 # AI translated +Please wait... = Please wait... Remove = 削除 Rename state = ステート名を変更 # AI translated Reset = リセット diff --git a/assets/lang/jv_ID.ini b/assets/lang/jv_ID.ini index cfaef4da41..140223e10e 100644 --- a/assets/lang/jv_ID.ini +++ b/assets/lang/jv_ID.ini @@ -459,6 +459,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Ngilangi +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Unduh Dumps = Dumps @@ -508,6 +509,7 @@ Old savedata detected = Simpenan lawas terdeteksi Options = Opsi Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Remove Rename state = Rename state Reset = Ngreset diff --git a/assets/lang/km_KH.ini b/assets/lang/km_KH.ini index 974dc59d70..8e95c3e0a9 100644 --- a/assets/lang/km_KH.ini +++ b/assets/lang/km_KH.ini @@ -456,6 +456,7 @@ Details =ព័ត៌មានលម្អិត Disable All =បិទទាំងអស់។ Disabled =ពិការ Dismiss =ច្រានចោល +Do you want to quit the game? = Do you want to quit the game? Done! =រួចរាល់! Download =ទាញយក Dumps =ចាក់សំរាម @@ -505,6 +506,7 @@ Old savedata detected =បានរកឃើញទិន្នន័យរក្ Options =ជម្រើស Password =ពាក្យសម្ងាត់ Playing on TV = Playing on TV +Please wait... = Please wait... Remove =ដកចេញ Rename state = Rename state Reset =កំណត់ឡើងវិញ diff --git a/assets/lang/ko_KR.ini b/assets/lang/ko_KR.ini index 680ce76efe..0155d6e01c 100644 --- a/assets/lang/ko_KR.ini +++ b/assets/lang/ko_KR.ini @@ -461,6 +461,7 @@ Details = 상세 Disable All = 모두 비활성화 Disabled = 비활성화 Dismiss = 거절 +Do you want to quit the game? = Do you want to quit the game? Done! = 완료! Download = 다운로드 Dumps = 덤프 @@ -510,6 +511,7 @@ Old savedata detected = 오래된 저장 데이터 감지됨 Options = 옵션 Password = 비밀번호 Playing on TV = TV에서 플레이 중 # AI translated +Please wait... = Please wait... Remove = 제거 Rename state = 저장 상태 이름 변경 # AI translated Reset = 재설정 diff --git a/assets/lang/ku_SO.ini b/assets/lang/ku_SO.ini index 627e50021f..12f2987a8a 100644 --- a/assets/lang/ku_SO.ini +++ b/assets/lang/ku_SO.ini @@ -473,6 +473,7 @@ Details = Details Disable All = لە کارخستنی هەمووی Disabled = لە کارخرا Dismiss = Dismiss +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Download Dumps = Dumps @@ -522,6 +523,7 @@ Old savedata detected = Old savedata detected Options = Options Password = وشەی نهێنی Playing on TV = Playing on TV +Please wait... = Please wait... Remove = لابردن Rename state = Rename state Reset = Reset diff --git a/assets/lang/lo_LA.ini b/assets/lang/lo_LA.ini index 2d2cb98c91..aa89b97a5d 100644 --- a/assets/lang/lo_LA.ini +++ b/assets/lang/lo_LA.ini @@ -459,6 +459,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = ຍົກເລີກ +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = ດາວໂຫຼດ Dumps = Dumps @@ -508,6 +509,7 @@ Old savedata detected = ກວດພົບຂໍ້ມູນເຊບເກົ Options = ໂຕເລືອກ Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Remove Rename state = Rename state Reset = ຕັ້ງຄ່າໃໝ່ diff --git a/assets/lang/lt-LT.ini b/assets/lang/lt-LT.ini index de8ee795fc..b6e90f5202 100644 --- a/assets/lang/lt-LT.ini +++ b/assets/lang/lt-LT.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Atmesti +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Parsisiūsti Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Aptikti seniau išsaugoti duomenys Options = Options Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Remove Rename state = Pervadinti statusą # AI translated Reset = Perkrauti diff --git a/assets/lang/ms_MY.ini b/assets/lang/ms_MY.ini index 6d0ad91abd..1edd61d1b0 100644 --- a/assets/lang/ms_MY.ini +++ b/assets/lang/ms_MY.ini @@ -461,6 +461,7 @@ Details = Butiran Disable All = Nyahaktifkan Semua Disabled = Dinyahaktifkan Dismiss = Abaikan +Do you want to quit the game? = Do you want to quit the game? Done! = Selesai! Download = Muat turun Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Data simpanan lama dikesan Options = Pilihan Password = Kata laluan Playing on TV = Bermain di TV # AI translated +Please wait... = Please wait... Remove = Buang Rename state = Namakan semula keadaan # AI translated Reset = Mula semula diff --git a/assets/lang/nl_NL.ini b/assets/lang/nl_NL.ini index 4a3a351f7e..81c430fff9 100644 --- a/assets/lang/nl_NL.ini +++ b/assets/lang/nl_NL.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Alles uitschakelen Disabled = Disabled Dismiss = Annuleren +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Downloaden Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Oude savedata gedetecteerd Options = Opties Password = Password Playing on TV = Spel speelt op de tv # AI translated +Please wait... = Please wait... Remove = Remove Rename state = State hernoemen # AI translated Reset = Herstellen diff --git a/assets/lang/nn_NO.ini b/assets/lang/nn_NO.ini index 1c30d8f73d..171dfb9d76 100644 --- a/assets/lang/nn_NO.ini +++ b/assets/lang/nn_NO.ini @@ -485,6 +485,7 @@ Details = Detaljar Disable All = Deaktiver alle Disabled = Deaktivert Dismiss = Lukk +Do you want to quit the game? = Do you want to quit the game? Done! = Ferdig! Download = Last ned Dumps = Dumpar @@ -534,6 +535,7 @@ Old savedata detected = Gamle lagringsdata oppdaga Options = Val Password = Passord Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Fjern Rename state = Gi tilstanden nytt namn # AI translated Reset = Tilbakestill diff --git a/assets/lang/no_NO.ini b/assets/lang/no_NO.ini index d6981906e3..d668d849d8 100644 --- a/assets/lang/no_NO.ini +++ b/assets/lang/no_NO.ini @@ -485,6 +485,7 @@ Details = Detaljer Disable All = Deaktiver alle Disabled = Deaktivert Dismiss = Lukk +Do you want to quit the game? = Do you want to quit the game? Done! = Ferdig! Download = Last ned Dumps = Dumper @@ -534,6 +535,7 @@ Old savedata detected = Gamle lagringsdata oppdaget Options = Alternativer Password = Passord Playing on TV = Spiller på TV-en # AI translated +Please wait... = Please wait... Remove = Fjern Rename state = Gi tilstanden nytt navn # AI translated Reset = Tilbakestill diff --git a/assets/lang/pl_PL.ini b/assets/lang/pl_PL.ini index 99e6bfe032..2e6bb4544f 100644 --- a/assets/lang/pl_PL.ini +++ b/assets/lang/pl_PL.ini @@ -461,6 +461,7 @@ Details = Zmiany Disable All = Wyłącz wszystko Disabled = Wyłączono Dismiss = Odrzuć +Do you want to quit the game? = Czy chcesz zakończyć grę? Done! = Ukończono! Download = Pobierz Dumps = Zrzuty @@ -510,6 +511,7 @@ Old savedata detected = Wykryto stary zapis gry Options = Opcje Password = Hasło Playing on TV = Gra na telewizorze # AI translated +Please wait... = Proszę czekać... Remove = Usuń Rename state = Zmień nazwę stanu # AI translated Reset = Resetuj diff --git a/assets/lang/pt_BR.ini b/assets/lang/pt_BR.ini index 7b4ce547fa..83289effc5 100644 --- a/assets/lang/pt_BR.ini +++ b/assets/lang/pt_BR.ini @@ -482,6 +482,7 @@ Details = Detalhes Disable All = Desativar tudo Disabled = Desativado Dismiss = Ignorar +Do you want to quit the game? = Do you want to quit the game? Done! = Concluído! Download = Baixar Dumps = Dumps @@ -531,6 +532,7 @@ Old savedata detected = Dados antigos do save detectados Options = Opções Password = Senha Playing on TV = Jogando na TV # AI translated +Please wait... = Please wait... Remove = Remover Rename state = Renomear o state Reset = Resetar diff --git a/assets/lang/pt_PT.ini b/assets/lang/pt_PT.ini index c5ba077a70..951ecc1134 100644 --- a/assets/lang/pt_PT.ini +++ b/assets/lang/pt_PT.ini @@ -485,6 +485,7 @@ Details = Detalhes Disable All = Desativar tudo Disabled = Desativado Dismiss = Ignorar +Do you want to quit the game? = Do you want to quit the game? Done! = Concluído! Download = Fazer download Dumps = Dumps @@ -534,6 +535,7 @@ Old savedata detected = Dados antigos de salvamento detetados Options = Definições Password = Palavra-passe Playing on TV = A jogar na TV # AI translated +Please wait... = Please wait... Remove = Remover Rename state = Renomear estado # AI translated Reset = Restaurar diff --git a/assets/lang/ro_RO.ini b/assets/lang/ro_RO.ini index f9c4c877f3..ea1ead5279 100644 --- a/assets/lang/ro_RO.ini +++ b/assets/lang/ro_RO.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Disable all Disabled = Disabled Dismiss = Ignoră +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Descarcă Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Date vechi salvate detectate Options = Options Password = Password Playing on TV = Se joacă pe televizor # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Redenumește starea # AI translated Reset = Resetare diff --git a/assets/lang/ru_RU.ini b/assets/lang/ru_RU.ini index 80067abce8..fab77eb918 100644 --- a/assets/lang/ru_RU.ini +++ b/assets/lang/ru_RU.ini @@ -461,6 +461,7 @@ Details = Подробности Disable All = Отключить всё Disabled = Откл. Dismiss = Отклонить +Do you want to quit the game? = Do you want to quit the game? Done! = Завершено! Download = Загрузить Dumps = Дампы @@ -510,6 +511,7 @@ Old savedata detected = Старый формат сохранений обна Options = Опции Password = Пароль Playing on TV = Игра на телевизоре # AI translated +Please wait... = Please wait... Remove = Удалить Rename state = Переименовать состояние # AI translated Reset = Сбросить diff --git a/assets/lang/sv_SE.ini b/assets/lang/sv_SE.ini index 963364aa94..1c7056346e 100644 --- a/assets/lang/sv_SE.ini +++ b/assets/lang/sv_SE.ini @@ -462,6 +462,7 @@ Details = Detaljer Disable All = Inaktivera allt Disabled = Av Dismiss = Stäng +Do you want to quit the game? = Do you want to quit the game? Done! = Klar! Download = Hämta Dumps = Dumpar @@ -511,6 +512,7 @@ Old savedata detected = Gamla sparade data hittades Options = Inställningar Password = Lösenord Playing on TV = Spelar på TV:n # AI translated +Please wait... = Please wait... Remove = Ta bort Rename state = Byt namn på tillstånd # AI translated Reset = Återställ diff --git a/assets/lang/tg_PH.ini b/assets/lang/tg_PH.ini index 6180e398d1..4477e6a561 100644 --- a/assets/lang/tg_PH.ini +++ b/assets/lang/tg_PH.ini @@ -461,6 +461,7 @@ Details = Detalye Disable All = I-disable lahat Disabled = Disabled Dismiss = Dismiss +Do you want to quit the game? = Do you want to quit the game? Done! = Tapos na! Download = Download Dumps = Mga Dump @@ -510,6 +511,7 @@ Old savedata detected = Nahanap ang lumang SAVEDATA Options = Mga Opsyon Password = Password Playing on TV = Playing on TV +Please wait... = Please wait... Remove = Alisin Rename state = Palitan ang pangalan # AI translated Reset = I-Reset diff --git a/assets/lang/th_TH.ini b/assets/lang/th_TH.ini index 0c321ae324..4ab8416979 100644 --- a/assets/lang/th_TH.ini +++ b/assets/lang/th_TH.ini @@ -477,6 +477,7 @@ Details = รายละเอียด Disable All = ปิดใช้งานทั้งหมด Disabled = ปิดการใช้งาน Dismiss = ยกเลิก +Do you want to quit the game? = Do you want to quit the game? Done! = เรียบร้อย! Download = ดาวน์โหลด Dumps = ไฟล์ดั๊มพ์ @@ -526,6 +527,7 @@ Old savedata detected = ตรวจพบข้อมูลเซฟเก่ Options = ตัวเลือก Password = รหัสผ่าน Playing on TV = Playing on TV +Please wait... = Please wait... Remove = ลบออก Rename state = เปลี่ยนชื่อเซฟสเตท # AI translated Reset = รีเซ็ต diff --git a/assets/lang/tr_TR.ini b/assets/lang/tr_TR.ini index 100f4ff11e..51b9fbd984 100644 --- a/assets/lang/tr_TR.ini +++ b/assets/lang/tr_TR.ini @@ -463,6 +463,7 @@ Details = Ayrıntılar Disable All = Tümünü Devre Dışı Bırak Disabled = Devre dışı Dismiss = Reddet +Do you want to quit the game? = Do you want to quit the game? Done! = Tamamlandı! Download = İndir Dumps = Dökümler @@ -512,6 +513,7 @@ Old savedata detected = Eski kayıt bulundu Options = Ayarlar Password = Şifre Playing on TV = TV'de oynanıyor # AI translated +Please wait... = Please wait... Remove = Kaldır Rename state = Durumu yeniden adlandır # AI translated Reset = Sıfırla diff --git a/assets/lang/uk_UA.ini b/assets/lang/uk_UA.ini index 05142d13ef..99fc30b83c 100644 --- a/assets/lang/uk_UA.ini +++ b/assets/lang/uk_UA.ini @@ -461,6 +461,7 @@ Details = Подробиці Disable All = Вимкнути все Disabled = Вимкнено Dismiss = Відхилити +Do you want to quit the game? = Do you want to quit the game? Done! = Готово! Download = Завантажити Dumps = Звалища @@ -510,6 +511,7 @@ Old savedata detected = Виявлено старий формат збереж Options = Опції: Password = Пароль Playing on TV = Гра на телевізорі # AI translated +Please wait... = Please wait... Remove = Видалити Rename state = Перейменувати стан # AI translated Reset = Скинути diff --git a/assets/lang/vi_VN.ini b/assets/lang/vi_VN.ini index 1b4d4ee569..af387e38f6 100644 --- a/assets/lang/vi_VN.ini +++ b/assets/lang/vi_VN.ini @@ -461,6 +461,7 @@ Details = Details Disable All = Vô hiệu hóa tất cả Disabled = Disabled Dismiss = Bỏ qua +Do you want to quit the game? = Do you want to quit the game? Done! = Done! Download = Tải về Dumps = Dumps @@ -510,6 +511,7 @@ Old savedata detected = Phát hiện dữ liệu cũ Options = Tùy chọn Password = Password Playing on TV = Đang chơi trên TV # AI translated +Please wait... = Please wait... Remove = Remove Rename state = Đổi tên save state # AI translated Reset = Mặc định diff --git a/assets/lang/zh_CN.ini b/assets/lang/zh_CN.ini index f11c55fd58..22400ee106 100644 --- a/assets/lang/zh_CN.ini +++ b/assets/lang/zh_CN.ini @@ -461,6 +461,7 @@ Details = 详细内容 Disable All = 全部禁用 Disabled = 禁用 Dismiss = 忽略 +Do you want to quit the game? = Do you want to quit the game? Done! = 完成! Download = 下载 Dumps = 转储 @@ -510,6 +511,7 @@ Old savedata detected = 检测到旧版本的存档数据 Options = 选项 Password = 密码 Playing on TV = 正在电视上游玩 # AI translated +Please wait... = Please wait... Remove = 删除 Rename state = 重命名即时存档 # AI translated Reset = 重置 diff --git a/assets/lang/zh_TW.ini b/assets/lang/zh_TW.ini index 65d04e7280..b88822c1f5 100644 --- a/assets/lang/zh_TW.ini +++ b/assets/lang/zh_TW.ini @@ -461,6 +461,7 @@ Details = 詳細資料 Disable All = 全部停用 Disabled = 已停用 Dismiss = 關閉 +Do you want to quit the game? = Do you want to quit the game? Done! = 完成! Download = 下載 Dumps = 傾印 @@ -510,6 +511,7 @@ Old savedata detected = 偵測到舊版存檔資料 Options = 選項 Password = 密碼 Playing on TV = 正在電視上遊玩 # AI translated +Please wait... = Please wait... Remove = 移除 Rename state = 重新命名存檔 # AI translated Reset = 重設