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 = 重設