// 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 shouldn't get input meanwhile (TODO: though nothing enforces this?) // 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"); exitDeadline_ = time_now_d() + 4.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 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()) { exitDeadline_ = -1.0; } return Action::NONE; } // A game whose exit callback never gets round to exiting: quit it anyway. if (exitDeadline_ >= 0.0 && time_now_d() > exitDeadline_) { exitDeadline_ = -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) && exitDeadline_ < 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; } 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(); } }