2 Commits
Author SHA1 Message Date
TheRealRetro 6bca99ff6e 0.7.5: the cheat search, your own cheats and the PS5 keyboard (release notes, version) 2026-10-17 21:27:02 -04:00
TheRealRetro 129d055f99 Cheat search, your own cheats and the PS5 keyboard
Cheats in the pause menu gets a cheat search (8, 16 or 32-bit values,
narrowed down step by step, with undo) whose results become cheats or are
set once, and codes typed in. The player's own cheats come first, can be
renamed, changed or deleted (Square; on a list cheat, an own copy), and are
kept in cheats/<ROM file>.custom.txt in the cheat list's format.

Names and codes are typed on the system keyboard (libSceImeDialog). Its
module is loaded before the app leaves the sandbox: afterwards the system
looks for it inside the sandbox and doesn't find it (80020063).

The touchpad in any page the pause menu opened goes back to the game.
2026-10-17 21:25:23 -04:00
20 changed files with 1710 additions and 65 deletions

No files matched your search

+5 -3
View File
@@ -33,7 +33,7 @@
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
PS5_HOST ?= ps5
PS5_PORT ?= 9021
VERSION ?= 0.7.4
VERSION ?= 0.7.5
# param.json's contentVersion, NN.NNN.NNN (0.6.1 -> 00.006.001): the release tag, as in the Homebrew Browser; it
# must also be written in app/sce_sys/param.json (checked below)
CONTENT_VERSION := $(shell echo $(VERSION) | awk -F. '{printf "%02d.%03d.%03d", $$1, $$2, $$3}')
@@ -96,7 +96,7 @@ CORE_SRC := \
CORE_SUB_SRC := md5/md5.c minizip/ioapi.c minizip/unzip.c minizip/zip.c
# the port's side of the core (C)
N64_SRC := n64/sdl/sdl_shim.c n64/core/perf.c n64/core/jit_ps5.c n64/core/vidext_ps5.c n64/core/eventloop_ps5.c n64/core/screenshot_ps5.c \
n64/core/osal_dynlib_static.c
n64/core/osal_dynlib_static.c n64/core/memory_ps5.c
# the plugins
PLUGIN_C_SRC := n64/plugins/rsp_cxd4_ps5.c n64/plugins/rsp_hle_ps5.c n64/plugins/gfx_ps5.c
PLUGIN_CXX_SRC := n64/plugins/audio_ps5.cpp n64/plugins/input_ps5.cpp
@@ -121,9 +121,11 @@ EMU_SRC := coreorbis/main-boot.cpp \
coreorbis/orbis-shims/ProsperoVideo.cpp coreorbis/orbis-shims/ProsperoAudio.cpp \
coreorbis/orbis-shims/ProsperoInput.cpp coreorbis/orbis-shims/ProsperoJailbreak.cpp \
coreorbis/orbis-shims/ProsperoCrash.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp \
coreorbis/orbis-shims/ProsperoKeyboard.cpp \
frontend/fe_emu.cpp frontend/fe_menu.cpp frontend/fe_settings.cpp frontend/fe_text.cpp \
frontend/fe_games.cpp frontend/fe_http.cpp frontend/fe_covers.cpp frontend/fe_shelf.cpp \
frontend/fe_prefetch.cpp frontend/fe_update.cpp frontend/fe_cheats.cpp frontend/fe_draw.cpp
frontend/fe_prefetch.cpp frontend/fe_update.cpp frontend/fe_cheats.cpp frontend/fe_draw.cpp \
frontend/fe_search.cpp
# the payloads: the helper, and the installer (= helper + install)
HELPER_SRC := coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.cpp \
coreorbis/orbis-shims/ProsperoHelper.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp
+16 -3
View File
@@ -169,7 +169,20 @@ game (after a question), Triangle back to the game list, Cross confirm, Circle b
- **cheats**: the GameShark codes of the core's `data/mupencheat.txt` (about 560 game versions, found by the ROM
header's CRCs and country; built into `eboot.bin` compressed, `frontend/fe_cheats.cpp`). Each one is off or on,
or off or one of its choices, with the database's note beside it; the ones left on are on again the next time
(`cheats/<ROM file>.txt`). Codes that need the GameShark's button are left out;
(`cheats/<ROM file>.txt`). Codes that need the GameShark's button are left out. Above the list:
- **Search for a Cheat** (`frontend/fe_search.cpp`): finds where the game keeps a value in its RAM. It starts
from every 8, 16 or 32-bit value, either equal to a number or unknown, and each step keeps the addresses whose
value is (not) the number, went up or down (by the number), changed or didn't, since the step before; Undo
goes one step back. A result becomes a cheat (GameShark 80 / 81 codes keeping the value) or is set once
(`n64/core/memory_ps5.c` reads and writes the RDRAM);
- **Type In Codes**: GameShark codes typed on the PS5's keyboard.
The player's own cheats come first in the list (Square renames one, changes its codes or deletes it; on one of
the database's, Square makes an own copy to change) and are kept in `cheats/<ROM file>.custom.txt` in the
database's format, so codes can be put there from a PC too. Names and codes are typed on the system's on-screen
keyboard (`libSceImeDialog`, `coreorbis/orbis-shims/ProsperoKeyboard.cpp`). Its system module is loaded at
start, before the app leaves the sandbox: afterwards the system looks for modules inside the sandbox and doesn't
find them (`80020063`). Where it doesn't load the app still runs, only without typing;
- speed (normal or fast forward);
- every setting below but the game list's (Continue where you left off, Game list view, Download covers, Check
for updates: Settings on the game list has them);
@@ -311,8 +324,8 @@ ROM, on Linux:
N64PS5_PS5_ROOT=/tmp/n64 N64PS5_HOST_DUMP=150 N64PS5_HOST_MAX_FLIPS=200 build/host/mupen64plus-ps5-host test.z64
```
The run writes flip 150 to a `.ppm` file. `N64PS5_HOST_PAD="frame:buttons;..."` scripts the pad (see
`host/sce_host.cpp`).
The run writes flip 150 to a `.ppm` file. `N64PS5_HOST_PAD="frame:buttons;..."` scripts the pad, and
`N64PS5_HOST_IME="text|text..."` what is typed on the keyboard each time it opens (see `host/sce_host.cpp`).
**Dynarec.** `new_dynarec.c` is built with `-Dmprotect=n64ps5_jit_mprotect` (no change to the core's source),
its NASM stubs are translated to GNU syntax in `n64/core/linkage_x64.S`, and the struct offsets they need are
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "99064",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
"contentVersion": "00.007.004",
"contentVersion": "00.007.005",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+51
View File
@@ -177,6 +177,57 @@ int sceUserServiceInitialize(void* params);
int sceUserServiceGetForegroundUser(int32_t* user);
int sceUserServiceGetLoginUserIdList(int32_t users[4]);
// ---- libSceSysmodule: a system module the app loads while it runs ---------------------------------------
// Its functions are imported like the others, but they work only once the module is loaded (ps5-generals'
// SDL_ps5kbm.c does the same for the keyboard 0x0106 and the mouse 0x00a9, the PS4's numbers).
#define SCE_SYSMODULE_IME_DIALOG 0x0096
int sceSysmoduleLoadModule(uint16_t id);
// ---- libSceImeDialog (the on-screen keyboard; PS4's layout, as shadPS4's ime_dialog.h has it) -------------
// Called only after sceSysmoduleLoadModule(SCE_SYSMODULE_IME_DIALOG) succeeded (ProsperoKeyboard.cpp). Loading
// the .sprx by its path doesn't work from the app (sceKernelLoadStartModule: 80020063).
#define SCE_IME_TYPE_DEFAULT 0
#define SCE_IME_TYPE_BASIC_LATIN 1
#define SCE_IME_TYPE_NUMBER 4
#define SCE_IME_HALIGN_CENTER 1
#define SCE_IME_VALIGN_CENTER 1
#define SCE_IME_DIALOG_MAX_TEXT_LENGTH 2048
#define SCE_IME_DIALOG_STATUS_NONE 0
#define SCE_IME_DIALOG_STATUS_RUNNING 1
#define SCE_IME_DIALOG_STATUS_FINISHED 2
#define SCE_IME_DIALOG_END_STATUS_OK 0
#define SCE_IME_DIALOG_END_STATUS_USER_CANCELED 1
#define SCE_IME_DIALOG_END_STATUS_ABORTED 2
typedef struct SceImeDialogParam
{
int32_t userId;
int32_t type; // SCE_IME_TYPE_*
uint64_t supportedLanguages; // 0: the system's
int32_t enterLabel; // 0: Done
int32_t inputMethod; // 0
void* filter;
uint32_t option;
uint32_t maxTextLength; // characters, the buffer holds one more
uint16_t* inputTextBuffer; // UTF-16: the text to start with, and the result
float posx, posy; // the text field, 1920 x 1080
int32_t horizontalAlignment, verticalAlignment;
const uint16_t* placeholder;
const uint16_t* title;
int8_t reserved[16];
} SceImeDialogParam;
typedef struct SceImeDialogResult
{
int32_t endstatus; // SCE_IME_DIALOG_END_STATUS_*
int32_t reserved[12];
} SceImeDialogResult;
int sceImeDialogInit(const SceImeDialogParam* param, const void* extended);
int sceImeDialogGetStatus(void); // SCE_IME_DIALOG_STATUS_*
int sceImeDialogGetResult(SceImeDialogResult* result);
int sceImeDialogTerm(void);
// ---- libSceNet / libSceNetCtl / libSceSsl / libSceHttp2 (as the SDK's http2_get sample and PS5SX2) ----
int sceNetInit(void);
int sceNetPoolCreate(const char* name, int size, int flags);
+3
View File
@@ -18,6 +18,7 @@
#include "ProsperoCrash.h"
#include "ProsperoInput.h"
#include "ProsperoJailbreak.h"
#include "ProsperoKeyboard.h"
#include "ProsperoNotify.h"
#include "ProsperoSce.h"
#include "ProsperoVideo.h"
@@ -106,6 +107,8 @@ int Run(int argc, char** argv)
// A newer release on GitHub, downloaded now for the same reason (fe_update.h); offered further down.
N64_STAGE(Boot, "update check");
fe::UpdateOffer update = fe::CheckForUpdate();
// the on-screen keyboard's system module (cheat names and codes): out of the sandbox it can't be loaded
ps5keyboard::Preload();
// out of the sandbox, like PS5SX2: before it the app sees no /data and no USB drives
N64_STAGE(Boot, "jailbreak");
@@ -0,0 +1,186 @@
// Mupen64Plus PS5: the system's on-screen keyboard (ProsperoKeyboard.h).
//
// libSceImeDialog's functions are imported like the other system calls, but its module is one the app loads
// itself, with sceSysmoduleLoadModule (ps5-generals' SDL port does the same for the keyboard and mouse
// modules). That has to happen while the app is still in its sandbox: once it left it, a module asked for is
// looked for at the sandbox's path and not found (80020063 on the console, by path or by sysmodule; ps5-native's
// docs/CURL.md, "Elevated apps"), so main-boot.cpp calls Preload() before the jailbreak. Until the module loaded
// its functions aren't called, so where it doesn't load the app still runs, only without typing. The dialog is the
// system's: it draws itself over the app's picture and takes the controller while it is up. sceImeDialogInit
// opens it, sceImeDialogGetStatus says when the player closed it, sceImeDialogGetResult how (Done or canceled),
// and sceImeDialogTerm ends it.
//
// SPDX-License-Identifier: MIT
#include "ProsperoKeyboard.h"
#include "OrbisPaths.h"
#include "ProsperoSce.h"
#include <cstdio>
#include <cstring>
#include <vector>
static_assert(sizeof(SceImeDialogParam) == 0x60, "SceImeDialogParam is 0x60 bytes");
namespace ps5keyboard
{
namespace
{
bool s_tried = false;
int s_load = -1; // sceSysmoduleLoadModule's answer: 0 (or more) loaded
bool Load()
{
if (!s_tried)
{
s_tried = true;
s_load = sceSysmoduleLoadModule(SCE_SYSMODULE_IME_DIALOG);
OrbisLog("[keyboard] sceSysmoduleLoadModule(IME dialog %#x) -> %x", SCE_SYSMODULE_IME_DIALOG, unsigned(s_load));
}
return s_load >= 0;
}
} // namespace
void Preload()
{
Load();
}
namespace
{
// UTF-8 -> UTF-16 (at most max units), and back
std::vector<uint16_t> ToUtf16(const std::string& s, size_t max)
{
std::vector<uint16_t> out;
for (size_t i = 0; i < s.size();)
{
const unsigned char c = (unsigned char)s[i];
uint32_t cp = c;
int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
if (extra)
cp = c & (0x3F >> extra);
i++;
for (; extra > 0 && i < s.size(); extra--, i++)
cp = cp << 6 | ((unsigned char)s[i] & 0x3F);
const size_t need = cp >= 0x10000 ? 2 : 1;
if (out.size() + need > max)
break;
if (cp >= 0x10000)
{
cp -= 0x10000;
out.push_back(uint16_t(0xD800 | (cp >> 10)));
out.push_back(uint16_t(0xDC00 | (cp & 0x3FF)));
}
else
out.push_back(uint16_t(cp));
}
return out;
}
std::string ToUtf8(const uint16_t* s)
{
std::string out;
for (size_t i = 0; s[i]; i++)
{
uint32_t cp = s[i];
if (cp >= 0xD800 && cp < 0xDC00 && s[i + 1] >= 0xDC00 && s[i + 1] < 0xE000)
{
cp = 0x10000 + ((cp - 0xD800) << 10) + (s[i + 1] - 0xDC00);
i++;
}
if (cp < 0x80)
out += char(cp);
else if (cp < 0x800)
{
out += char(0xC0 | (cp >> 6));
out += char(0x80 | (cp & 0x3F));
}
else if (cp < 0x10000)
{
out += char(0xE0 | (cp >> 12));
out += char(0x80 | ((cp >> 6) & 0x3F));
out += char(0x80 | (cp & 0x3F));
}
else
{
out += char(0xF0 | (cp >> 18));
out += char(0x80 | ((cp >> 12) & 0x3F));
out += char(0x80 | ((cp >> 6) & 0x3F));
out += char(0x80 | (cp & 0x3F));
}
}
return out;
}
bool Fail(std::string* error, const char* what, int code)
{
char text[96];
snprintf(text, sizeof(text), "The keyboard did not open (%s %08X)", what, unsigned(code));
OrbisLog("[keyboard] %s", text);
if (error)
*error = text;
return false;
}
} // namespace
bool Edit(const std::string& title, std::string& text, int max_chars, Type type, const std::function<void()>& frame,
std::string* error)
{
if (!Load())
return Fail(error, "module", s_load);
const size_t max = size_t(max_chars < 1 ? 1 : max_chars > SCE_IME_DIALOG_MAX_TEXT_LENGTH ? SCE_IME_DIALOG_MAX_TEXT_LENGTH : max_chars);
std::vector<uint16_t> buffer = ToUtf16(text, max);
buffer.resize(max + 1, 0);
std::vector<uint16_t> title16 = ToUtf16(title, 128);
title16.push_back(0);
SceImeDialogParam param;
memset(&param, 0, sizeof(param));
int32_t user = -1;
sceUserServiceGetForegroundUser(&user);
param.userId = user;
param.type = type == Type::Number ? SCE_IME_TYPE_NUMBER : type == Type::Latin ? SCE_IME_TYPE_BASIC_LATIN : SCE_IME_TYPE_DEFAULT;
param.maxTextLength = uint32_t(max);
param.inputTextBuffer = buffer.data();
param.posx = 960.0f;
param.posy = 540.0f;
param.horizontalAlignment = SCE_IME_HALIGN_CENTER;
param.verticalAlignment = SCE_IME_VALIGN_CENTER;
param.title = title16.data();
int r = sceImeDialogInit(&param, nullptr);
if (r != 0)
{
// one left open (a crash of the menu around it): end that one and try again
sceImeDialogTerm();
r = sceImeDialogInit(&param, nullptr);
}
OrbisLog("[keyboard] sceImeDialogInit(user %x, type %d, max %zu) -> %x", unsigned(user), param.type, max, unsigned(r));
if (r != 0)
return Fail(error, "init", r);
// running until the player closes it (the first frames may still say "none" while it comes up)
int status = SCE_IME_DIALOG_STATUS_RUNNING;
for (int n = 0;; n++)
{
status = sceImeDialogGetStatus();
if (status != SCE_IME_DIALOG_STATUS_RUNNING && !(status == SCE_IME_DIALOG_STATUS_NONE && n < 30))
break;
frame();
}
SceImeDialogResult result;
memset(&result, 0, sizeof(result));
result.endstatus = SCE_IME_DIALOG_END_STATUS_ABORTED;
const int rr = status == SCE_IME_DIALOG_STATUS_FINISHED ? sceImeDialogGetResult(&result) : status;
sceImeDialogTerm();
OrbisLog("[keyboard] closed: status %x, result %x, end %d", unsigned(status), unsigned(rr), int(result.endstatus));
if (status != SCE_IME_DIALOG_STATUS_FINISHED || rr != 0)
return Fail(error, "status", status != SCE_IME_DIALOG_STATUS_FINISHED ? status : rr);
if (result.endstatus != SCE_IME_DIALOG_END_STATUS_OK)
return false;
buffer[max] = 0;
text = ToUtf8(buffer.data());
return true;
}
} // namespace ps5keyboard
@@ -0,0 +1,27 @@
// Mupen64Plus PS5: the system's on-screen keyboard (libSceImeDialog), for naming cheats and typing codes.
// SPDX-License-Identifier: MIT
#pragma once
#include <functional>
#include <string>
namespace ps5keyboard
{
// Loads the keyboard's system module. Before the app leaves the sandbox (main-boot.cpp): afterwards the system
// looks for modules at a path inside the sandbox and doesn't find them (80020063; ps5-native's docs/CURL.md,
// "Elevated apps"). The module stays loaded.
void Preload();
enum class Type
{
Text, // any text: a name
Number, // digits
Latin, // letters and digits: GameShark codes
};
// Opens the keyboard over the picture with `text` in its field, titled `title`, and calls `frame` (show one
// frame) until the player closes it. True with the new text (UTF-8) when the player pressed Done; false when they
// canceled it, or when the keyboard could not open -- then `error` says why ("the keyboard did not open (0x...)").
bool Edit(const std::string& title, std::string& text, int max_chars, Type type, const std::function<void()>& frame,
std::string* error = nullptr);
} // namespace ps5keyboard
+148
View File
@@ -175,6 +175,154 @@ std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::s
return out;
}
std::vector<Cheat> Parse(const std::string& text)
{
std::vector<Cheat> out;
size_t at = 0;
while (at < text.size())
{
size_t end = text.find('\n', at);
if (end == std::string::npos)
end = text.size();
std::string line = text.substr(at, end - at);
at = end + 1;
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
size_t lead = 0;
while (lead < line.size() && (line[lead] == ' ' || line[lead] == '\t'))
lead++;
line.erase(0, lead);
if (line.empty() || line[0] == '#' || line.compare(0, 4, "crc ") == 0 || line.compare(0, 3, "gn ") == 0)
continue;
if (line.compare(0, 3, "cn ") == 0)
{
out.emplace_back();
out.back().name = line.substr(3);
out.back().mine = true;
}
else if (!out.empty() && line.compare(0, 3, "cd ") == 0)
out.back().note = line.substr(3);
else if (!out.empty())
ParseCode(line, out.back());
}
std::vector<Cheat> usable;
for (Cheat& c : out)
if (!c.name.empty() && Usable(c))
usable.push_back(std::move(c));
return usable;
}
std::string Format(const std::vector<Cheat>& list)
{
std::string out = "# Mupen64Plus PS5: your own cheats for this game, in the format of mupen64plus's cheat list.\n"
"# A cheat is a \" cn name\" line, then its codes: an address and a value, \"8033B21D 0064\".\n";
for (const Cheat& c : list)
{
out += " cn " + c.name + "\n";
if (!c.note.empty())
out += " cd " + c.note + "\n";
char code[24];
bool listed = false; // a cheat with choices lists them on its first "????" line
for (size_t i = 0; i < c.addresses.size(); i++)
{
if (c.values[i] >= 0)
snprintf(code, sizeof(code), " %08X %04X", unsigned(c.addresses[i]), unsigned(c.values[i] & 0xFFFF));
else
snprintf(code, sizeof(code), " %08X ????", unsigned(c.addresses[i]));
out += code;
if (c.values[i] < 0 && !listed)
{
listed = true;
for (size_t k = 0; k < c.options.size(); k++)
{
snprintf(code, sizeof(code), "%s%04X:", k ? "," : " ", unsigned(c.options[k].second & 0xFFFF));
out += code + ("\"" + c.options[k].first + "\"");
}
}
out += "\n";
}
}
return out;
}
bool ParseCodes(const std::string& text, Cheat& c, std::string* error)
{
std::vector<std::string> tokens;
std::string cur;
for (const char ch : text + " ")
{
if (ch == ' ' || ch == ',' || ch == '\n' || ch == '\r' || ch == '\t' || ch == ';' || ch == ':' || ch == '+')
{
if (!cur.empty())
tokens.push_back(cur);
cur.clear();
}
else
cur += ch;
}
auto hex = [](const std::string& t, size_t max) {
if (t.empty() || t.size() > max)
return false;
for (const char ch : t)
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')))
return false;
return true;
};
Cheat out;
for (size_t i = 0, step = 2; i < tokens.size(); i += step)
{
// "8033B21D0064" as one token is a code too
std::string address = tokens[i], value = i + 1 < tokens.size() ? tokens[i + 1] : "";
step = 2;
if (address.size() == 12 && hex(address, 12))
{
value = address.substr(8);
address.resize(8);
step = 1;
}
if (address.size() != 8 || !hex(address, 8) || !hex(value, 4))
{
if (error)
*error = "\"" + tokens[i] + (value.empty() ? "" : " " + value) +
"\" is not a code: a code is 8 hex digits and 4 more, like 8033B21D 0064";
return false;
}
out.addresses.push_back(uint32_t(strtoul(address.c_str(), nullptr, 16)));
out.values.push_back(int(strtol(value.c_str(), nullptr, 16)));
}
if (out.addresses.empty())
{
if (error)
*error = "No codes: type them like 8033B21D 0064";
return false;
}
if (NeedsGameSharkButton(out))
{
if (error)
*error = "Codes starting 88, 89, A8, A9 or D8 to DB need the GameShark's button, which a DualSense hasn't got";
return false;
}
c.addresses = std::move(out.addresses);
c.values = std::move(out.values);
c.options.clear();
return true;
}
std::string CodesText(const Cheat& c)
{
std::string out;
char code[24];
for (size_t i = 0; i < c.addresses.size(); i++)
{
int v = c.values[i];
if (v < 0)
v = c.options.empty() ? 0 : c.options[size_t(c.state > 0 ? c.state - 1 : 0)].second;
snprintf(code, sizeof(code), "%s%08X %04X", out.empty() ? "" : ", ", unsigned(c.addresses[i]), unsigned(v & 0xFFFF));
out += code;
}
return out;
}
std::string DisplayName(const std::string& name)
{
std::string s = name;
+15 -1
View File
@@ -1,5 +1,6 @@
// Mupen64Plus PS5 frontend: cheats -- the GameShark codes of mupen64plus-core's data/mupencheat.txt (built into
// eboot.bin, compressed), switched on and off in the pause menu and remembered per game (fe_emu.cpp).
// eboot.bin, compressed) and the player's own (found with the cheat search or typed in), switched on and off in
// the pause menu and remembered per game (fe_emu.cpp).
// SPDX-License-Identifier: MIT
#pragma once
@@ -20,6 +21,8 @@ struct Cheat
std::vector<int> values; // -1: the chosen option's value
std::vector<std::pair<std::string, int>> options; // a cheat with choices ("????"), else empty
int state = 0; // 0 off; then 1 on, or with options 1 + the chosen option's index
bool mine = false; // the player's own (found with the cheat search or typed in), not the database's
int uid = 0; // its name in the core while the game runs (fe_emu.cpp)
int States() const { return options.empty() ? 2 : int(options.size()) + 1; }
};
@@ -28,6 +31,17 @@ struct Cheat
// the DualSense has none.
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game);
// The player's own cheats: a file in the database's format (" cn name" lines, each followed by its codes,
// "8033B21D 0064"), read leniently so one written on a PC works too; and back.
std::vector<Cheat> Parse(const std::string& text);
std::string Format(const std::vector<Cheat>& list);
// Codes typed in: pairs of an address (8 hex digits) and a value (up to 4), separated by spaces, commas or new
// lines. False with `error` set when they aren't (or need the GameShark's button).
bool ParseCodes(const std::string& text, Cheat& c, std::string* error);
// "8033B21D 0064, 8033B21E 0001" (the chosen option's value, or the first, where the code has "????")
std::string CodesText(const Cheat& c);
// "Play As\Player 1" -> "Play As: Player 1"
std::string DisplayName(const std::string& name);
} // namespace cheats
+12
View File
@@ -293,6 +293,18 @@ void DrawIcon(Icon icon, int x, int y, int size, uint32_t color)
FillTriangle(cx - s * 0.25f, y + s * 0.45f, cx + s * 0.25f, y + s * 0.45f, cx, y + s * 0.72f, color);
FillRoundRect(x + int(s * 0.12f), y + int(s * 0.8f), int(s * 0.76f), std::max(2, int(s * 0.09f)), 1, color);
break;
case Icon::Search:
{
// a magnifying glass: a ring at the upper left, its handle down to the lower right
const float r = s * 0.25f, rx = x + s * 0.42f, ry = y + s * 0.42f;
StrokeArc(rx, ry, r, s * 0.1f, 0.0f, 360.0f, color);
const float d = 0.70710678f, hw = s * 0.07f;
const float ax = rx + (r + s * 0.03f) * d, ay = ry + (r + s * 0.03f) * d;
const float bx = x + s * 0.9f, by = y + s * 0.9f;
FillTriangle(ax - hw * d, ay + hw * d, ax + hw * d, ay - hw * d, bx + hw * d, by - hw * d, color);
FillTriangle(ax - hw * d, ay + hw * d, bx + hw * d, by - hw * d, bx - hw * d, by + hw * d, color);
break;
}
}
}
} // namespace gfx
+1
View File
@@ -42,6 +42,7 @@ enum class Icon
Screen, // a rectangle
Gauge,
Download,
Search, // a magnifying glass
};
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color);
} // namespace gfx
+145 -6
View File
@@ -228,9 +228,11 @@ struct State
bool game_layout = false;
bool own_layout = false;
fe::Layout layout;
// cheats: the database's for this ROM, with what the player chose
// cheats: the player's own, then the database's for this ROM, with what the player chose
std::vector<fe::cheats::Cheat> cheats;
std::string cheats_game;
int next_uid = 0;
fe::search::Search search; // the cheat search, while the game runs
// "continue where you left off": the state loaded at the first VI, and the one saved on the way out
bool resume_load = false;
bool resume_loading = false;
@@ -539,12 +541,52 @@ void SaveCheatStates()
}
}
// The player's own cheats: cheats/<ROM file>.custom.txt, in the database's format (fe::cheats::Parse).
std::string CustomCheatsPath()
{
return OrbisDir("cheats") + "/" + g.file_base + ".custom.txt";
}
std::vector<fe::cheats::Cheat> LoadCustomCheats()
{
std::string text;
if (FILE* f = fopen(CustomCheatsPath().c_str(), "r"))
{
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
text.append(buf, n);
fclose(f);
}
return fe::cheats::Parse(text);
}
void SaveCustomCheats()
{
std::vector<fe::cheats::Cheat> mine;
for (const fe::cheats::Cheat& c : g.cheats)
if (c.mine)
mine.push_back(c);
if (mine.empty())
{
unlink(CustomCheatsPath().c_str());
return;
}
OrbisMkdirs(OrbisDir("cheats"));
if (FILE* f = fopen(CustomCheatsPath().c_str(), "w"))
{
fputs(fe::cheats::Format(mine).c_str(), f);
fclose(f);
}
}
// Hands cheat `index` to the core as it is now: on (with its codes, the chosen option's value in place of
// "????") or off (the core puts the old values back). Its name in the core is unique: "<index>:<name>".
// "????") or off (the core puts the old values back). Its name in the core is "#<uid>", which stays the same
// while the game runs whatever the list or the cheat's name does.
void ApplyCheat(int index)
{
const fe::cheats::Cheat& c = g.cheats[size_t(index)];
const std::string id = std::to_string(index) + ":" + c.name;
const std::string id = "#" + std::to_string(c.uid);
if (c.state == 0)
{
CoreCheatEnabled(id.c_str(), 0);
@@ -890,10 +932,20 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
n64ps5_rsp_set_hle(cfg.hle_audio);
static const char* const kPaks[] = {"none", "Controller Pak", "Rumble Pak"};
OrbisLog("[emu] controller pak: %s", kPaks[cfg.pak < 0 || cfg.pak > 2 ? 1 : cfg.pak]);
// cheats: the database's for this ROM (by the header's CRCs and country), the ones left on are on again
// cheats: the player's own, then the database's for this ROM (by the header's CRCs and country); the ones
// left on are on again
g.cheats_game.clear();
g.cheats = fe::cheats::ForGame(__builtin_bswap32(header.CRC1), __builtin_bswap32(header.CRC2),
header.Country_code, &g.cheats_game);
g.cheats = LoadCustomCheats();
const size_t mine = g.cheats.size();
for (fe::cheats::Cheat& c : fe::cheats::ForGame(__builtin_bswap32(header.CRC1), __builtin_bswap32(header.CRC2),
header.Country_code, &g.cheats_game))
g.cheats.push_back(std::move(c));
g.next_uid = 0;
for (fe::cheats::Cheat& c : g.cheats)
c.uid = ++g.next_uid;
if (mine)
OrbisLog("[cheats] %zu of your own", mine);
g.search.Clear();
LoadCheatStates();
std::string cheats_on;
for (size_t i = 0; i < g.cheats.size(); i++)
@@ -984,6 +1036,7 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
g.leaving = false;
g.running = false;
g.game_layout = false;
g.search.Clear(); // its 10-20 MB
N64_STAGE(Emu, "idle");
if (r != M64ERR_SUCCESS && g.vi == 0)
{
@@ -1035,6 +1088,92 @@ void SetCheat(int index, int state)
c.state == 0 ? "off" : c.options.empty() ? "on" : c.options[size_t(c.state - 1)].first.c_str());
}
std::string UniqueCheatName(const std::string& name, int except)
{
std::string base = name.empty() ? "My cheat" : name, out = base;
for (int n = 2;; n++)
{
bool taken = false;
for (size_t i = 0; i < g.cheats.size(); i++)
taken |= int(i) != except && g.cheats[i].name == out;
if (!taken)
return out;
out = base + " (" + std::to_string(n) + ")";
}
}
int AddMyCheat(const fe::cheats::Cheat& cheat)
{
if (!g.running)
return -1;
fe::cheats::Cheat c = cheat;
c.mine = true;
c.uid = ++g.next_uid;
c.name = UniqueCheatName(c.name, -1);
c.state = 1;
// after the player's other own cheats, before the database's
size_t at = 0;
while (at < g.cheats.size() && g.cheats[at].mine)
at++;
g.cheats.insert(g.cheats.begin() + long(at), c);
ApplyCheat(int(at));
SaveCustomCheats();
SaveCheatStates();
OrbisLog("[cheats] yours: \"%s\" (%s), on", c.name.c_str(), fe::cheats::CodesText(c).c_str());
return int(at);
}
void UpdateMyCheat(int index, const fe::cheats::Cheat& cheat)
{
if (!g.running || index < 0 || index >= int(g.cheats.size()) || !g.cheats[size_t(index)].mine)
return;
fe::cheats::Cheat& c = g.cheats[size_t(index)];
c.name = UniqueCheatName(cheat.name, index);
c.note = cheat.note;
c.addresses = cheat.addresses;
c.values = cheat.values;
c.options = cheat.options;
if (c.state >= c.States())
c.state = 1;
if (c.state)
ApplyCheat(index); // the new codes
SaveCustomCheats();
SaveCheatStates();
OrbisLog("[cheats] yours: \"%s\" changed (%s)", c.name.c_str(), fe::cheats::CodesText(c).c_str());
}
void DeleteMyCheat(int index)
{
if (!g.running || index < 0 || index >= int(g.cheats.size()) || !g.cheats[size_t(index)].mine)
return;
g.cheats[size_t(index)].state = 0;
ApplyCheat(index); // off: the core puts the old values back
OrbisLog("[cheats] yours: \"%s\" deleted", g.cheats[size_t(index)].name.c_str());
g.cheats.erase(g.cheats.begin() + index);
SaveCustomCheats();
SaveCheatStates();
}
fe::search::Search& CheatSearch()
{
return g.search;
}
const uint32_t* Ram(uint32_t* size)
{
*size = 0;
return g.running ? n64ps5_ram(size) : nullptr;
}
void PokeRam(uint32_t address, uint32_t value, int bytes)
{
if (g.running)
{
n64ps5_ram_write(address, value, bytes);
OrbisLog("[cheats] RAM %08X = %u (%d-bit)", unsigned(0x80000000u | address), unsigned(value), bytes * 8);
}
}
fe::Layout GameLayout()
{
return g.game_layout ? g.layout : fe::Config().layout;
+15 -2
View File
@@ -3,6 +3,7 @@
#pragma once
#include "fe_cheats.h"
#include "fe_search.h"
#include "fe_settings.h"
#include <string>
@@ -27,11 +28,23 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume = fa
bool ResumeStateExists(const std::string& rom, std::string* saved_at = nullptr);
bool GameLoaded();
// The running game's cheats (fe_cheats.h; empty when the database has none for it), and switching one
// (pause menu): applied at once and remembered for the game in cheats/<ROM file>.txt.
// The running game's cheats (fe_cheats.h): the player's own first, then the database's (none when it has none
// for the game). Switching one (pause menu) applies it at once; what is on is remembered for the game in
// cheats/<ROM file>.txt.
std::vector<fe::cheats::Cheat>& Cheats();
std::string CheatsGameName();
void SetCheat(int index, int state);
// The player's own cheats, kept in cheats/<ROM file>.custom.txt: a new one (turned on; its index is returned),
// changed (its name, note and codes) or deleted. Names are made unique: "Lives" -> "Lives (2)".
int AddMyCheat(const fe::cheats::Cheat& cheat);
void UpdateMyCheat(int index, const fe::cheats::Cheat& cheat);
void DeleteMyCheat(int index);
std::string UniqueCheatName(const std::string& name, int except = -1);
// The cheat search (fe_search.h) of the running game, over its RAM (nullptr when no game runs). PokeRam writes a
// value once (the cheat search's "Set it now").
fe::search::Search& CheatSearch();
const uint32_t* Ram(uint32_t* size);
void PokeRam(uint32_t address, uint32_t value, int bytes);
// The running game's controller layout: its own (fe::LoadGameLayout) or else the one of Settings. Setting it
// (pause menu) gives the game its own layout file, unless it is the same as Settings' again.
fe::Layout GameLayout();
+741 -49
View File
@@ -14,6 +14,7 @@
#include "OrbisPaths.h"
#include "ProsperoInput.h"
#include "ProsperoKeyboard.h"
#include "ProsperoSce.h"
#include "ProsperoVideo.h"
@@ -22,6 +23,7 @@
#include <sys/stat.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <ctime>
@@ -421,6 +423,16 @@ constexpr const char* kRam = "\xF0\x9F\x96\xAA"; // U+1F5AA
constexpr const char* kJoystick = "\xF0\x9F\x95\xB9"; // U+1F579
constexpr const char* kSwap = "\xE2\x86\x94"; // U+2194
constexpr const char* kInfo = "\xF0\x9F\x9B\x88"; // U+1F6C8
constexpr const char* kKeyboard = "\xE2\x8C\xA8"; // U+2328
constexpr const char* kMemo = "\xF0\x9F\x93\x9D"; // U+1F4DD
constexpr const char* kBallotX = "\xE2\x9C\x97"; // U+2717
// the cheat search's tests (Roboto's and PromptFont's)
constexpr const char* kNotEqual = "\xE2\x89\xA0"; // U+2260
constexpr const char* kMinus = "\xE2\x88\x92"; // U+2212
constexpr const char* kUp = "\xE2\x86\x91"; // U+2191
constexpr const char* kDown = "\xE2\x86\x93"; // U+2193
constexpr const char* kUpDown = "\xE2\x86\x95"; // U+2195
constexpr const char* kBullet = "\xE2\x80\xA2"; // U+2022
RowIcon SettingIcon(int s)
{
@@ -485,6 +497,7 @@ enum class Kind
Slider, // the volume
Page, // opens a page of its own: value >
Action, // happens on Cross: value, then the Cross symbol
Info, // only shows its value
};
Kind SettingKind(int s)
@@ -561,7 +574,14 @@ void DrawSettingRow(const SettingRow& r, int y, bool selected)
{
const int bx = RR - 22 - TextWidthPx(icon::Cross, 30);
DrawTextPx(bx, cy - 16, icon::Cross, 30, r.enabled ? kBorder : kDimmed);
DrawTextPx(bx - 14 - TextWidthPx(r.value.c_str(), 21), cy - 12, r.value.c_str(), 21, r.enabled ? kHint : kDimmed);
const std::string v = FitTextPx(r.value, 21, bx - 14 - (RX + 400));
DrawTextPx(bx - 14 - TextWidthPx(v.c_str(), 21), cy - 12, v.c_str(), 21, r.enabled ? kHint : kDimmed);
break;
}
case Kind::Info:
{
const std::string v = FitTextPx(r.value, 21, RR - 10 - (RX + 400));
DrawTextPx(RR - 10 - TextWidthPx(v.c_str(), 21), cy - 12, v.c_str(), 21, value);
break;
}
case Kind::Toggle:
@@ -697,8 +717,9 @@ void DrawRows(const std::vector<SettingRow>& rows, int sel, int& top, int visibl
}
}
void DrawToast(const std::string& text)
void DrawToast(const std::string& message)
{
const std::string text = FitTextPx(message, 24, PW - 140);
const int tw = TextWidthPx(text.c_str(), 24);
const int bx = PX + (PW - tw - 60) / 2, by = FY - 74;
gfx::FillRoundRect(bx, by, tw + 60, 52, 26, Rgb(58, 47, 176), 240);
@@ -808,27 +829,33 @@ const char* PadName(int pad)
return pad >= 0 && pad < N64PS5_PAD_COUNT ? kNames[pad] : "";
}
// "Reset the game?" over the menu: Square asked for it
void DrawConfirmReset()
// a question over the menu ("Reset the game?"): Cross `yes`, Circle `no`
void DrawConfirm(const RowIcon& ic, const std::string& title, const std::string& text, const char* yes, const char* no)
{
gfx::FillRoundRect(PX + 3, PY + 3, PW - 6, PH - 6, 24, Rgb(5, 4, 18), 150);
const int bw = 620, bh = 250, bx = PX + (PW - bw) / 2, by = PY + (PH - bh) / 2;
const int bw = 680, bh = 250, bx = PX + (PW - bw) / 2, by = PY + (PH - bh) / 2;
gfx::GlowRoundRect(bx, by, bw, bh, 20, 22, kGlow, 150);
gfx::GradientRoundRect(bx, by, bw, bh, 20, kPanelTop, kPanelBottom, false);
gfx::StrokeRoundRect(bx, by, bw, bh, 20, 2.5f, kBorder);
gfx::DrawIcon(gfx::Icon::Restart, bx + 40, by + 38, 50, kIcon);
DrawTextPx(bx + 112, by + 40, "Reset the game?", 40, kTitle, false, 1);
DrawTextPx(bx + 112, by + 98, "Progress you haven't saved is lost.", 23, kHint);
const std::string yes = std::string(icon::Cross), no = std::string(icon::Circle);
DrawRowIcon(ic, bx + 40, by + 38, 50, kIcon);
DrawTextPx(bx + 112, by + 40, FitTextPx(title, 40, bw - 150).c_str(), 40, kTitle, false, 1);
DrawTextPx(bx + 112, by + 98, FitTextPx(text, 23, bw - 150).c_str(), 23, kHint);
const std::string cross = std::string(icon::Cross), circle = std::string(icon::Circle);
int x = bx + 112;
const int y = by + 172;
DrawTextPx(x, y - 6, yes.c_str(), 34, kBorder);
x += TextWidthPx(yes.c_str(), 34) + 10;
DrawTextPx(x, y, "Reset", 24, kLabel);
x += TextWidthPx("Reset", 24) + 48;
DrawTextPx(x, y - 6, no.c_str(), 34, kBorder);
x += TextWidthPx(no.c_str(), 34) + 10;
DrawTextPx(x, y, "Cancel", 24, kLabel);
DrawTextPx(x, y - 6, cross.c_str(), 34, kBorder);
x += TextWidthPx(cross.c_str(), 34) + 10;
DrawTextPx(x, y, yes, 24, kLabel);
x += TextWidthPx(yes, 24) + 48;
DrawTextPx(x, y - 6, circle.c_str(), 34, kBorder);
x += TextWidthPx(circle.c_str(), 34) + 10;
DrawTextPx(x, y, no, 24, kLabel);
}
// Square asked for it
void DrawConfirmReset()
{
DrawConfirm(Drawn(gfx::Icon::Restart), "Reset the game?", "Progress you haven't saved is lost.", "Reset", "Cancel");
}
} // namespace pm
@@ -853,6 +880,19 @@ void WaitForRelease()
}
}
// The touchpad in a page the pause menu opened (Controller Layout, Cheats and the cheat search's pages): straight
// back to the game. Every page on the way returns, and the pause menu resumes.
bool g_resume = false;
bool ResumeNow(const Nav& n)
{
if (!emu::GameLoaded())
return false; // Settings on the game list
if (n.touchpad)
g_resume = true;
return g_resume;
}
// What each DualSense control presses on the N64 pad. From Settings (the game list) it is every game's layout;
// from the pause menu, the running game's own (kept with the game; the same as every game's until changed).
// L3 and R3 aren't in it: in a game they load and save states.
@@ -905,7 +945,7 @@ void LayoutMenu()
}
else if (sel == kPads && n.ok)
layout = in_game ? cfg.layout : Layout();
if (n.back || n.options)
if (n.back || n.options || ResumeNow(n))
{
if (in_game)
emu::SetGameLayout(layout);
@@ -1214,61 +1254,710 @@ std::string CheatState(const cheats::Cheat& c)
return c.options.empty() ? "On" : c.options[size_t(c.state - 1)].first;
}
// The running game's cheats: each one off / on, or off / one of its choices, with the database's note under the
// list.
void CheatsMenu()
// a page's picture under its rows: the panel is drawn once (its glow takes a while), then copied back each frame
class PageBase
{
std::vector<cheats::Cheat>& list = emu::Cheats();
const pm::Panel panel{pm::Glyph(pm::kStar), "Cheats", "PAUSED", emu::CheatsGameName(), "",
{{icon::Cross, "Change"}, {icon::Circle, "Back"}}, nullptr};
const int visible = pm::kVisible - 3; // room for the note
NavReader nav;
int sel = 0, top = 0;
std::vector<uint32_t> base;
bool dirty = true;
for (;;)
public:
void Draw(const pm::Panel& panel)
{
if (dirty)
if (m_dirty)
{
pm::DrawPanel(panel);
base.assign(ps5video::Surface(), ps5video::Surface() + size_t(W) * H);
dirty = false;
m_base.assign(ps5video::Surface(), ps5video::Surface() + size_t(W) * H);
m_dirty = false;
}
else
memcpy(ps5video::Surface(), base.data(), base.size() * sizeof(uint32_t));
std::vector<pm::SettingRow> rows;
for (const cheats::Cheat& c : list)
rows.push_back({pm::Glyph(pm::kStar), cheats::DisplayName(c.name), false, pm::Kind::Choice, CheatState(c),
false, 0, true});
memcpy(ps5video::Surface(), m_base.data(), m_base.size() * sizeof(uint32_t));
}
void Redraw() { m_dirty = true; }
private:
std::vector<uint32_t> m_base;
bool m_dirty = true;
};
// a short message over the rows, for a few seconds
struct Toast
{
std::string text;
double until = 0;
void Show(const std::string& t, double seconds = 2.5)
{
text = t;
until = Now() + seconds;
}
void Draw() const
{
if (!text.empty() && Now() < until)
pm::DrawToast(text);
}
};
// The PS5's keyboard over the menu as the screen shows it. The caller starts its NavReader again afterwards: the
// button that closed the keyboard may still be down.
bool Keyboard(const std::string& title, std::string& text, int max_chars, ps5keyboard::Type type, std::string* error)
{
return ps5keyboard::Edit(title, text, max_chars, type, [] { Present(); }, error);
}
// a number typed on the keyboard, at most `max` (more is cut to it, and `error` says so); false when the player
// canceled it or typed something else
bool KeyboardNumber(const std::string& title, uint32_t& value, uint32_t max, std::string* error)
{
std::string text = std::to_string(value);
if (!Keyboard(title, text, 10, ps5keyboard::Type::Number, error))
return false;
uint64_t v = 0;
bool digits = false;
for (const char c : text)
{
if (c == ' ' || c == ',')
continue;
if (c < '0' || c > '9')
{
*error = "\"" + text + "\" is not a number";
return false;
}
v = std::min<uint64_t>(v * 10 + uint64_t(c - '0'), 0xFFFFFFFFFull);
digits = true;
}
if (!digits)
return false;
if (v > max)
{
*error = "The most it can be is " + search::Thousands(max);
v = max;
}
value = uint32_t(v);
return true;
}
std::string Trim(const std::string& s)
{
const size_t a = s.find_first_not_of(" \t\r\n");
if (a == std::string::npos)
return "";
return s.substr(a, s.find_last_not_of(" \t\r\n") - a + 1);
}
// an address of the N64's RAM as cheat codes write it: 0x123456 -> "80123456"
std::string RamAddress(uint32_t address)
{
char text[16];
snprintf(text, sizeof(text), "80%06X", unsigned(address & 0xFFFFFF));
return text;
}
const char* BitsName(int bytes)
{
return bytes == 1 ? "8-bit" : bytes == 2 ? "16-bit" : "32-bit";
}
// A cheat's own page, its name and codes: a new one (index -1, maybe a copy of one of the list) or one of the
// player's own (its index in emu::Cheats()), which can be deleted too.
void EditCheatMenu(int index, cheats::Cheat cheat)
{
const bool is_new = index < 0;
const pm::Panel panel{pm::Glyph(pm::kMemo), is_new ? "New Cheat" : "Your Cheat", "PAUSED", "YOUR OWN CHEAT", "",
{{icon::Cross, "Confirm"}, {icon::Circle, "Back"}}, nullptr};
enum
{
E_NAME,
E_CODES,
E_SAVE,
E_DELETE,
};
const int count = is_new ? 3 : 4;
const int visible = pm::kVisible - 4; // room for the codes
std::string codes = cheats::CodesText(cheat); // as typed: kept when they aren't valid, to fix them
std::string problem; // what is wrong with them
bool changed = false;
int confirm = 0; // 1: leave without saving? 2: delete it?
NavReader nav;
int sel = 0, top = 0;
PageBase page;
Toast toast;
for (;;)
{
page.Draw(panel);
std::string first = codes.substr(0, codes.find(','));
const int more = int(std::count(codes.begin(), codes.end(), ','));
if (more)
first += " +" + std::to_string(more);
std::vector<pm::SettingRow> rows = {
{pm::Glyph(pm::kMemo), "Name", false, pm::Kind::Action, cheat.name.empty() ? "Type one" : cheat.name, false, 0,
true},
{pm::Glyph(pm::kKeyboard), "Codes", false, pm::Kind::Action,
!problem.empty() ? "Not valid" : codes.empty() ? "Type them" : first, false, 0, true},
{pm::Glyph(pm::kStar), is_new ? "Add the Cheat" : "Save the Changes", false, pm::Kind::Action,
is_new ? "Turns it on" : "", false, 0, !cheat.addresses.empty() && problem.empty()},
};
if (!is_new)
rows.push_back({pm::Glyph(pm::kBallotX), "Delete This Cheat", false, pm::Kind::Action, "", false, 0, true});
pm::DrawRows(rows, sel, top, visible);
const bool has_note = sel < int(list.size()) && !list[size_t(sel)].note.empty();
pm::DrawNote(pm::RY0 + visible * pm::RH + 10, has_note ? "ABOUT THIS CHEAT" : "NOTE",
has_note ? list[size_t(sel)].note
: "Cheats can make a game misbehave or crash. If one does, turn it off (or reset the game).");
pm::DrawNote(pm::RY0 + visible * pm::RH + 10, !problem.empty() ? "CHECK THE CODES" : "CODES",
!problem.empty() ? problem
: !codes.empty()
? codes
: "A code is an address (8 hex digits) and a value (4), like 8033B21D 0064. A cheat can have several: "
"type them one after another.");
toast.Draw();
if (confirm == 1)
pm::DrawConfirm(pm::Glyph(pm::kMemo), "Leave without saving?", "What you changed here is lost.", "Leave", "Stay");
else if (confirm == 2)
pm::DrawConfirm(pm::Glyph(pm::kBallotX), "Delete this cheat?", cheat.name, "Delete", "Keep It");
Present();
const Nav n = nav.Read();
pm::MoveSelection(n, sel, int(list.size()));
if (sel < int(list.size()) && (n.left || n.right || n.ok))
if (confirm)
{
const cheats::Cheat& c = list[size_t(sel)];
emu::SetCheat(sel, (c.state + (n.left ? c.States() - 1 : 1)) % c.States());
if (n.ok)
{
if (confirm == 2)
emu::DeleteMyCheat(index);
WaitForRelease();
return;
}
if (n.back || n.options)
confirm = 0;
continue;
}
if (n.back || n.options)
if (n.back || n.options || n.touchpad)
{
if (changed)
{
confirm = 1; // the touchpad too: not without asking
continue;
}
ResumeNow(n);
WaitForRelease();
return;
}
pm::MoveSelection(n, sel, count);
if (!n.ok)
continue;
std::string error;
switch (sel)
{
case E_NAME:
{
std::string name = cheat.name;
if (Keyboard("Name of the cheat", name, 48, ps5keyboard::Type::Text, &error) && !Trim(name).empty() &&
Trim(name) != cheat.name)
{
cheat.name = Trim(name);
changed = true;
}
nav = NavReader();
break;
}
case E_CODES:
{
std::string text = codes;
if (Keyboard("GameShark codes", text, 1200, ps5keyboard::Type::Latin, &error) && Trim(text) != codes)
{
codes = Trim(text);
changed = true;
std::string why;
if (codes.empty())
{
cheat.addresses.clear();
cheat.values.clear();
problem.clear();
}
else if (cheats::ParseCodes(codes, cheat, &why))
{
codes = cheats::CodesText(cheat);
problem.clear();
}
else
problem = why;
}
nav = NavReader();
break;
}
case E_SAVE:
if (cheat.addresses.empty() || !problem.empty())
{
toast.Show(problem.empty() ? "Type its codes first" : "Fix the codes first");
break;
}
if (cheat.name.empty())
cheat.name = "My cheat";
if (is_new)
emu::AddMyCheat(cheat);
else
emu::UpdateMyCheat(index, cheat);
WaitForRelease();
return;
case E_DELETE: confirm = 2; break;
}
if (!error.empty())
toast.Show(error, 4.0);
}
}
// A cheat from one of the search's results: the value kept where the player wants it while the cheat is on (a
// GameShark code 80/81 with that value), or just set once. Returns what to tell the player.
std::string MakeCheatMenu(uint32_t address, int bytes)
{
uint32_t size = 0;
const uint32_t* ram = emu::Ram(&size);
if (!ram || address >= size)
return "";
const uint32_t max = search::MaxValue(bytes);
uint32_t keep = search::Read(ram, address, bytes);
std::string name = "Found " + RamAddress(address);
const pm::Panel panel{pm::Glyph(pm::kStar), "New Cheat", "PAUSED", RamAddress(address) + " " + BitsName(bytes), "",
{{icon::Cross, "Confirm"}, {icon::Circle, "Back"}}, nullptr};
enum
{
M_NOW,
M_KEEP,
M_NAME,
M_SAVE,
M_SET,
M_COUNT
};
const int visible = pm::kVisible - 4;
NavReader nav;
int sel = M_KEEP, top = 0;
PageBase page;
Toast toast;
for (;;)
{
page.Draw(panel);
const uint32_t now = search::Read(ram, address, bytes);
const std::vector<pm::SettingRow> rows = {
{pm::Glyph("#"), "Value Now", false, pm::Kind::Info, search::Thousands(now), false, 0, true},
{pm::Glyph(pm::kStar), "Keep It At", false, pm::Kind::Choice, search::Thousands(keep), false, 0, true},
{pm::Glyph(pm::kMemo), "Name", false, pm::Kind::Action, name, false, 0, true},
{pm::Drawn(gfx::Icon::Download), "Save as a Cheat", false, pm::Kind::Action, "Turns it on", false, 0, true},
{pm::Drawn(gfx::Icon::Play), "Set It Once", false, pm::Kind::Action, "Now, not kept", false, 0, true},
};
pm::DrawRows(rows, sel, top, visible);
pm::DrawNote(pm::RY0 + visible * pm::RH + 10, "WHAT IT DOES",
"Saved as a cheat, it keeps the value at " + search::Thousands(keep) +
" while the cheat is on; you find it in Cheats with the others. Set It Once changes the value now, and "
"the game goes on from there. Left and right change the value, Cross types it.");
toast.Draw();
Present();
const Nav n = nav.Read();
if (n.back || n.options || ResumeNow(n))
{
WaitForRelease();
return "";
}
pm::MoveSelection(n, sel, M_COUNT);
std::string error;
if (sel == M_KEEP && (n.left || n.right))
keep = n.left ? (keep > 0 ? keep - 1 : 0) : (keep < max ? keep + 1 : max);
else if (sel == M_KEEP && n.ok)
{
KeyboardNumber("The value to keep", keep, max, &error);
nav = NavReader();
}
else if (sel == M_NAME && n.ok)
{
std::string text = name;
if (Keyboard("Name of the cheat", text, 48, ps5keyboard::Type::Text, &error) && !Trim(text).empty())
name = Trim(text);
nav = NavReader();
}
else if (sel == M_SAVE && n.ok)
{
cheats::Cheat c;
c.name = name;
if (bytes == 4)
{
// two 16-bit writes: the upper half, then the lower
c.addresses = {0x81000000u | address, 0x81000000u | (address + 2)};
c.values = {int(keep >> 16), int(keep & 0xFFFF)};
}
else
{
c.addresses = {(bytes == 1 ? 0x80000000u : 0x81000000u) | address};
c.values = {int(keep & max)};
}
const int at = emu::AddMyCheat(c);
WaitForRelease();
return at < 0 ? "" : "Saved and turned on: " + emu::Cheats()[size_t(at)].name;
}
else if (sel == M_SET && n.ok)
{
emu::PokeRam(address, keep, bytes);
toast.Show("Set to " + search::Thousands(keep));
}
if (!error.empty())
toast.Show(error, 4.0);
}
}
// The addresses still in the search, with their value now and at the last step; Cross makes a cheat of one.
void ResultsMenu()
{
search::Search& s = emu::CheatSearch();
uint32_t size = 0;
const uint32_t* ram = emu::Ram(&size);
if (!ram || !s.Active())
return;
const size_t kMax = 300;
const std::vector<search::Search::Result> results = s.Results(kMax);
const pm::Panel panel{pm::Drawn(gfx::Icon::Search), "Results", "PAUSED", "ADDRESS", "NOW",
{{icon::Cross, "Make a Cheat"}, {icon::Circle, "Back"}}, nullptr};
const int visible = pm::kVisible - 3;
NavReader nav;
int sel = 0, top = 0;
PageBase page;
Toast toast;
for (;;)
{
page.Draw(panel);
std::vector<pm::SettingRow> rows;
for (const search::Search::Result& r : results)
{
const uint32_t now = search::Read(ram, r.address, s.Bytes());
rows.push_back({pm::Glyph(pm::kRam), RamAddress(r.address), false, pm::Kind::Page,
now == r.before ? search::Thousands(now) : search::Thousands(now) + " (was " + search::Thousands(r.before) + ")",
false, 0, true});
}
pm::DrawRows(rows, sel, top, visible);
pm::DrawNote(pm::RY0 + visible * pm::RH + 10, "RESULTS",
s.Count() > results.size()
? "The first " + search::Thousands(results.size()) + " of " + search::Thousands(s.Count()) +
". Narrow the search down more to see the rest."
: "Pick the one that matches what the game shows. If several do, play on and narrow it down more, or "
"try them one by one.");
toast.Draw();
Present();
const Nav n = nav.Read();
if (n.back || n.options || ResumeNow(n))
{
WaitForRelease();
return;
}
pm::MoveSelection(n, sel, int(rows.size()));
if ((n.ok || n.right) && sel < int(results.size()))
{
const std::string said = MakeCheatMenu(results[size_t(sel)].address, s.Bytes());
if (g_resume)
return;
if (!said.empty())
toast.Show(said, 3.0);
nav = NavReader();
page.Redraw();
}
}
}
// The cheat search (fe_search.h): a size and a value to start from, then a step after each time the player played
// on, until Results shows few enough addresses to try.
void SearchMenu()
{
search::Search& s = emu::CheatSearch();
static int s_size = 0; // 8, 16, 32-bit; kept while the app runs
static uint32_t s_number = 0;
static const int kBytes[3] = {1, 2, 4};
enum Row
{
Q_SIZE,
Q_NUMBER,
Q_START,
Q_START_ANY,
Q_RESULTS,
Q_EQUAL,
Q_NOT_EQUAL,
Q_UP_BY,
Q_DOWN_BY,
Q_CHANGED,
Q_UNCHANGED,
Q_UP,
Q_DOWN,
Q_UNDO,
Q_NEW,
};
const int visible = pm::kVisible - 3;
NavReader nav;
int sel = 0, top = 0;
bool was_active = !s.Active(); // the panel's headings follow the search
pm::Panel panel{};
PageBase page;
Toast toast;
for (;;)
{
const bool active = s.Active();
const int bytes = active ? s.Bytes() : kBytes[s_size];
const uint32_t max = search::MaxValue(bytes);
s_number = std::min(s_number, max);
const std::vector<Row> ids = active
? std::vector<Row>{Q_RESULTS, Q_NUMBER, Q_EQUAL, Q_NOT_EQUAL, Q_UP_BY, Q_DOWN_BY, Q_CHANGED, Q_UNCHANGED, Q_UP,
Q_DOWN, Q_UNDO, Q_NEW}
: std::vector<Row>{Q_SIZE, Q_NUMBER, Q_START, Q_START_ANY};
if (active != was_active)
{
was_active = active;
sel = 0;
top = 0;
panel = pm::Panel{pm::Drawn(gfx::Icon::Search), "Cheat Search", "PAUSED",
active ? std::string(BitsName(bytes)) + " values" : "New search", "",
{{icon::Cross, "Confirm"}, {icon::Circle, "Back"}}, nullptr};
for (char& c : panel.section)
c = char(toupper((unsigned char)c));
page.Redraw();
}
page.Draw(panel);
std::vector<pm::SettingRow> rows;
for (const Row id : ids)
{
switch (id)
{
case Q_SIZE:
{
static const char* const kSizes[3] = {"8-bit (0-255)", "16-bit (0-65535)", "32-bit"};
rows.push_back({pm::Glyph(pm::kRam), "Value Size", false, pm::Kind::Choice, kSizes[s_size], false, 0, true});
break;
}
case Q_NUMBER:
rows.push_back({pm::Glyph("#"), "Value", false, pm::Kind::Choice, search::Thousands(s_number), false, 0, true});
break;
case Q_START:
rows.push_back({pm::Drawn(gfx::Icon::Search), "Search for the Value", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_START_ANY:
rows.push_back({pm::Drawn(gfx::Icon::Search), "Search Without a Value", false, pm::Kind::Action,
"Bars, timers", false, 0, true});
break;
case Q_RESULTS:
rows.push_back({pm::Glyph(pm::kRam), "Results", false, pm::Kind::Page, search::Thousands(s.Count()) + " left",
false, 0, s.Count() > 0});
break;
case Q_EQUAL: rows.push_back({pm::Glyph("="), "Is the Value", false, pm::Kind::Action, "", false, 0, true}); break;
case Q_NOT_EQUAL:
rows.push_back({pm::Glyph(pm::kNotEqual), "Is Not the Value", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_UP_BY:
rows.push_back({pm::Glyph("+"), "Went Up by the Value", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_DOWN_BY:
rows.push_back({pm::Glyph(pm::kMinus), "Went Down by the Value", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_CHANGED:
rows.push_back({pm::Glyph(pm::kUpDown), "Changed", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_UNCHANGED:
rows.push_back({pm::Glyph(pm::kBullet), "Didn't Change", false, pm::Kind::Action, "", false, 0, true});
break;
case Q_UP: rows.push_back({pm::Glyph(pm::kUp), "Went Up", false, pm::Kind::Action, "", false, 0, true}); break;
case Q_DOWN: rows.push_back({pm::Glyph(pm::kDown), "Went Down", false, pm::Kind::Action, "", false, 0, true}); break;
case Q_UNDO:
rows.push_back({pm::Drawn(gfx::Icon::Restart), "Undo the Last Step", false, pm::Kind::Action, "", false, 0,
s.CanUndo()});
break;
case Q_NEW:
rows.push_back({pm::Glyph(pm::kBallotX), "New Search", false, pm::Kind::Action, "", false, 0, true});
break;
}
}
pm::DrawRows(rows, sel, top, visible);
pm::DrawNote(pm::RY0 + visible * pm::RH + 10, "HOW IT WORKS",
active ? "Play until the value changes in the game, pause, and pick here what happened to it. Do that a few "
"times; when only a few are left, open Results."
: "Set the value the game shows (3 lives: 3) and search for it, or search without one for things with "
"no number, like a health bar. 8-bit fits most counters.");
toast.Draw();
Present();
const Nav n = nav.Read();
if (n.back || n.options || ResumeNow(n))
{
WaitForRelease();
return;
}
pm::MoveSelection(n, sel, int(ids.size()));
if (!(n.left || n.right || n.ok))
continue;
const Row id = ids[size_t(sel)];
uint32_t ram_size = 0;
const uint32_t* ram = emu::Ram(&ram_size);
std::string error;
auto step = [&](search::Test test) {
if (!n.ok || !ram)
return;
const uint64_t before = s.Count();
s.Filter(ram, test, s_number);
const uint64_t left = s.Count();
OrbisLog("[cheats] search step %d: %llu -> %llu", s.Steps(), (unsigned long long)before, (unsigned long long)left);
toast.Show(left == 0 ? "None left: undo the last step, or start again"
: left <= 50 ? search::Thousands(left) + " left: open Results"
: search::Thousands(left) + " left: play on, then narrow it down",
3.0);
};
switch (id)
{
case Q_SIZE: s_size = (s_size + (n.left ? 2 : 1)) % 3; break;
case Q_NUMBER:
if (n.ok)
{
KeyboardNumber("The value", s_number, max, &error);
nav = NavReader();
}
else
s_number = n.left ? (s_number > 0 ? s_number - 1 : 0) : (s_number < max ? s_number + 1 : max);
break;
case Q_START:
case Q_START_ANY:
if (n.ok && ram)
{
s.Start(ram, ram_size, kBytes[s_size]);
OrbisLog("[cheats] search: %s values, %u KiB of RAM%s", BitsName(s.Bytes()), unsigned(ram_size >> 10),
id == Q_START ? ", for the value" : "");
if (id == Q_START)
step(search::Test::Equal);
else
toast.Show(search::Thousands(s.Count()) + " values: play until it changes", 3.0);
}
break;
case Q_RESULTS:
if ((n.ok || n.right) && s.Count() > 0)
{
ResultsMenu();
if (g_resume)
return;
nav = NavReader();
page.Redraw();
}
break;
case Q_EQUAL: step(search::Test::Equal); break;
case Q_NOT_EQUAL: step(search::Test::NotEqual); break;
case Q_UP_BY: step(search::Test::IncreasedBy); break;
case Q_DOWN_BY: step(search::Test::DecreasedBy); break;
case Q_CHANGED: step(search::Test::Changed); break;
case Q_UNCHANGED: step(search::Test::Unchanged); break;
case Q_UP: step(search::Test::Increased); break;
case Q_DOWN: step(search::Test::Decreased); break;
case Q_UNDO:
if (n.ok && s.Undo())
toast.Show("Back one step: " + search::Thousands(s.Count()) + " left");
break;
case Q_NEW:
if (n.ok)
s.Clear();
break;
}
if (!error.empty())
toast.Show(error, 4.0);
}
}
// The running game's cheats: the cheat search, codes to type in, the player's own cheats and then the database's,
// each one off / on, or off / one of its choices; what to know about the one chosen under the list.
void CheatsMenu()
{
enum
{
C_SEARCH,
C_TYPE,
C_FIRST, // the cheats
};
const std::string game = emu::CheatsGameName();
const pm::Panel panel{pm::Glyph(pm::kStar), "Cheats", "PAUSED", game.empty() ? emu::GameName() : game, "",
{{icon::Cross, "Change"}, {icon::Square, "Edit"}, {icon::Circle, "Back"}}, nullptr};
const int visible = pm::kVisible - 3; // room for the note
NavReader nav;
int sel = 0, top = 0;
PageBase page;
for (;;)
{
std::vector<cheats::Cheat>& list = emu::Cheats();
const int count = C_FIRST + int(list.size());
sel = std::min(sel, count - 1);
page.Draw(panel);
const search::Search& s = emu::CheatSearch();
std::vector<pm::SettingRow> rows = {
{pm::Drawn(gfx::Icon::Search), "Search for a Cheat", false, pm::Kind::Page,
s.Active() ? search::Thousands(s.Count()) + " left" : "Start one", false, 0, true},
{pm::Glyph(pm::kKeyboard), "Type In Codes", false, pm::Kind::Page, "GameShark", false, 0, true},
};
for (const cheats::Cheat& c : list)
rows.push_back({pm::Glyph(c.mine ? pm::kMemo : pm::kStar), cheats::DisplayName(c.name), false, pm::Kind::Choice,
CheatState(c), false, 0, true});
pm::DrawRows(rows, sel, top, visible);
const int note_y = pm::RY0 + visible * pm::RH + 10;
if (sel == C_SEARCH)
pm::DrawNote(note_y, "CHEAT SEARCH",
"Find where the game keeps a number, like your lives or health, and make a cheat that keeps it where you "
"want. You play, then tell the search what happened to the number, a few times over.");
else if (sel == C_TYPE)
pm::DrawNote(note_y, "YOUR OWN CODES",
"Type GameShark codes with the PS5 keyboard, like 8033B21D 0064. Your cheats are kept in "
"/data/mupen64plus/cheats, where you can also put them from a PC.");
else
{
const cheats::Cheat& c = list[size_t(sel - C_FIRST)];
if (c.mine)
pm::DrawNote(note_y, "YOUR CHEAT", "Codes: " + cheats::CodesText(c) + ". Square renames it, changes its codes or deletes it.");
else
pm::DrawNote(note_y, c.note.empty() ? "NOTE" : "ABOUT THIS CHEAT",
c.note.empty() ? "Cheats can make a game misbehave or crash. If one does, turn it off (or reset the "
"game). Square makes a copy of it you can change."
: c.note);
}
Present();
const Nav n = nav.Read();
if (n.back || n.options || ResumeNow(n))
{
WaitForRelease();
return;
}
pm::MoveSelection(n, sel, count);
if (n.square && sel >= C_FIRST)
{
cheats::Cheat copy = list[size_t(sel - C_FIRST)];
if (copy.mine)
EditCheatMenu(sel - C_FIRST, copy);
else
{
// the database's stays as it is: the copy is the player's own
copy.name = emu::UniqueCheatName(cheats::DisplayName(copy.name) + " (edited)");
copy.mine = true;
EditCheatMenu(-1, copy);
}
if (g_resume)
return;
nav = NavReader();
page.Redraw();
continue;
}
if (!(n.left || n.right || n.ok))
continue;
if (sel == C_SEARCH || sel == C_TYPE)
{
if (n.left)
continue;
if (sel == C_SEARCH)
SearchMenu();
else
EditCheatMenu(-1, cheats::Cheat());
if (g_resume)
return;
nav = NavReader();
page.Redraw();
continue;
}
const cheats::Cheat& c = list[size_t(sel - C_FIRST)];
emu::SetCheat(sel - C_FIRST, (c.state + (n.left ? c.States() - 1 : 1)) % c.States());
}
}
} // namespace
PauseAction PauseMenu()
{
Settings& cfg = Config();
NavReader nav;
g_resume = false;
// the rows: an S_* setting by its number, or one of these
enum
{
@@ -1320,8 +2009,7 @@ PauseAction PauseMenu()
for (const cheats::Cheat& c : emu::Cheats())
on += c.state ? 1 : 0;
snprintf(text, sizeof(text), "%d on", on);
return {pm::Glyph(pm::kStar), "Cheats", false, pm::Kind::Page,
emu::Cheats().empty() ? "None for this game" : on ? text : "Off", false, 0, !emu::Cheats().empty()};
return {pm::Glyph(pm::kStar), "Cheats", false, pm::Kind::Page, on ? text : "Off", false, 0, true};
}
case R_QUIT:
return {pm::Glyph(pm::kDoor), "Quit Mupen64Plus", false, pm::Kind::Action, "Close the app", false, 0, true};
@@ -1404,9 +2092,11 @@ PauseAction PauseMenu()
case R_SLOT: cfg.state_slot = (cfg.state_slot + (dir < 0 ? 9 : 1)) % 10; break;
case R_SPEED: emu::SetFastForward(!emu::FastForward()); break;
case R_CHEATS:
if (dir > 0 && !emu::Cheats().empty())
if (dir > 0)
{
CheatsMenu();
if (g_resume)
return PauseAction::Resume;
nav = NavReader();
dirty = true;
}
@@ -1420,6 +2110,8 @@ PauseAction PauseMenu()
const int s = ids[size_t(sel)];
ChangeSetting(s, dir);
cfg.Save();
if (g_resume)
return PauseAction::Resume; // the touchpad in Controller Layout
if (s == S_LAYOUT)
nav = NavReader();
dirty = true; // the picture (aspect, filters) or a page drawn over it
+153
View File
@@ -0,0 +1,153 @@
// Mupen64Plus PS5 frontend: the cheat search (fe_search.h).
//
// 8 MiB of RAM is 8,388,608 8-bit values: a step over all of them takes a few milliseconds. The search keeps one
// bit per value still in it (1 MiB at most) and a copy of the RAM from the last step, and the same once more for
// Undo.
//
// SPDX-License-Identifier: MIT
#include "fe_search.h"
#include <cstring>
namespace fe
{
namespace search
{
uint32_t Read(const uint32_t* ram, uint32_t address, int bytes)
{
// the core keeps the RAM as host-order 32-bit words: byte n of the N64's memory is byte n ^ 3 of the array
const uint8_t* b = reinterpret_cast<const uint8_t*>(ram);
switch (bytes)
{
case 1: return b[address ^ 3];
case 2:
{
uint16_t v;
memcpy(&v, b + (address ^ 2), 2);
return v;
}
default: return ram[address >> 2];
}
}
uint32_t MaxValue(int bytes)
{
return bytes == 1 ? 0xFFu : bytes == 2 ? 0xFFFFu : 0xFFFFFFFFu;
}
std::string Thousands(uint64_t n)
{
std::string digits = std::to_string(n), out;
for (size_t i = 0; i < digits.size(); i++)
{
if (i && (digits.size() - i) % 3 == 0)
out += ',';
out += digits[i];
}
return out;
}
void Search::Start(const uint32_t* ram, uint32_t ram_size, int bytes)
{
Clear();
m_bytes = bytes == 2 || bytes == 4 ? bytes : 1;
m_size = ram_size & ~3u;
const uint64_t n = m_size / uint32_t(m_bytes);
m_bits.assign(size_t((n + 63) / 64), ~0ull);
if (n % 64)
m_bits.back() = (1ull << (n % 64)) - 1;
m_count = n;
m_before.assign(ram, ram + m_size / 4);
}
void Search::Filter(const uint32_t* ram, Test test, uint32_t number)
{
if (!Active())
return;
m_undo_bits = m_bits;
m_undo_before = m_before;
m_undo_count = m_count;
const uint32_t mask = MaxValue(m_bytes);
number &= mask;
const uint32_t* before = m_before.data();
uint64_t count = 0;
for (size_t w = 0; w < m_bits.size(); w++)
{
uint64_t bits = m_bits[w];
while (bits)
{
const int i = __builtin_ctzll(bits);
bits &= bits - 1;
const uint32_t address = uint32_t((w * 64 + size_t(i)) * size_t(m_bytes));
const uint32_t now = Read(ram, address, m_bytes), was = Read(before, address, m_bytes);
bool keep;
switch (test)
{
case Test::Equal: keep = now == number; break;
case Test::NotEqual: keep = now != number; break;
case Test::Greater: keep = now > number; break;
case Test::Less: keep = now < number; break;
case Test::Changed: keep = now != was; break;
case Test::Unchanged: keep = now == was; break;
case Test::Increased: keep = now > was; break;
case Test::Decreased: keep = now < was; break;
case Test::IncreasedBy: keep = ((now - was) & mask) == number && now != was; break;
case Test::DecreasedBy: keep = ((was - now) & mask) == number && now != was; break;
default: keep = true; break;
}
if (keep)
count++;
else
m_bits[w] &= ~(1ull << i);
}
}
m_count = count;
m_before.assign(ram, ram + m_size / 4);
m_steps++;
}
bool Search::Undo()
{
if (!CanUndo())
return false;
m_bits.swap(m_undo_bits);
m_before.swap(m_undo_before);
m_count = m_undo_count;
m_undo_bits.clear();
m_undo_before.clear();
m_steps = m_steps > 0 ? m_steps - 1 : 0;
return true;
}
void Search::Clear()
{
m_bytes = 0;
m_size = 0;
m_count = 0;
m_steps = 0;
std::vector<uint64_t>().swap(m_bits);
std::vector<uint32_t>().swap(m_before);
std::vector<uint64_t>().swap(m_undo_bits);
std::vector<uint32_t>().swap(m_undo_before);
m_undo_count = 0;
}
std::vector<Search::Result> Search::Results(size_t max) const
{
std::vector<Result> out;
for (size_t w = 0; w < m_bits.size() && out.size() < max; w++)
{
uint64_t bits = m_bits[w];
while (bits && out.size() < max)
{
const int i = __builtin_ctzll(bits);
bits &= bits - 1;
const uint32_t address = uint32_t((w * 64 + size_t(i)) * size_t(m_bytes));
out.push_back({address, Read(m_before.data(), address, m_bytes)});
}
}
return out;
}
} // namespace search
} // namespace fe
+76
View File
@@ -0,0 +1,76 @@
// Mupen64Plus PS5 frontend: the cheat search -- finding where a game keeps a value (lives, health, money) in the
// N64's RAM, by narrowing every address down to the ones that changed the way the value did.
//
// A search looks at every 1, 2 or 4-byte value of the RAM (8, 16 or 32-bit; at addresses of that alignment).
// Start() remembers the RAM; each Filter() keeps the addresses whose value passes the test -- against a number,
// or against what it was the last time -- and remembers the RAM again. Undo() goes one step back.
//
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace fe
{
namespace search
{
enum class Test
{
Equal, // == the number
NotEqual,
Greater, // > the number
Less,
Changed, // than the last time
Unchanged,
Increased,
Decreased,
IncreasedBy, // by the number
DecreasedBy,
};
// the value at N64 RAM address `address` (bytes 1, 2 or 4), from RAM in the core's layout (n64_bridge.h)
uint32_t Read(const uint32_t* ram, uint32_t address, int bytes);
// the biggest value of that size
uint32_t MaxValue(int bytes);
// 1234567 -> "1,234,567"
std::string Thousands(uint64_t n);
class Search
{
public:
bool Active() const { return m_bytes != 0; }
int Bytes() const { return m_bytes; }
uint64_t Count() const { return m_count; }
int Steps() const { return m_steps; } // filters since Start
bool CanUndo() const { return !m_undo_bits.empty(); }
// a new search over the RAM (ram_size bytes) for values of `bytes` bytes: every address is in it
void Start(const uint32_t* ram, uint32_t ram_size, int bytes);
// keeps the addresses that pass `test` (with `number` where the test has one); the RAM is remembered again
void Filter(const uint32_t* ram, Test test, uint32_t number);
bool Undo();
void Clear();
struct Result
{
uint32_t address; // in the RAM, 0..size-1
uint32_t before; // the value the last step saw
};
// the first `max` addresses still in the search, lowest first
std::vector<Result> Results(size_t max) const;
private:
int m_bytes = 0;
uint32_t m_size = 0;
uint64_t m_count = 0;
int m_steps = 0;
std::vector<uint64_t> m_bits; // one bit per address / bytes
std::vector<uint32_t> m_before; // the RAM at the last step
std::vector<uint64_t> m_undo_bits;
std::vector<uint32_t> m_undo_before;
uint64_t m_undo_count = 0;
};
} // namespace search
} // namespace fe
+72
View File
@@ -6,6 +6,7 @@
// N64PS5_HOST_DUMP_DIR=dir where (default .)
// N64PS5_HOST_MAX_FLIPS=n abort after n flips (a hang guard)
// N64PS5_HOST_REALTIME=1 flips wait 1/60 s
// N64PS5_HOST_IME="text[|text...]" what is typed on the keyboard, in turn ("!" cancels)
//
// SPDX-License-Identifier: MIT
@@ -198,6 +199,77 @@ int sceKernelUsleep(unsigned int usec)
{
return usleep(usec);
}
// ---- libSceSysmodule (only the IME dialog loads) and libSceImeDialog, faked --------------------------------
// N64PS5_HOST_IME="text[|text...]": what the player types in each keyboard opened, in turn ("!" cancels it; with
// none left, it is canceled). The keyboard stays up for three status calls.
static std::vector<std::string> g_ime_texts;
static size_t g_ime_next = 0;
static int g_ime_polls = -1; // -1: not open
static bool g_ime_cancel = false;
static SceImeDialogParam g_ime_param;
int sceImeDialogInit(const SceImeDialogParam* p, const void*)
{
if (g_ime_polls >= 0)
return (int)0x80bc0001; // busy
static bool parsed = false;
if (!parsed)
{
parsed = true;
if (const char* s = getenv("N64PS5_HOST_IME"))
{
std::string str = s;
size_t pos = 0;
for (;;)
{
const size_t end = str.find('|', pos);
g_ime_texts.push_back(str.substr(pos, end == std::string::npos ? std::string::npos : end - pos));
if (end == std::string::npos)
break;
pos = end + 1;
}
}
}
g_ime_param = *p;
g_ime_polls = 0;
std::string title;
for (const uint16_t* t = p->title; t && *t; t++)
title += *t < 0x80 ? char(*t) : '?';
const std::string text = g_ime_next < g_ime_texts.size() ? g_ime_texts[g_ime_next++] : "!";
g_ime_cancel = text == "!";
if (!g_ime_cancel)
{
size_t i = 0;
for (; i < text.size() && i < p->maxTextLength; i++)
p->inputTextBuffer[i] = uint16_t((unsigned char)text[i]);
p->inputTextBuffer[i] = 0;
}
fprintf(stderr, "[host] keyboard \"%s\" (type %d, max %u): %s\n", title.c_str(), p->type, p->maxTextLength,
g_ime_cancel ? "canceled" : text.c_str());
return 0;
}
int sceImeDialogGetStatus(void)
{
if (g_ime_polls < 0)
return SCE_IME_DIALOG_STATUS_NONE;
return ++g_ime_polls > 3 ? SCE_IME_DIALOG_STATUS_FINISHED : SCE_IME_DIALOG_STATUS_RUNNING;
}
int sceImeDialogGetResult(SceImeDialogResult* r)
{
r->endstatus = g_ime_cancel ? SCE_IME_DIALOG_END_STATUS_USER_CANCELED : SCE_IME_DIALOG_END_STATUS_OK;
return 0;
}
int sceImeDialogTerm(void)
{
g_ime_polls = -1;
return 0;
}
int sceSysmoduleLoadModule(uint16_t id)
{
return id == SCE_SYSMODULE_IME_DIALOG ? 0 : (int)0x805a1000; // not found
}
int sceSystemServiceHideSplashScreen(void)
{
fprintf(stderr, "[host] splash screen hidden\n");
+29
View File
@@ -0,0 +1,29 @@
// Mupen64Plus PS5: the N64's RAM for the cheat search (n64_bridge.h).
// SPDX-License-Identifier: MIT
#include "n64_bridge.h"
#include "device/device.h"
#include "main/main.h"
#include "osal/preproc.h"
const uint32_t* n64ps5_ram(uint32_t* size)
{
*size = (uint32_t)g_dev.rdram.dram_size;
return g_dev.rdram.dram;
}
void n64ps5_ram_write(uint32_t address, uint32_t value, int bytes)
{
address &= 0x00ffffff;
if (address + (uint32_t)bytes > g_dev.rdram.dram_size)
return;
uint8_t* dram = (uint8_t*)g_dev.rdram.dram;
switch (bytes)
{
case 1: dram[address ^ S8] = (uint8_t)value; break;
case 2: *(uint16_t*)(dram + (address ^ S16)) = (uint16_t)value; break;
default: *(uint32_t*)(dram + address) = value; break;
}
invalidate_r4300_cached_code(&g_dev.r4300, 0x80000000u | address, (size_t)bytes);
}
+8
View File
@@ -125,6 +125,14 @@ void n64ps5_input_set_options(const n64ps5_input_options* opt);
// buttons.
void n64ps5_input_block(bool block);
// ---- the N64's RAM (core/memory_ps5.c), for the cheat search ---------------------------------------------
// The RDRAM as the core keeps it: 32-bit words in the host's byte order (byte n of the N64's memory is byte
// n ^ 3 here). `size` gets its size, 4 or 8 MiB (the Expansion Pak).
const uint32_t* n64ps5_ram(uint32_t* size);
// Writes `value` (`bytes` 1, 2 or 4, at an address of that alignment) the way a cheat code does, so recompiled
// code there is dropped too.
void n64ps5_ram_write(uint32_t address, uint32_t value, int bytes);
#ifdef __cplusplus
}
#endif
+6
View File
@@ -0,0 +1,6 @@
## New
- Cheat search: find where a game keeps a number (lives, health, money) and make your own cheat from it. In the pause menu, Cheats → Search for a Cheat: search for the value the game shows, or without a value for things like a health bar. Then play, pause, and pick what happened to it (changed, went up, went down by the value...) until only a few addresses are left. In Results, keep one at the value you want as a cheat, or set it just once. Works on 8, 16 and 32-bit values; Undo takes back a step.
- Type in your own GameShark codes with the PS5's on-screen keyboard (Cheats → Type In Codes).
- Your own cheats come first in the Cheats list and are kept with the game. Square renames one, changes its codes or deletes it; on a cheat of the list, Square makes your own copy to fix. They are saved in /data/mupen64plus/cheats/<game>.custom.txt, where you can also add codes from a PC.
- Cheats is in the pause menu for every game now, also the ones the cheat list has nothing for.
- The touchpad goes back to the game from any page of the pause menu (Cheats, the cheat search, Controller Layout).