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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NB244iFpvm14JH2FbwHXeQ
This commit is contained in:
LunaMooandClaude Opus 5.5 committed 2026-10-26 11:39:48 +01:00
1 parent 888abaa97e
commit 90cea302b9
67 files changed
+1217 -12

No files matched your search

+19
View File
@@ -17,6 +17,7 @@
#include <algorithm>
#include <cmath>
#include <atomic>
#include <mutex>
#include <vector>
@@ -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<bool> 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<std::mutex> 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;
+4
View File
@@ -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);
+38
View File
@@ -2762,6 +2762,44 @@ static std::vector<u8> VshmainReturnArgs(const std::vector<u8> &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<VSHLaunch> &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<VSHLaunch> &launchedFromVSH = PSP_CoreParameter().launchedFromVSH;
+7
View File
@@ -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);
+15 -2
View File
@@ -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 };
+3 -2
View File
@@ -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();
+13
View File
@@ -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<ScePspDateTime>::Create(pspTimePtr);
+4
View File
@@ -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);
+3 -4
View File
@@ -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;
+2
View File
@@ -43,6 +43,8 @@ list(APPEND UISource
DisplayLayoutScreen.h
EmuScreen.h
EmuScreen.cpp
ImposeHome.h
ImposeHome.cpp
GameInfoCache.h
GameInfoCache.cpp
IAPScreen.cpp
+4 -2
View File
@@ -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);
}
}
}
+24 -1
View File
@@ -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<bool> umdAutoboot)
: gamePath_(filename), mountIso_(mountIso), umdAutoboot_(umdAutoboot) {
: gamePath_(filename), mountIso_(mountIso), umdAutoboot_(umdAutoboot), imposeHome_(std::make_unique<ImposeHomeScreen>()) {
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);
+5
View File
@@ -18,6 +18,7 @@
#pragma once
#include <list>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -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<ImposeHomeScreen> imposeHome_;
};
bool MustRunBehind();
+884
View File
@@ -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 <algorithm>
#include <cstring>
#include <string>
#include <vector>
#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<uint8_t> &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<uint32_t> 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<uint8_t> &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<uint8_t> 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<uint8_t> &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<uint32_t> 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<StatusGlyph> StatusBarGlyphs(int month, int day, int hour, int minute, bool dayFirst, bool twelveHour) {
std::vector<StatusGlyph> 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<StatusGlyph> 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<ImposeArt>();
}
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();
}
}
+78
View File
@@ -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 <cstdint>
#include <memory>
#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<ImposeArt> art_;
};
+2
View File
@@ -42,6 +42,7 @@
<ClCompile Include="DisplayLayoutScreen.cpp" />
<ClCompile Include="DriverManagerScreen.cpp" />
<ClCompile Include="EmuScreen.cpp" />
<ClCompile Include="ImposeHome.cpp" />
<ClCompile Include="GameBrowser.cpp" />
<ClCompile Include="GameInfoCache.cpp" />
<ClCompile Include="GamepadEmu.cpp" />
@@ -114,6 +115,7 @@
<ClInclude Include="DisplayLayoutScreen.h" />
<ClInclude Include="DriverManagerScreen.h" />
<ClInclude Include="EmuScreen.h" />
<ClInclude Include="ImposeHome.h" />
<ClInclude Include="GameBrowser.h" />
<ClInclude Include="GameInfoCache.h" />
<ClInclude Include="GamepadEmu.h" />
+6
View File
@@ -4,6 +4,9 @@
<ClCompile Include="GameInfoCache.cpp" />
<ClCompile Include="NativeApp.cpp" />
<ClCompile Include="OnScreenDisplay.cpp" />
<ClCompile Include="ImposeHome.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="EmuScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
@@ -195,6 +198,9 @@
<ItemGroup>
<ClInclude Include="GameInfoCache.h" />
<ClInclude Include="OnScreenDisplay.h" />
<ClInclude Include="ImposeHome.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="EmuScreen.h">
<Filter>Screens</Filter>
</ClInclude>
+2
View File
@@ -101,6 +101,7 @@
<ClInclude Include="..\..\UI\DisplayLayoutScreen.h" />
<ClInclude Include="..\..\UI\DriverManagerScreen.h" />
<ClInclude Include="..\..\UI\EmuScreen.h" />
<ClInclude Include="..\..\UI\ImposeHome.h" />
<ClInclude Include="..\..\UI\GameBrowser.h" />
<ClInclude Include="..\..\UI\GameInfoCache.h" />
<ClInclude Include="..\..\UI\GamepadEmu.h" />
@@ -167,6 +168,7 @@
<ClCompile Include="..\..\UI\DisplayLayoutScreen.cpp" />
<ClCompile Include="..\..\UI\DriverManagerScreen.cpp" />
<ClCompile Include="..\..\UI\EmuScreen.cpp" />
<ClCompile Include="..\..\UI\ImposeHome.cpp" />
<ClCompile Include="..\..\UI\GameBrowser.cpp" />
<ClCompile Include="..\..\UI\GameInfoCache.cpp" />
<ClCompile Include="..\..\UI\GamepadEmu.cpp" />
+6
View File
@@ -59,6 +59,9 @@
<ClCompile Include="..\..\UI\DriverManagerScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="..\..\UI\ImposeHome.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="..\..\UI\EmuScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
@@ -242,6 +245,9 @@
<ClInclude Include="..\..\UI\DriverManagerScreen.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="..\..\UI\ImposeHome.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="..\..\UI\EmuScreen.h">
<Filter>Screens</Filter>
</ClInclude>
+5 -1
View File
@@ -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:
+1
View File
@@ -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 \
+2
View File
@@ -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 = ‎إستعادة
+2
View File
@@ -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
+2
View File
@@ -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 = Скінуць
+2
View File
@@ -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 = Нулиране
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 = بازیابی
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 = ΕΠΑΝΑΦΟΡΑ
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 = リセット
+2
View File
@@ -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
+2
View File
@@ -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 =កំណត់ឡើងវិញ
+2
View File
@@ -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 = 재설정
+2
View File
@@ -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
+2
View File
@@ -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 = ຕັ້ງຄ່າໃໝ່
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 = Сбросить
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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 = รีเซ็ต
+2
View File
@@ -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
+2
View File
@@ -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 = Скинути
+2
View File
@@ -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
+2
View File
@@ -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 = 重置
+2
View File
@@ -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 = 重設