mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-26 13:36:45 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a915117cad | ||
|
|
6bca99ff6e | ||
|
|
129d055f99 |
No files matched your search
+5
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(¶m, 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(¶m, nullptr);
|
||||
if (r != 0)
|
||||
{
|
||||
// one left open (a crash of the menu around it): end that one and try again
|
||||
sceImeDialogTerm();
|
||||
r = sceImeDialogInit(¶m, 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user