mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-26 15:02:42 +01:00
Compare commits
11
Commits
00.007.003
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a915117cad | ||
|
|
6bca99ff6e | ||
|
|
129d055f99 | ||
|
|
89786fd668 | ||
|
|
f97346904d | ||
|
|
0ec5920b37 | ||
|
|
0ca7ea5721 | ||
|
|
240baec05e | ||
|
|
7b2a21208e | ||
|
|
9e3b6c1596 | ||
|
|
bca88bf154 |
No files matched your search
@@ -72,7 +72,8 @@ The GPU renderer needs RADV built first (mihawk-99/PS5_Vulkan, in WSL `/root/gpu
|
||||
Releasing:
|
||||
|
||||
1. Raise `VERSION` in `ps5/Makefile`, and set the matching `contentVersion` in `ps5/app/sce_sys/param.json`
|
||||
(0.7.0 → `00.007.000`; the build stops if they differ). That number is also the release tag.
|
||||
(0.7.0 → `00.007.000`; the build stops if they differ). The release tag is the version as people write it
|
||||
(`0.7.4`; up to 0.7.3 it was the `contentVersion`, `00.007.003`). The app reads both as numbers.
|
||||
2. Write `release-notes/<contentVersion>.md`: a few short lines, shown on the console in the update question.
|
||||
3. Commit and push, then run `release.bat`. It builds, creates the release with `Mupen64PlusPS5.zip`, and checks
|
||||
GitHub's digest against the local zip.
|
||||
|
||||
+13
-3
@@ -33,7 +33,7 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
PS5_HOST ?= ps5
|
||||
PS5_PORT ?= 9021
|
||||
VERSION ?= 0.7.3
|
||||
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_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
|
||||
@@ -155,6 +157,13 @@ $(GAMEDB): tools/make_n64db.py $(M64_INI) $(BOXARTS)
|
||||
%/port/frontend/fe_games.o: EXTRA_FLAGS = -DGAMEDB_TSV='"$(abspath $(GAMEDB))"'
|
||||
# the ROM catalog itself goes into fe_emu.o (written to /data/mupen64plus/data for the core)
|
||||
%/port/frontend/fe_emu.o: EXTRA_FLAGS = -DM64P_INI_PATH='"$(abspath $(M64_INI))"'
|
||||
# the core's cheat database (1.2 MB of text), compressed into fe_cheats.o: 4 bytes of size, then zlib
|
||||
CHEATS_Z := build/gen/mupencheat.z
|
||||
$(CHEATS_Z): $(M64)/data/mupencheat.txt
|
||||
@mkdir -p $(dir $@)
|
||||
python3 -c "import struct, sys, zlib; d = open(sys.argv[1], 'rb').read(); \
|
||||
open(sys.argv[2], 'wb').write(struct.pack('<I', len(d)) + zlib.compress(d, 9))" $< $@
|
||||
%/port/frontend/fe_cheats.o: EXTRA_FLAGS = -DCHEATS_Z='"$(abspath $(CHEATS_Z))"'
|
||||
# stb_image in fe_covers.o has warnings of its own
|
||||
%/port/frontend/fe_covers.o: EXTRA_FLAGS = -Wno-error -Wno-sign-compare -Wno-unused-but-set-variable -Wno-implicit-fallthrough
|
||||
# the UI fonts (PS5SX2's: PCSX2's Roboto Regular and PromptFont) built into fe_text.o
|
||||
@@ -597,6 +606,7 @@ $(HOST_OUT)/mupen64plus-ps5-host: $(HOST_CORE_OBJ) $(HOST_EMU_OBJ) $(HOST_ZLIB_O
|
||||
# the files built into fe_games.o / fe_emu.o
|
||||
$(PS5_OUT)/port/frontend/fe_games.o $(HOST_OUT)/port/frontend/fe_games.o: $(GAMEDB)
|
||||
$(PS5_OUT)/port/frontend/fe_emu.o $(HOST_OUT)/port/frontend/fe_emu.o: $(M64_INI)
|
||||
$(PS5_OUT)/port/frontend/fe_cheats.o $(HOST_OUT)/port/frontend/fe_cheats.o: $(CHEATS_Z)
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
+39
-11
@@ -143,7 +143,8 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
|
||||
## Controls
|
||||
|
||||
**In a game** (buttons by position, as on the N64 pad)
|
||||
**In a game** (the default layout: buttons by position, as on the N64 pad; Settings → Controller layout changes
|
||||
it, for every game or, from the pause menu, for one game)
|
||||
|
||||
| PS5 | N64 |
|
||||
|---|---|
|
||||
@@ -155,14 +156,37 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| right stick | C buttons |
|
||||
| Triangle / Circle | C-up / C-down |
|
||||
| OPTIONS | Start |
|
||||
| **touchpad click** or **L3 + R3** | pause menu |
|
||||
| **touchpad click** | pause menu |
|
||||
| **R3** (press the right stick) | save the state in the current slot |
|
||||
| **L3** (press the left stick) | load the state of the current slot |
|
||||
|
||||
**The pause menu** has:
|
||||
**The pause menu** (the touchpad) is a panel over the paused game, drawn on the CPU with the anti-aliased shapes
|
||||
of `frontend/fe_draw.cpp`; Settings on the game list and the Controller Layout and Cheats pages share its look
|
||||
(`fe_menu.cpp`, namespace `pm`). Its footer's buttons work anywhere in it: touchpad resume, Square reset the
|
||||
game (after a question), Triangle back to the game list, Cross confirm, Circle back (resume). Its rows:
|
||||
|
||||
- resume, save state, load state, and the state slot (0–9);
|
||||
- save state and load state (Cross), and the state slot (0–9);
|
||||
- **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. 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;
|
||||
- reset, back to the game list, and quit.
|
||||
- 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);
|
||||
- quit (Cross).
|
||||
|
||||
**Settings** (marked `*` when they apply from the next game start):
|
||||
|
||||
@@ -183,9 +207,11 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
|
||||
| Stick dead zone | 0–30% |
|
||||
| Invert left stick | turns the analog stick's X (left/right), Y (up/down) or both round; Off by default |
|
||||
| Invert right stick | the same for the right stick's C buttons (Triangle / Circle stay C-up / C-down); Off by default |
|
||||
| Controller layout | what each DualSense control presses on the N64 pad: Cross, Circle, Square, Triangle, L1, R1, L2, R2 (separately), OPTIONS, the D-pad and the right stick's four directions, each set to an N64 button or nothing (the left stick stays the analog stick). From the game list it is every game's layout (in `mupen64plus-ps5.ini`); from the pause menu, the running game's own (`layouts/<ROM file>.txt`, gone again when it matches every game's) |
|
||||
| Continue where you left off | Ask (default): leaving a game from the pause menu saves where you were (`states/<ROM file>.resume`), and starting it again asks "Continue where you left off?" (✕ continue, □ start over, ○ back). Off: neither |
|
||||
| Game list view | Shelf (3D, default), Grid or List; the touchpad on the game list switches too |
|
||||
| Show FPS | the speed (`VI/s`, 100% = full speed) and, below it, where the emulation thread's time goes: `cpu` (VR4300 interpreter), `rsp` (rsp-hle + cxd4), `rdp` (the renderer: angrylion's rasteriser incl. waiting for its threads, or handing commands to the GPU), `vi` (the VI filter, or reading the GPU's picture back), `idle` (waiting for the TV: spare time). Also logged every 10 s in `boot.log` |
|
||||
| Volume | the game's sound, 0–100% (default 100%) |
|
||||
| Sound, Download covers | |
|
||||
| Check for updates | ask at start-up when a newer GitHub release exists (see [Updates](#updates)) |
|
||||
|
||||
@@ -243,7 +269,7 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
Interpreter.
|
||||
- **The RSP is the next bottleneck.** cxd4 interprets every graphics microcode instruction (GoldenEye:
|
||||
~30% of the time); audio goes through rsp-hle (Audio processing: Fast).
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, a cheats menu, netplay.
|
||||
- **Unsupported:** 64DD, the Transfer Pak, screenshots, netplay.
|
||||
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
|
||||
|
||||
## Building
|
||||
@@ -298,16 +324,18 @@ 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
|
||||
generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`. On the host,
|
||||
`N64PS5_JIT=direct` exercises the PS5's direct-memory way (an anonymous RWX mapping stands in for it).
|
||||
|
||||
**Art.** `tools/make_art.py app/sce_sys` redraws `icon0.png`, `background-source.png` and the BC7
|
||||
`pic0.dds`/`pic1.dds`.
|
||||
**Art.** The home screen's icon and background are pictures in `app/art/` (`icon.png`, `background.png`).
|
||||
`tools/make_art.py app/sce_sys --images app/art/icon.png app/art/background.png` fits them to `icon0.png`
|
||||
(512x512) and `background-source.png` (3840x2160) and writes the BC7 `pic0.dds`/`pic1.dds`; without
|
||||
`--images` it draws the old art itself.
|
||||
|
||||
## Source layout
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 460 KiB After Width: | Height: | Size: 7.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 314 KiB |
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99064",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
|
||||
"contentVersion": "00.007.003",
|
||||
"contentVersion": "00.007.005",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,334 @@
|
||||
// Mupen64Plus PS5 frontend: the cheat database (fe_cheats.h).
|
||||
//
|
||||
// mupencheat.txt, as the core ships it (converted from Project64's database by Gent):
|
||||
//
|
||||
// crc 635A2BFF-8B022326-C:45 the ROM header's CRC1, CRC2 and country code
|
||||
// gn Super Mario 64 (U)
|
||||
// cn Infinite Lives a cheat (a '\' puts it in a group)
|
||||
// cd Some explanation optional
|
||||
// 8033B21D 0064 its codes: address, value
|
||||
// 8013CD0E ???? 0001:"1 Lap",... a value to choose from a list
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fe_cheats.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
|
||||
#include "zlib.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifndef CHEATS_Z
|
||||
#error "CHEATS_Z: the compressed mupencheat.txt (the Makefile builds it)"
|
||||
#endif
|
||||
|
||||
#define FE_INCBIN(sym, path) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".balign 16\n" \
|
||||
".global " #sym "_begin\n" #sym "_begin:\n" \
|
||||
".incbin \"" path "\"\n" \
|
||||
".global " #sym "_end\n" #sym "_end:\n" \
|
||||
".previous\n"); \
|
||||
extern "C" const unsigned char sym##_begin[]; \
|
||||
extern "C" const unsigned char sym##_end[];
|
||||
|
||||
// 4 bytes: the text's size (little endian), then the text, zlib-compressed
|
||||
FE_INCBIN(n64ps5_cheats_z, CHEATS_Z)
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace cheats
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string Inflate()
|
||||
{
|
||||
const unsigned char* z = n64ps5_cheats_z_begin;
|
||||
const size_t zsize = size_t(n64ps5_cheats_z_end - n64ps5_cheats_z_begin);
|
||||
if (zsize < 4)
|
||||
return "";
|
||||
uLongf size = uLongf(z[0]) | uLongf(z[1]) << 8 | uLongf(z[2]) << 16 | uLongf(z[3]) << 24;
|
||||
std::string text(size, '\0');
|
||||
if (uncompress(reinterpret_cast<Bytef*>(&text[0]), &size, z + 4, uLong(zsize - 4)) != Z_OK)
|
||||
{
|
||||
OrbisLog("[cheats] the built-in cheat list is damaged");
|
||||
return "";
|
||||
}
|
||||
text.resize(size);
|
||||
return text;
|
||||
}
|
||||
|
||||
// a code line: "8033B21D 0064" or "8013CD0E ???? 0001:"1 Lap",0002:"2 Laps""
|
||||
bool ParseCode(const std::string& line, Cheat& c)
|
||||
{
|
||||
char* end = nullptr;
|
||||
const unsigned long address = strtoul(line.c_str(), &end, 16);
|
||||
if (end == line.c_str() || *end != ' ')
|
||||
return false;
|
||||
const std::string rest = end + 1;
|
||||
if (rest.compare(0, 4, "????") == 0)
|
||||
{
|
||||
c.addresses.push_back(uint32_t(address));
|
||||
c.values.push_back(-1);
|
||||
// the choices: hex value, ':', quoted name, separated by commas. Every "????" of a cheat takes the same
|
||||
// choice; the list is on one of its lines (28 cheats leave the first one's empty).
|
||||
std::vector<std::pair<std::string, int>> options;
|
||||
size_t at = 4;
|
||||
while (at < rest.size())
|
||||
{
|
||||
while (at < rest.size() && (rest[at] == ' ' || rest[at] == ','))
|
||||
at++;
|
||||
const size_t colon = rest.find(':', at);
|
||||
const size_t q1 = colon == std::string::npos ? colon : rest.find('"', colon);
|
||||
const size_t q2 = q1 == std::string::npos ? q1 : rest.find('"', q1 + 1);
|
||||
if (q2 == std::string::npos)
|
||||
break;
|
||||
const int value = int(strtol(rest.substr(at, colon - at).c_str(), nullptr, 16));
|
||||
options.emplace_back(rest.substr(q1 + 1, q2 - q1 - 1), value);
|
||||
at = q2 + 1;
|
||||
}
|
||||
if (c.options.empty())
|
||||
c.options = std::move(options);
|
||||
return true;
|
||||
}
|
||||
c.addresses.push_back(uint32_t(address));
|
||||
c.values.push_back(int(strtol(rest.c_str(), nullptr, 16)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// codes the core applies only while the GameShark's button is held
|
||||
bool NeedsGameSharkButton(const Cheat& c)
|
||||
{
|
||||
for (uint32_t a : c.addresses)
|
||||
{
|
||||
const uint32_t type = a & 0xFF000000u;
|
||||
if (type == 0x88000000u || type == 0x89000000u || type == 0xA8000000u || type == 0xA9000000u ||
|
||||
(type >= 0xD8000000u && type <= 0xDB000000u))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// a cheat the menu can offer: it has codes, needs no GameShark button, and a choice has its list
|
||||
bool Usable(const Cheat& c)
|
||||
{
|
||||
if (c.addresses.empty() || NeedsGameSharkButton(c))
|
||||
return false;
|
||||
for (int v : c.values)
|
||||
if (v < 0 && c.options.empty())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game)
|
||||
{
|
||||
std::vector<Cheat> out;
|
||||
const std::string text = Inflate();
|
||||
char want[48];
|
||||
snprintf(want, sizeof(want), "crc %08X-%08X-C:%02X", unsigned(crc1), unsigned(crc2), unsigned(country));
|
||||
size_t at = text.find(want);
|
||||
if (at == std::string::npos)
|
||||
return out;
|
||||
at = text.find('\n', at);
|
||||
int skipped = 0;
|
||||
Cheat* cur = nullptr;
|
||||
while (at != std::string::npos && at + 1 < text.size())
|
||||
{
|
||||
const size_t start = at + 1;
|
||||
at = text.find('\n', start);
|
||||
std::string line = text.substr(start, at == std::string::npos ? std::string::npos : at - start);
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
if (line.compare(0, 4, "crc ") == 0)
|
||||
break; // the next game
|
||||
if (line.compare(0, 3, "gn ") == 0)
|
||||
{
|
||||
if (game)
|
||||
*game = line.substr(3);
|
||||
}
|
||||
else if (line.compare(0, 4, " cn ") == 0)
|
||||
{
|
||||
if (cur && !Usable(*cur))
|
||||
{
|
||||
skipped++;
|
||||
out.pop_back();
|
||||
}
|
||||
out.emplace_back();
|
||||
cur = &out.back();
|
||||
cur->name = line.substr(4);
|
||||
}
|
||||
else if (cur && line.compare(0, 5, " cd ") == 0)
|
||||
cur->note = line.substr(5);
|
||||
else if (cur && line.size() > 2 && line[0] == ' ' && line[1] == ' ')
|
||||
ParseCode(line.substr(2), *cur);
|
||||
}
|
||||
if (cur && !Usable(*cur))
|
||||
{
|
||||
skipped++;
|
||||
out.pop_back();
|
||||
}
|
||||
if (skipped)
|
||||
OrbisLog("[cheats] %d cheat(s) left out (the GameShark's button, or no codes)", skipped);
|
||||
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;
|
||||
for (size_t i = s.find('\\'); i != std::string::npos; i = s.find('\\', i + 2))
|
||||
s.replace(i, 1, ": ");
|
||||
return s;
|
||||
}
|
||||
} // namespace cheats
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,48 @@
|
||||
// Mupen64Plus PS5 frontend: cheats -- the GameShark codes of mupen64plus-core's data/mupencheat.txt (built into
|
||||
// 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
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace cheats
|
||||
{
|
||||
struct Cheat
|
||||
{
|
||||
std::string name; // as in the file: "Play As\Player 1" is "Player 1" in the group "Play As"
|
||||
std::string note; // what to know before using it (its "cd" line), often empty
|
||||
std::vector<uint32_t> addresses;
|
||||
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; }
|
||||
};
|
||||
|
||||
// The cheats of the game whose ROM header has these CRCs and country code (the database's "crc" lines), in
|
||||
// the database's order; `game` gets its name there. Cheats that need the GameShark's button are left out:
|
||||
// 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
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,311 @@
|
||||
// Mupen64Plus PS5 frontend: anti-aliased shapes (fe_draw.h). Rounded rectangles and circles use their signed
|
||||
// distance (coverage = 0.5 - distance), the triangles 4 x 4 samples per pixel: menus only, a few per frame.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fe_draw.h"
|
||||
|
||||
#include "ProsperoVideo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace gfx
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr int W = ps5video::kWidth;
|
||||
constexpr int H = ps5video::kHeight;
|
||||
|
||||
inline float Clamp01(float v)
|
||||
{
|
||||
return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
|
||||
}
|
||||
|
||||
inline void Put(int x, int y, uint32_t color, float coverage, int alpha)
|
||||
{
|
||||
if (coverage <= 0.0f || x < 0 || y < 0 || x >= W || y >= H)
|
||||
return;
|
||||
uint32_t& p = ps5video::Surface()[size_t(y) * W + x];
|
||||
p = Blend(p, color, int(coverage * alpha + 0.5f));
|
||||
}
|
||||
|
||||
struct RoundRect
|
||||
{
|
||||
float cx, cy, hw, hh, r;
|
||||
RoundRect(int x, int y, int w, int h, int radius)
|
||||
{
|
||||
hw = w * 0.5f;
|
||||
hh = h * 0.5f;
|
||||
cx = x + hw;
|
||||
cy = y + hh;
|
||||
r = std::max(0.0f, std::min(float(radius), std::min(hw, hh)));
|
||||
}
|
||||
// signed distance of a pixel's centre to the outline (negative inside)
|
||||
float Distance(int x, int y) const
|
||||
{
|
||||
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
|
||||
const float qx = ax - (hw - r), qy = ay - (hh - r);
|
||||
if (qx <= 0.0f || qy <= 0.0f)
|
||||
return std::max(ax - hw, ay - hh);
|
||||
return std::sqrt(qx * qx + qy * qy) - r;
|
||||
}
|
||||
// deep inside, away from the edges and corners by more than `band`
|
||||
bool Inside(int x, int y, float band) const
|
||||
{
|
||||
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
|
||||
return (ax < hw - r && ay < hh - band) || (ay < hh - r && ax < hw - band);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
uint32_t Blend(uint32_t dst, uint32_t color, int alpha)
|
||||
{
|
||||
if (alpha <= 0)
|
||||
return dst;
|
||||
const uint32_t a = uint32_t(std::min(alpha, 255));
|
||||
const uint32_t w = a + (a >> 7);
|
||||
const uint32_t iw = 256 - w;
|
||||
const uint32_t rb = (((dst & 0xff00ffu) * iw + (color & 0xff00ffu) * w) >> 8) & 0xff00ffu;
|
||||
const uint32_t g = (((dst & 0x00ff00u) * iw + (color & 0x00ff00u) * w) >> 8) & 0x00ff00u;
|
||||
return 0xff000000u | rb | g;
|
||||
}
|
||||
|
||||
uint32_t Mix(uint32_t a, uint32_t b, float t)
|
||||
{
|
||||
return Blend(a, b, int(Clamp01(t) * 255.0f + 0.5f));
|
||||
}
|
||||
|
||||
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
Put(xx, yy, color, rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
|
||||
}
|
||||
|
||||
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
{
|
||||
const float t = horizontal ? float(xx - x) / std::max(1, w - 1) : float(yy - y) / std::max(1, h - 1);
|
||||
Put(xx, yy, Mix(c0, c1, t), rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
|
||||
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
|
||||
{
|
||||
if (rr.Inside(xx, yy, thick + 1.0f))
|
||||
continue;
|
||||
const float d = rr.Distance(xx, yy);
|
||||
Put(xx, yy, color, Clamp01(0.5f - d) - Clamp01(0.5f - d - thick), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha)
|
||||
{
|
||||
const RoundRect rr(x, y, w, h, r);
|
||||
for (int yy = std::max(0, y - size); yy < std::min(H, y + h + size); yy++)
|
||||
for (int xx = std::max(0, x - size); xx < std::min(W, x + w + size); xx++)
|
||||
{
|
||||
if (rr.Inside(xx, yy, 0.0f))
|
||||
continue;
|
||||
const float d = rr.Distance(xx, yy);
|
||||
if (d <= 0.0f || d >= size)
|
||||
continue;
|
||||
const float f = 1.0f - d / size;
|
||||
Put(xx, yy, color, f * f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha)
|
||||
{
|
||||
for (int yy = std::max(0, int(cy - r - 1)); yy <= std::min(H - 1, int(cy + r + 1)); yy++)
|
||||
for (int xx = std::max(0, int(cx - r - 1)); xx <= std::min(W - 1, int(cx + r + 1)); xx++)
|
||||
{
|
||||
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
|
||||
Put(xx, yy, color, Clamp01(0.5f - (std::sqrt(dx * dx + dy * dy) - r)), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha)
|
||||
{
|
||||
StrokeArc(cx, cy, r - thick * 0.5f, thick, 0.0f, 360.0f, color, alpha);
|
||||
}
|
||||
|
||||
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha)
|
||||
{
|
||||
const float area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0);
|
||||
if (std::fabs(area) < 1e-3f)
|
||||
return;
|
||||
const float s = area > 0 ? 1.0f : -1.0f;
|
||||
auto edge = [s](float ax, float ay, float bx, float by, float px, float py) {
|
||||
return s * ((bx - ax) * (py - ay) - (by - ay) * (px - ax));
|
||||
};
|
||||
const int minx = std::max(0, int(std::floor(std::min({x0, x1, x2})))), maxx = std::min(W - 1, int(std::ceil(std::max({x0, x1, x2}))));
|
||||
const int miny = std::max(0, int(std::floor(std::min({y0, y1, y2})))), maxy = std::min(H - 1, int(std::ceil(std::max({y0, y1, y2}))));
|
||||
for (int yy = miny; yy <= maxy; yy++)
|
||||
for (int xx = minx; xx <= maxx; xx++)
|
||||
{
|
||||
int in = 0;
|
||||
for (int sy = 0; sy < 4; sy++)
|
||||
for (int sx = 0; sx < 4; sx++)
|
||||
{
|
||||
const float px = xx + (sx + 0.5f) / 4.0f, py = yy + (sy + 0.5f) / 4.0f;
|
||||
if (edge(x0, y0, x1, y1, px, py) >= 0 && edge(x1, y1, x2, y2, px, py) >= 0 && edge(x2, y2, x0, y0, px, py) >= 0)
|
||||
in++;
|
||||
}
|
||||
Put(xx, yy, color, in / 16.0f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha)
|
||||
{
|
||||
const float outer = r + thick * 0.5f + 1.0f;
|
||||
const bool full = a1 - a0 >= 360.0f;
|
||||
for (int yy = std::max(0, int(cy - outer)); yy <= std::min(H - 1, int(cy + outer)); yy++)
|
||||
for (int xx = std::max(0, int(cx - outer)); xx <= std::min(W - 1, int(cx + outer)); xx++)
|
||||
{
|
||||
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
|
||||
const float cov = Clamp01(thick * 0.5f + 0.5f - std::fabs(std::sqrt(dx * dx + dy * dy) - r));
|
||||
if (cov <= 0.0f)
|
||||
continue;
|
||||
if (!full)
|
||||
{
|
||||
float a = std::atan2(dy, dx) * 57.29578f;
|
||||
if (a < 0)
|
||||
a += 360.0f;
|
||||
float lo = std::fmod(a0, 360.0f), span = a1 - a0;
|
||||
if (lo < 0)
|
||||
lo += 360.0f;
|
||||
float rel = a - lo;
|
||||
if (rel < 0)
|
||||
rel += 360.0f;
|
||||
if (rel > span)
|
||||
continue;
|
||||
}
|
||||
Put(xx, yy, color, cov, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void HLine(int x, int y, int w, uint32_t color, int alpha)
|
||||
{
|
||||
for (int xx = x; xx < x + w; xx++)
|
||||
Put(xx, y, color, 1.0f, alpha);
|
||||
}
|
||||
|
||||
void VLine(int x, int y, int h, uint32_t color, int alpha)
|
||||
{
|
||||
for (int yy = y; yy < y + h; yy++)
|
||||
Put(x, yy, color, 1.0f, alpha);
|
||||
}
|
||||
|
||||
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color)
|
||||
{
|
||||
const float s = float(size);
|
||||
const float cx = x + s * 0.5f, cy = y + s * 0.5f;
|
||||
switch (icon)
|
||||
{
|
||||
case Icon::Pause:
|
||||
{
|
||||
const int bw = int(s * 0.2f), bh = int(s * 0.62f), gap = int(s * 0.16f);
|
||||
const int top = int(cy - bh * 0.5f), left = int(cx - gap * 0.5f) - bw;
|
||||
FillRoundRect(left, top, bw, bh, bw / 3, color);
|
||||
FillRoundRect(int(cx + gap * 0.5f), top, bw, bh, bw / 3, color);
|
||||
break;
|
||||
}
|
||||
case Icon::Play:
|
||||
FillTriangle(x + s * 0.22f, y + s * 0.14f, x + s * 0.22f, y + s * 0.86f, x + s * 0.86f, cy, color);
|
||||
break;
|
||||
case Icon::Restart:
|
||||
{
|
||||
// most of a circle, open at the top, with an arrowhead at its upper left end pointing on round
|
||||
const float r = s * 0.32f, t = s * 0.12f;
|
||||
StrokeArc(cx, cy, r, t, 300.0f, 600.0f, color);
|
||||
const float a = 240.0f / 57.29578f;
|
||||
const float px = cx + r * std::cos(a), py = cy + r * std::sin(a);
|
||||
const float tx = -std::sin(a), ty = std::cos(a); // the way round (clockwise)
|
||||
const float nx = std::cos(a), ny = std::sin(a);
|
||||
const float hl = s * 0.22f, hw = s * 0.17f;
|
||||
FillTriangle(px + tx * hl, py + ty * hl, px + nx * hw, py + ny * hw, px - nx * hw, py - ny * hw, color);
|
||||
break;
|
||||
}
|
||||
case Icon::FastForward:
|
||||
FillTriangle(x + s * 0.08f, y + s * 0.2f, x + s * 0.08f, y + s * 0.8f, x + s * 0.5f, cy, color);
|
||||
FillTriangle(x + s * 0.5f, y + s * 0.2f, x + s * 0.5f, y + s * 0.8f, x + s * 0.92f, cy, color);
|
||||
break;
|
||||
case Icon::Grid:
|
||||
for (int j = 0; j < 3; j++)
|
||||
for (int i = 0; i < 3; i++)
|
||||
FillRoundRect(x + int(s * (0.1f + 0.29f * i)), y + int(s * (0.1f + 0.29f * j)), int(s * 0.22f),
|
||||
int(s * 0.22f), 2, color);
|
||||
break;
|
||||
case Icon::Tiles:
|
||||
for (int j = 0; j < 2; j++)
|
||||
for (int i = 0; i < 2; i++)
|
||||
FillRoundRect(x + int(s * (0.1f + 0.43f * i)), y + int(s * (0.1f + 0.43f * j)), int(s * 0.36f),
|
||||
int(s * 0.36f), int(s * 0.08f), color);
|
||||
break;
|
||||
case Icon::Frame:
|
||||
{
|
||||
// a dashed square: four dashes a side
|
||||
const int a = int(s * 0.12f), b = int(s * 0.88f), t = std::max(2, int(s * 0.09f)), d = (b - a) / 7;
|
||||
for (int k = 0; k < 7; k += 2)
|
||||
{
|
||||
FillRoundRect(x + a + k * d, y + a, d, t, 1, color);
|
||||
FillRoundRect(x + a + k * d, y + b - t, d, t, 1, color);
|
||||
FillRoundRect(x + a, y + a + k * d, t, d, 1, color);
|
||||
FillRoundRect(x + b - t, y + a + k * d, t, d, 1, color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Icon::Bars:
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
const float hgt = s * (0.3f + 0.25f * i);
|
||||
FillRoundRect(x + int(s * (0.12f + 0.28f * i)), y + int(s * 0.88f - hgt), int(s * 0.2f), int(hgt), 2, color);
|
||||
}
|
||||
break;
|
||||
case Icon::Screen:
|
||||
StrokeRoundRect(x + int(s * 0.06f), y + int(s * 0.2f), int(s * 0.88f), int(s * 0.6f), int(s * 0.08f),
|
||||
std::max(2.0f, s * 0.09f), color);
|
||||
break;
|
||||
case Icon::Gauge:
|
||||
{
|
||||
const float r = s * 0.38f;
|
||||
StrokeArc(cx, cy + s * 0.08f, r, s * 0.1f, 150.0f, 390.0f, color);
|
||||
const float a = 300.0f / 57.29578f;
|
||||
const float ex = cx + r * 0.8f * std::cos(a), ey = cy + s * 0.08f + r * 0.8f * std::sin(a);
|
||||
const float nx = -std::sin(a) * s * 0.05f, ny = std::cos(a) * s * 0.05f;
|
||||
FillTriangle(cx + nx, cy + s * 0.08f + ny, cx - nx, cy + s * 0.08f - ny, ex, ey, color);
|
||||
FillCircle(cx, cy + s * 0.08f, s * 0.08f, color);
|
||||
break;
|
||||
}
|
||||
case Icon::Download:
|
||||
FillRoundRect(int(cx - s * 0.07f), y + int(s * 0.1f), int(s * 0.14f), int(s * 0.4f), 1, 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
|
||||
} // namespace fe
|
||||
@@ -0,0 +1,49 @@
|
||||
// Mupen64Plus PS5 frontend: anti-aliased shapes on the CPU-drawn screen (ps5video::Surface()), for the pause menu's
|
||||
// look: rounded panels and buttons, glows, gradients, switches, sliders and a few drawn icons.
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
namespace gfx
|
||||
{
|
||||
// color over the pixel at `alpha` (0..255)
|
||||
uint32_t Blend(uint32_t dst, uint32_t color, int alpha);
|
||||
uint32_t Mix(uint32_t a, uint32_t b, float t); // a -> b
|
||||
|
||||
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha = 255);
|
||||
// from c0 to c1, left to right (horizontal) or top to bottom
|
||||
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha = 255);
|
||||
// a border `thick` pixels wide, inside the rectangle
|
||||
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha = 255);
|
||||
// light around the rectangle, fading out over `size` pixels
|
||||
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha);
|
||||
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha = 255);
|
||||
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha = 255);
|
||||
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha = 255);
|
||||
// part of a ring, angles in degrees, clockwise from 3 o'clock (screen y grows downwards)
|
||||
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha = 255);
|
||||
void HLine(int x, int y, int w, uint32_t color, int alpha = 255);
|
||||
void VLine(int x, int y, int h, uint32_t color, int alpha = 255);
|
||||
|
||||
// small icons drawn with the shapes above, in a `size` x `size` box
|
||||
enum class Icon
|
||||
{
|
||||
Pause,
|
||||
Play,
|
||||
Restart, // a circular arrow
|
||||
FastForward,
|
||||
Grid, // 3 x 3 squares
|
||||
Tiles, // 2 x 2 squares
|
||||
Frame, // a dashed square
|
||||
Bars, // a bar chart
|
||||
Screen, // a rectangle
|
||||
Gauge,
|
||||
Download,
|
||||
Search, // a magnifying glass
|
||||
};
|
||||
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color);
|
||||
} // namespace gfx
|
||||
} // namespace fe
|
||||
+299
-6
@@ -17,6 +17,8 @@
|
||||
|
||||
#include "fe_emu.h"
|
||||
|
||||
#include "fe_cheats.h"
|
||||
|
||||
#include "fe_games.h"
|
||||
#include "fe_menu.h"
|
||||
#include "fe_settings.h"
|
||||
@@ -49,6 +51,7 @@
|
||||
#include <cstdarg>
|
||||
#include <dirent.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
@@ -221,6 +224,15 @@ struct State
|
||||
uint32_t prev_buttons = 0;
|
||||
bool block_until_release = false;
|
||||
int pending_save_slot = -1;
|
||||
// the controller layout while a game is loaded (from RunGame on): its own or Settings'
|
||||
bool game_layout = false;
|
||||
bool own_layout = false;
|
||||
fe::Layout layout;
|
||||
// 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;
|
||||
@@ -483,6 +495,112 @@ std::string FileBase(const std::string& path)
|
||||
return base;
|
||||
}
|
||||
|
||||
// The cheats the player left on, by name: "<state>\t<name>" lines (fe_cheats.h's state).
|
||||
std::string CheatsPath()
|
||||
{
|
||||
return OrbisDir("cheats") + "/" + g.file_base + ".txt";
|
||||
}
|
||||
|
||||
void LoadCheatStates()
|
||||
{
|
||||
FILE* f = fopen(CheatsPath().c_str(), "r");
|
||||
if (!f)
|
||||
return;
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
char* tab = strchr(line, '\t');
|
||||
if (!tab)
|
||||
continue;
|
||||
*tab = 0;
|
||||
const int state = atoi(line);
|
||||
for (fe::cheats::Cheat& c : g.cheats)
|
||||
if (c.name == tab + 1 && state > 0 && state < c.States())
|
||||
c.state = state;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void SaveCheatStates()
|
||||
{
|
||||
std::string text;
|
||||
for (const fe::cheats::Cheat& c : g.cheats)
|
||||
if (c.state)
|
||||
text += std::to_string(c.state) + "\t" + c.name + "\n";
|
||||
if (text.empty())
|
||||
{
|
||||
unlink(CheatsPath().c_str());
|
||||
return;
|
||||
}
|
||||
OrbisMkdirs(OrbisDir("cheats"));
|
||||
if (FILE* f = fopen(CheatsPath().c_str(), "w"))
|
||||
{
|
||||
fputs(text.c_str(), f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 "#<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(c.uid);
|
||||
if (c.state == 0)
|
||||
{
|
||||
CoreCheatEnabled(id.c_str(), 0);
|
||||
return;
|
||||
}
|
||||
const int choice = c.options.empty() ? 0 : c.options[size_t(c.state - 1)].second;
|
||||
std::vector<m64p_cheat_code> codes;
|
||||
for (size_t i = 0; i < c.addresses.size(); i++)
|
||||
codes.push_back({c.addresses[i], c.values[i] < 0 ? choice : c.values[i]});
|
||||
const m64p_error r = CoreAddCheat(id.c_str(), codes.data(), int(codes.size()));
|
||||
if (r != M64ERR_SUCCESS)
|
||||
OrbisLog("[cheats] the core refused \"%s\" (%d)", c.name.c_str(), int(r));
|
||||
}
|
||||
|
||||
// "Continue where you left off": saved when the player leaves the game from the pause menu.
|
||||
std::string ResumePath(const std::string& file_base)
|
||||
{
|
||||
@@ -627,12 +745,19 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
|
||||
n64ps5_input_block(false);
|
||||
}
|
||||
|
||||
const uint32_t combo = SCE_PAD_BUTTON_L3 | SCE_PAD_BUTTON_R3;
|
||||
const bool menu = (pressed & SCE_PAD_BUTTON_TOUCH_PAD) || ((raw & combo) == combo && (pressed & combo));
|
||||
if (menu && !g.stopping && !g.leaving)
|
||||
// the touchpad opens the pause menu; R3 (pressed) saves to the state slot, L3 loads it
|
||||
if (!g.stopping && !g.leaving)
|
||||
{
|
||||
OpenPauseMenu();
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
const int slot = fe::Config().state_slot;
|
||||
if (pressed & SCE_PAD_BUTTON_TOUCH_PAD)
|
||||
{
|
||||
OpenPauseMenu();
|
||||
g.prev_buttons = ps5input::Pad(0).raw_buttons;
|
||||
}
|
||||
else if (pressed & SCE_PAD_BUTTON_R3)
|
||||
emu::Osd(emu::SaveState(slot) ? "Saved in slot %d" : "Could not save in slot %d", slot);
|
||||
else if (pressed & SCE_PAD_BUTTON_L3)
|
||||
emu::Osd(emu::StateExists(slot) && emu::LoadState(slot) ? "Loaded slot %d" : "Slot %d is empty", slot);
|
||||
}
|
||||
|
||||
UpdateFps();
|
||||
@@ -737,6 +862,7 @@ void DeinitCore()
|
||||
|
||||
ExitReason RunGame(const std::string& path, std::string* error, bool resume)
|
||||
{
|
||||
g.game_layout = false; // Settings' until this game's is known
|
||||
std::string dummy;
|
||||
if (!error)
|
||||
error = &dummy;
|
||||
@@ -773,6 +899,10 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
|
||||
|
||||
g.rom_path = path;
|
||||
g.file_base = FileBase(path);
|
||||
g.own_layout = fe::LoadGameLayout(g.file_base, g.layout);
|
||||
if (!g.own_layout)
|
||||
g.layout = fe::Config().layout;
|
||||
g.game_layout = true;
|
||||
g.pal = IsPalCountry(header.Country_code);
|
||||
// the title: as on the shelf (No-Intro name), else the catalog's GoodName, else the file name
|
||||
uint32_t crc = 0;
|
||||
@@ -802,6 +932,33 @@ 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 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 = 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++)
|
||||
if (g.cheats[i].state)
|
||||
{
|
||||
ApplyCheat(int(i));
|
||||
cheats_on += (cheats_on.empty() ? ": " : ", ") + fe::cheats::DisplayName(g.cheats[i].name);
|
||||
}
|
||||
if (!g.cheats.empty())
|
||||
OrbisLog("[cheats] %zu for \"%s\"%s", g.cheats.size(), g.cheats_game.c_str(),
|
||||
cheats_on.empty() ? ", none on" : cheats_on.c_str());
|
||||
OrbisLog("[emu] controller layout: %s%s", g.own_layout ? "this game's own " : "",
|
||||
g.layout == fe::Layout() ? "(the N64 pad's)" : fe::LayoutToText(g.layout).c_str());
|
||||
LogSaves("at start", settings.goodname, settings.MD5);
|
||||
const n64ps5_gfx_options gfx = {cfg.vi_filter ? 0 : 1, cfg.hide_overscan, cfg.render_threads, cfg.dp_compat,
|
||||
cfg.upscale, cfg.gpu_sync};
|
||||
@@ -878,6 +1035,8 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
|
||||
n64ps5_audio_set_mute(!fe::Config().audio);
|
||||
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)
|
||||
{
|
||||
@@ -907,6 +1066,134 @@ bool GameLoaded()
|
||||
return g.running;
|
||||
}
|
||||
|
||||
std::vector<fe::cheats::Cheat>& Cheats()
|
||||
{
|
||||
return g.cheats;
|
||||
}
|
||||
|
||||
std::string CheatsGameName()
|
||||
{
|
||||
return g.cheats_game;
|
||||
}
|
||||
|
||||
void SetCheat(int index, int state)
|
||||
{
|
||||
if (!g.running || index < 0 || index >= int(g.cheats.size()))
|
||||
return;
|
||||
fe::cheats::Cheat& c = g.cheats[size_t(index)];
|
||||
c.state = state < 0 || state >= c.States() ? 0 : state;
|
||||
ApplyCheat(index);
|
||||
SaveCheatStates();
|
||||
OrbisLog("[cheats] %s: %s", fe::cheats::DisplayName(c.name).c_str(),
|
||||
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;
|
||||
}
|
||||
|
||||
bool GameHasOwnLayout()
|
||||
{
|
||||
return g.game_layout && g.own_layout;
|
||||
}
|
||||
|
||||
void SetGameLayout(const fe::Layout& layout)
|
||||
{
|
||||
if (!g.game_layout)
|
||||
return;
|
||||
g.layout = layout;
|
||||
g.own_layout = layout != fe::Config().layout;
|
||||
fe::SaveGameLayout(g.file_base, g.own_layout ? &layout : nullptr);
|
||||
ApplySettings();
|
||||
}
|
||||
|
||||
std::string GameName()
|
||||
{
|
||||
return g.title;
|
||||
@@ -916,8 +1203,14 @@ void ApplySettings()
|
||||
{
|
||||
const fe::Settings& cfg = fe::Config();
|
||||
n64ps5_audio_set_mute(!cfg.audio);
|
||||
n64ps5_audio_set_volume(cfg.volume);
|
||||
n64ps5_audio_set_fast_forward(g.fast_forward.load());
|
||||
const n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick};
|
||||
n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick, {}};
|
||||
const fe::Layout& layout = g.game_layout ? g.layout : cfg.layout;
|
||||
memcpy(in.layout, layout.to, sizeof(in.layout));
|
||||
// L3 and R3 load and save states (n64ps5_on_vi): the game doesn't get them
|
||||
in.layout[N64PS5_PAD_L3] = N64PS5_N64_NONE;
|
||||
in.layout[N64PS5_PAD_R3] = N64PS5_N64_NONE;
|
||||
n64ps5_input_set_options(&in);
|
||||
ps5video::InvalidateN64();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "fe_cheats.h"
|
||||
#include "fe_search.h"
|
||||
#include "fe_settings.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace emu
|
||||
@@ -24,6 +28,28 @@ 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): 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();
|
||||
bool GameHasOwnLayout();
|
||||
void SetGameLayout(const fe::Layout& layout);
|
||||
std::string GameName(); // the title shown on the shelf (No-Intro name, ROM header name or file name)
|
||||
|
||||
// Settings from fe::Config() that apply while a game runs (aspect, smoothing, FPS, sound, dead zone).
|
||||
|
||||
+1532
-140
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
@@ -22,8 +23,97 @@ int Clamp(int v, int lo, int hi)
|
||||
{
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// the layout's names in the settings file and the per-game files
|
||||
const char* const kPadKeys[N64PS5_PAD_COUNT] = {"cross", "circle", "square", "triangle", "l1", "r1", "l2", "r2",
|
||||
"l3", "r3", "options", "up", "down", "left", "right", "rs_up", "rs_down", "rs_left", "rs_right"};
|
||||
const char* const kN64Keys[N64PS5_N64_COUNT] = {"none", "a", "b", "z", "l", "r", "start", "c_up", "c_down",
|
||||
"c_left", "c_right", "d_up", "d_down", "d_left", "d_right"};
|
||||
|
||||
std::string GameLayoutPath(const std::string& file_base)
|
||||
{
|
||||
return OrbisDir("layouts") + "/" + file_base + ".txt";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Layout::Layout()
|
||||
{
|
||||
n64ps5_input_default_layout(to);
|
||||
}
|
||||
|
||||
bool Layout::operator==(const Layout& o) const
|
||||
{
|
||||
return memcmp(to, o.to, sizeof(to)) == 0;
|
||||
}
|
||||
|
||||
std::string LayoutToText(const Layout& layout)
|
||||
{
|
||||
std::string s;
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
{
|
||||
if (i)
|
||||
s += ',';
|
||||
s += std::string(kPadKeys[i]) + '=' + kN64Keys[layout.to[i] < N64PS5_N64_COUNT ? layout.to[i] : 0];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Layout LayoutFromText(const std::string& text)
|
||||
{
|
||||
Layout layout;
|
||||
size_t at = 0;
|
||||
while (at < text.size())
|
||||
{
|
||||
size_t end = text.find(',', at);
|
||||
if (end == std::string::npos)
|
||||
end = text.size();
|
||||
const std::string item = text.substr(at, end - at);
|
||||
at = end + 1;
|
||||
const size_t eq = item.find('=');
|
||||
if (eq == std::string::npos)
|
||||
continue;
|
||||
const std::string pad = item.substr(0, eq), n64 = item.substr(eq + 1);
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
if (pad == kPadKeys[i])
|
||||
for (int t = 0; t < N64PS5_N64_COUNT; t++)
|
||||
if (n64 == kN64Keys[t])
|
||||
layout.to[i] = (unsigned char)t;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
bool LoadGameLayout(const std::string& file_base, Layout& out)
|
||||
{
|
||||
FILE* f = fopen(GameLayoutPath(file_base).c_str(), "r");
|
||||
if (!f)
|
||||
return false;
|
||||
char line[1024] = "";
|
||||
const bool ok = fgets(line, sizeof(line), f) != nullptr;
|
||||
fclose(f);
|
||||
if (!ok)
|
||||
return false;
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
out = LayoutFromText(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SaveGameLayout(const std::string& file_base, const Layout* layout)
|
||||
{
|
||||
const std::string path = GameLayoutPath(file_base);
|
||||
if (!layout)
|
||||
{
|
||||
unlink(path.c_str());
|
||||
return;
|
||||
}
|
||||
OrbisMkdirs(OrbisDir("layouts"));
|
||||
if (FILE* f = fopen(path.c_str(), "w"))
|
||||
{
|
||||
fprintf(f, "%s\n", LayoutToText(*layout).c_str());
|
||||
fclose(f);
|
||||
OrbisLog("[settings] %s: own controller layout", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
Settings& Config()
|
||||
{
|
||||
static Settings s;
|
||||
@@ -76,6 +166,8 @@ void Settings::Load()
|
||||
dp_compat = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "audio")
|
||||
audio = atoi(val) != 0;
|
||||
else if (key == "volume")
|
||||
volume = Clamp(atoi(val), 0, 100);
|
||||
else if (key == "pak")
|
||||
pak = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "deadzone")
|
||||
@@ -84,6 +176,8 @@ void Settings::Load()
|
||||
invert_stick = Clamp(atoi(val), 0, 3);
|
||||
else if (key == "invert_c_stick")
|
||||
invert_c_stick = Clamp(atoi(val), 0, 3);
|
||||
else if (key == "layout")
|
||||
layout = LayoutFromText(val);
|
||||
else if (key == "game_view")
|
||||
game_view = Clamp(atoi(val), 0, 2);
|
||||
else if (key == "state_slot")
|
||||
@@ -127,10 +221,12 @@ void Settings::Save() const
|
||||
fprintf(f, "gpu_sync=%d\n", gpu_sync ? 1 : 0);
|
||||
fprintf(f, "dp_compat=%d\n", dp_compat);
|
||||
fprintf(f, "audio=%d\n", audio ? 1 : 0);
|
||||
fprintf(f, "volume=%d\n", volume);
|
||||
fprintf(f, "pak=%d\n", pak);
|
||||
fprintf(f, "deadzone=%d\n", deadzone);
|
||||
fprintf(f, "invert_stick=%d\n", invert_stick);
|
||||
fprintf(f, "invert_c_stick=%d\n", invert_c_stick);
|
||||
fprintf(f, "layout=%s\n", LayoutToText(layout).c_str());
|
||||
fprintf(f, "game_view=%d\n", game_view);
|
||||
fprintf(f, "state_slot=%d\n", state_slot);
|
||||
fprintf(f, "resume=%d\n", resume ? 1 : 0);
|
||||
|
||||
@@ -2,10 +2,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include "n64_bridge.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace fe
|
||||
{
|
||||
// The controller layout: for each DualSense control (N64PS5_PAD_*), the N64 button it presses (N64PS5_N64_*).
|
||||
// The default one is the N64 pad's layout by position (n64ps5_input_default_layout).
|
||||
struct Layout
|
||||
{
|
||||
unsigned char to[N64PS5_PAD_COUNT];
|
||||
Layout();
|
||||
bool operator==(const Layout& o) const;
|
||||
bool operator!=(const Layout& o) const { return !(*this == o); }
|
||||
};
|
||||
// "cross=a,circle=c_down,..."; names it doesn't know keep their default
|
||||
std::string LayoutToText(const Layout& layout);
|
||||
Layout LayoutFromText(const std::string& text);
|
||||
// A game's own layout (pause menu): <root>/layouts/<ROM file name>.txt, by the ROM's file name like its states.
|
||||
bool LoadGameLayout(const std::string& file_base, Layout& out);
|
||||
// With nullptr the file goes and the game follows the layout of Settings.
|
||||
void SaveGameLayout(const std::string& file_base, const Layout* layout);
|
||||
|
||||
struct Settings
|
||||
{
|
||||
// picture
|
||||
@@ -24,11 +43,13 @@ struct Settings
|
||||
int render_threads = 6; // angrylion workers (1 = single-threaded)
|
||||
int dp_compat = 1; // angrylion sync points between its workers: 0 fewest (fastest), 1, 2 most (safest)
|
||||
bool audio = true;
|
||||
int volume = 100; // the game's sound, 0..100 %
|
||||
// controller
|
||||
int pak = 1; // 0 none, 1 Controller Pak, 2 Rumble Pak
|
||||
int deadzone = 12; // % of the stick's travel
|
||||
int invert_stick = 0; // the left (analog) stick's axes turned round: 0 off, 1 X, 2 Y, 3 X and Y
|
||||
int invert_c_stick = 0; // the right stick (C buttons), the same way
|
||||
Layout layout; // every game's, unless it has its own (LoadGameLayout)
|
||||
// misc
|
||||
int state_slot = 0; // 0..9
|
||||
bool resume = true; // leaving a game saves where the player was; starting it again offers to continue there
|
||||
|
||||
+27
-10
@@ -191,19 +191,19 @@ struct Target
|
||||
int w, h, pitch;
|
||||
};
|
||||
|
||||
void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t color, bool shadow)
|
||||
// bold: the text again 1..bold pixels to the right (one weight of Roboto is built in)
|
||||
void Draw(const Target& t, int x, int y, const char* utf8, int px, uint32_t color, bool shadow, int bold = 0)
|
||||
{
|
||||
Fonts& fonts = F();
|
||||
if (!fonts.ok || !utf8)
|
||||
return;
|
||||
const int px = EmPx(scale);
|
||||
const int baseline = y + int(std::lround(px * 0.80f));
|
||||
const int sh = std::max(1, scale / 2); // shadow offset
|
||||
for (int pass = shadow ? 0 : 1; pass < 2; pass++)
|
||||
const int sh = std::max(1, px / 20); // shadow offset
|
||||
for (int pass = shadow ? 0 : 1; pass < 2 + bold; pass++)
|
||||
{
|
||||
const uint32_t col = pass == 0 ? 0xff000000u : color;
|
||||
const uint32_t strength = pass == 0 ? 150 : 255;
|
||||
const int ox = pass == 0 ? sh : 0, oy = pass == 0 ? sh : 0;
|
||||
const int ox = pass == 0 ? sh : pass - 1, oy = pass == 0 ? sh : 0;
|
||||
float pen = float(x);
|
||||
uint32_t prev = 0;
|
||||
const char* s = utf8;
|
||||
@@ -238,11 +238,15 @@ void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t c
|
||||
} // namespace
|
||||
|
||||
int TextWidth(const char* utf8, int scale)
|
||||
{
|
||||
return TextWidthPx(utf8, EmPx(scale));
|
||||
}
|
||||
|
||||
int TextWidthPx(const char* utf8, int px)
|
||||
{
|
||||
Fonts& fonts = F();
|
||||
if (!fonts.ok || !utf8)
|
||||
return 0;
|
||||
const int px = EmPx(scale);
|
||||
float pen = 0;
|
||||
uint32_t prev = 0;
|
||||
const char* s = utf8;
|
||||
@@ -263,17 +267,30 @@ void DrawText(int x, int y, const char* utf8, int scale, uint32_t color, bool ou
|
||||
uint32_t* surf = ps5video::Surface();
|
||||
if (!surf)
|
||||
return;
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, scale, color, outline);
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, EmPx(scale), color, outline);
|
||||
}
|
||||
|
||||
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline, int bold)
|
||||
{
|
||||
uint32_t* surf = ps5video::Surface();
|
||||
if (!surf)
|
||||
return;
|
||||
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, px, color, outline, bold);
|
||||
}
|
||||
|
||||
void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale, uint32_t color, bool outline)
|
||||
{
|
||||
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, scale, color, outline);
|
||||
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, EmPx(scale), color, outline);
|
||||
}
|
||||
|
||||
std::string FitText(const std::string& utf8, int scale, int max_px)
|
||||
{
|
||||
if (TextWidth(utf8.c_str(), scale) <= max_px)
|
||||
return FitTextPx(utf8, EmPx(scale), max_px);
|
||||
}
|
||||
|
||||
std::string FitTextPx(const std::string& utf8, int px, int max_px)
|
||||
{
|
||||
if (TextWidthPx(utf8.c_str(), px) <= max_px)
|
||||
return utf8;
|
||||
std::string s = utf8;
|
||||
while (!s.empty())
|
||||
@@ -283,7 +300,7 @@ std::string FitText(const std::string& utf8, int scale, int max_px)
|
||||
n--;
|
||||
s.resize(n);
|
||||
const std::string t = s + "\xE2\x80\xA6"; // the ellipsis character, as PS5SX2
|
||||
if (TextWidth(t.c_str(), scale) <= max_px)
|
||||
if (TextWidthPx(t.c_str(), px) <= max_px)
|
||||
return t;
|
||||
}
|
||||
return "\xE2\x80\xA6";
|
||||
|
||||
@@ -30,6 +30,12 @@ void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale,
|
||||
// Cuts the string so it fits max_px (adds "...").
|
||||
std::string FitText(const std::string& utf8, int scale, int max_px);
|
||||
|
||||
// The same with the line height in pixels (the em; the pause menu's sizes), and a bold made of the text drawn
|
||||
// again `bold` pixels further right.
|
||||
int TextWidthPx(const char* utf8, int px);
|
||||
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline = false, int bold = 0);
|
||||
std::string FitTextPx(const std::string& utf8, int px, int max_px);
|
||||
|
||||
// PromptFont's controller symbols (the same code points as PS5SX2's fe_text.h).
|
||||
namespace icon
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+62
-1
@@ -52,26 +52,87 @@ void n64ps5_rsp_set_hle(bool on);
|
||||
|
||||
// ---- audio (plugins/audio_ps5.cpp) ---------------------------------------------------------------------
|
||||
void n64ps5_audio_set_mute(bool mute);
|
||||
// Settings "Volume": the game's sound scaled to 0..100 percent.
|
||||
void n64ps5_audio_set_volume(int percent);
|
||||
// Fast forward: the audio plugin drops what doesn't fit instead of slowing the game down.
|
||||
void n64ps5_audio_set_fast_forward(bool on);
|
||||
int n64ps5_audio_game_rate(void); // the game's current output rate in Hz (0 before it sets one)
|
||||
int n64ps5_audio_target(void); // the AudioOut ring level the rate control aims at (48 kHz frames)
|
||||
|
||||
// ---- input (plugins/input_ps5.cpp) ---------------------------------------------------------------------
|
||||
// The controller layout (Settings / pause menu "Controller layout"): for each DualSense control, the N64
|
||||
// button it presses. Several controls may press the same one (L2 and R2 are both Z by default). The left
|
||||
// stick is always the N64's analog stick.
|
||||
enum
|
||||
{
|
||||
N64PS5_PAD_CROSS,
|
||||
N64PS5_PAD_CIRCLE,
|
||||
N64PS5_PAD_SQUARE,
|
||||
N64PS5_PAD_TRIANGLE,
|
||||
N64PS5_PAD_L1,
|
||||
N64PS5_PAD_R1,
|
||||
N64PS5_PAD_L2,
|
||||
N64PS5_PAD_R2,
|
||||
N64PS5_PAD_L3,
|
||||
N64PS5_PAD_R3,
|
||||
N64PS5_PAD_OPTIONS,
|
||||
N64PS5_PAD_UP,
|
||||
N64PS5_PAD_DOWN,
|
||||
N64PS5_PAD_LEFT,
|
||||
N64PS5_PAD_RIGHT,
|
||||
N64PS5_PAD_RS_UP, // the right stick tilted (half way)
|
||||
N64PS5_PAD_RS_DOWN,
|
||||
N64PS5_PAD_RS_LEFT,
|
||||
N64PS5_PAD_RS_RIGHT,
|
||||
N64PS5_PAD_COUNT
|
||||
};
|
||||
enum
|
||||
{
|
||||
N64PS5_N64_NONE,
|
||||
N64PS5_N64_A,
|
||||
N64PS5_N64_B,
|
||||
N64PS5_N64_Z,
|
||||
N64PS5_N64_L,
|
||||
N64PS5_N64_R,
|
||||
N64PS5_N64_START,
|
||||
N64PS5_N64_C_UP,
|
||||
N64PS5_N64_C_DOWN,
|
||||
N64PS5_N64_C_LEFT,
|
||||
N64PS5_N64_C_RIGHT,
|
||||
N64PS5_N64_D_UP,
|
||||
N64PS5_N64_D_DOWN,
|
||||
N64PS5_N64_D_LEFT,
|
||||
N64PS5_N64_D_RIGHT,
|
||||
N64PS5_N64_COUNT
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int deadzone; // percent of the stick's travel ignored around the centre (0..30)
|
||||
int range; // N64 stick range at full tilt (the real stick reaches about 80)
|
||||
int pak; // 0 none, 1 Controller Pak (memory), 2 Rumble Pak
|
||||
int invert; // the analog stick's (left stick's) axes turned round: bit 0 X, bit 1 Y
|
||||
int invert_c; // the C buttons' right stick turned round: bit 0 X (C-left/right), bit 1 Y (C-up/down)
|
||||
int invert_c; // the right stick turned round: bit 0 X (left/right), bit 1 Y (up/down)
|
||||
unsigned char layout[N64PS5_PAD_COUNT]; // N64PS5_N64_* for each N64PS5_PAD_*
|
||||
} n64ps5_input_options;
|
||||
|
||||
// The layout of the N64 pad by position: Cross A, Square B, L2 / R2 Z, L1 / R1 L / R, OPTIONS Start, D-pad
|
||||
// D-pad, right stick and Triangle / Circle the C buttons.
|
||||
void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT]);
|
||||
|
||||
void n64ps5_input_set_options(const n64ps5_input_options* opt);
|
||||
// While the frontend uses the pad itself (a hotkey held, the pause menu just closed), the game sees no
|
||||
// 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
|
||||
@@ -48,7 +48,7 @@ m64p_system_type g_system = SYSTEM_NTSC;
|
||||
std::atomic<bool> g_mute{false};
|
||||
std::atomic<bool> g_fast_forward{false};
|
||||
std::atomic<int> g_rate{0};
|
||||
int g_volume = 100;
|
||||
std::atomic<int> g_volume{100};
|
||||
|
||||
// resampler state
|
||||
double g_pos = 0.0; // position between g_prev and the next input sample
|
||||
@@ -211,7 +211,7 @@ void AiLenChanged()
|
||||
g_out.resize(size_t(max_out) * 2);
|
||||
int16_t* out = g_out.data();
|
||||
int n = 0;
|
||||
const int vol = g_volume;
|
||||
const int vol = g_volume.load(std::memory_order_relaxed);
|
||||
// Linear interpolation between g_prev (position 0) and each next sample (position 1).
|
||||
for (int i = 0; i < in_frames; i++)
|
||||
{
|
||||
@@ -262,7 +262,7 @@ void VolumeDown()
|
||||
|
||||
int VolumeGetLevel()
|
||||
{
|
||||
return g_mute.load() ? 0 : g_volume;
|
||||
return g_mute.load() ? 0 : g_volume.load();
|
||||
}
|
||||
|
||||
void VolumeSetLevel(int level)
|
||||
@@ -276,7 +276,7 @@ const char* VolumeGetString()
|
||||
if (g_mute.load())
|
||||
snprintf(text, sizeof(text), "Mute");
|
||||
else
|
||||
snprintf(text, sizeof(text), "%d%%", g_volume);
|
||||
snprintf(text, sizeof(text), "%d%%", g_volume.load());
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -309,6 +309,11 @@ extern "C" void n64ps5_audio_set_mute(bool mute)
|
||||
g_mute.store(mute);
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_audio_set_volume(int percent)
|
||||
{
|
||||
g_volume.store(percent < 0 ? 0 : percent > 100 ? 100 : percent);
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_audio_set_fast_forward(bool on)
|
||||
{
|
||||
g_fast_forward.store(on);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Mupen64Plus PS5: the input plugin, DualSense / DualShock through libScePad (ProsperoInput.h).
|
||||
//
|
||||
// The frontend polls the pads once per video interrupt (fe_emu.cpp); GetKeys turns that state into the
|
||||
// N64 controller's buttons and stick. Mapping (by position, as on the N64 pad):
|
||||
// N64 controller's buttons and stick. The left stick is the analog stick; every other control presses the
|
||||
// N64 button the controller layout gives it (n64ps5_input_options.layout, per game). The default layout, by
|
||||
// position as on the N64 pad:
|
||||
//
|
||||
// left stick -> analog stick D-pad -> D-pad
|
||||
// Cross -> A Square -> B
|
||||
// L2 or R2 -> Z L1 / R1 -> L / R
|
||||
// right stick -> C buttons Triangle -> C-up, Circle -> C-down
|
||||
// OPTIONS -> Start
|
||||
// OPTIONS -> Start L3, R3 -> nothing
|
||||
//
|
||||
// Rumble: the core sends Rumble Pak writes through ControllerCommand; they drive the controller's motors.
|
||||
//
|
||||
@@ -33,7 +35,8 @@ constexpr int kNumControllers = 4;
|
||||
|
||||
bool g_init = false;
|
||||
CONTROL* g_controls = nullptr;
|
||||
n64ps5_input_options g_opt = {12, 80, 1, 0, 0};
|
||||
n64ps5_input_options g_opt = {12, 80, 1, 0, 0, {}};
|
||||
bool g_opt_set = false;
|
||||
std::atomic<bool> g_block{false};
|
||||
|
||||
// One stick axis (0..255, centre 128) -> -range..range, with a dead zone and a linear ramp after it.
|
||||
@@ -95,6 +98,8 @@ int PakPlugin()
|
||||
|
||||
void InitiateControllers(CONTROL_INFO info)
|
||||
{
|
||||
if (!g_opt_set) // before the frontend's first options: the default layout
|
||||
n64ps5_input_default_layout(g_opt.layout);
|
||||
g_controls = info.Controls;
|
||||
if (!g_controls)
|
||||
return;
|
||||
@@ -122,25 +127,50 @@ void GetKeys(int control, BUTTONS* keys)
|
||||
return;
|
||||
const uint32_t b = p.raw_buttons; // the D-pad only (the stick is the analog stick here)
|
||||
|
||||
keys->R_DPAD = (b & SCE_PAD_BUTTON_RIGHT) != 0;
|
||||
keys->L_DPAD = (b & SCE_PAD_BUTTON_LEFT) != 0;
|
||||
keys->D_DPAD = (b & SCE_PAD_BUTTON_DOWN) != 0;
|
||||
keys->U_DPAD = (b & SCE_PAD_BUTTON_UP) != 0;
|
||||
keys->START_BUTTON = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
keys->Z_TRIG = (b & (SCE_PAD_BUTTON_L2 | SCE_PAD_BUTTON_R2)) != 0;
|
||||
keys->B_BUTTON = (b & SCE_PAD_BUTTON_SQUARE) != 0;
|
||||
keys->A_BUTTON = (b & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
keys->L_TRIG = (b & SCE_PAD_BUTTON_L1) != 0;
|
||||
keys->R_TRIG = (b & SCE_PAD_BUTTON_R1) != 0;
|
||||
// what each DualSense control is doing; the right stick counts at half tilt. Settings "Invert left stick"
|
||||
// and "Invert right stick" (GitHub issue #3) turn the sticks' axes round.
|
||||
bool on[N64PS5_PAD_COUNT] = {};
|
||||
on[N64PS5_PAD_CROSS] = (b & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
on[N64PS5_PAD_CIRCLE] = (b & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
on[N64PS5_PAD_SQUARE] = (b & SCE_PAD_BUTTON_SQUARE) != 0;
|
||||
on[N64PS5_PAD_TRIANGLE] = (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
on[N64PS5_PAD_L1] = (b & SCE_PAD_BUTTON_L1) != 0;
|
||||
on[N64PS5_PAD_R1] = (b & SCE_PAD_BUTTON_R1) != 0;
|
||||
on[N64PS5_PAD_L2] = (b & SCE_PAD_BUTTON_L2) != 0;
|
||||
on[N64PS5_PAD_R2] = (b & SCE_PAD_BUTTON_R2) != 0;
|
||||
on[N64PS5_PAD_L3] = (b & SCE_PAD_BUTTON_L3) != 0;
|
||||
on[N64PS5_PAD_R3] = (b & SCE_PAD_BUTTON_R3) != 0;
|
||||
on[N64PS5_PAD_OPTIONS] = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
on[N64PS5_PAD_UP] = (b & SCE_PAD_BUTTON_UP) != 0;
|
||||
on[N64PS5_PAD_DOWN] = (b & SCE_PAD_BUTTON_DOWN) != 0;
|
||||
on[N64PS5_PAD_LEFT] = (b & SCE_PAD_BUTTON_LEFT) != 0;
|
||||
on[N64PS5_PAD_RIGHT] = (b & SCE_PAD_BUTTON_RIGHT) != 0;
|
||||
const bool rs_right = p.rx > 192, rs_left = p.rx < 64, rs_down = p.ry > 192, rs_up = p.ry < 64;
|
||||
const bool rs_flip_x = (g_opt.invert_c & 1) != 0, rs_flip_y = (g_opt.invert_c & 2) != 0;
|
||||
on[N64PS5_PAD_RS_RIGHT] = rs_flip_x ? rs_left : rs_right;
|
||||
on[N64PS5_PAD_RS_LEFT] = rs_flip_x ? rs_right : rs_left;
|
||||
on[N64PS5_PAD_RS_DOWN] = rs_flip_y ? rs_up : rs_down;
|
||||
on[N64PS5_PAD_RS_UP] = rs_flip_y ? rs_down : rs_up;
|
||||
|
||||
// C buttons: the right stick (half tilt), plus Triangle / Circle. Settings "Invert left stick" and
|
||||
// "Invert right stick" (GitHub issue #3) turn the sticks' axes round; Triangle / Circle stay as they are.
|
||||
const bool c_right = p.rx > 192, c_left = p.rx < 64, c_down = p.ry > 192, c_up = p.ry < 64;
|
||||
const bool c_flip_x = (g_opt.invert_c & 1) != 0, c_flip_y = (g_opt.invert_c & 2) != 0;
|
||||
keys->R_CBUTTON = c_flip_x ? c_left : c_right;
|
||||
keys->L_CBUTTON = c_flip_x ? c_right : c_left;
|
||||
keys->D_CBUTTON = (c_flip_y ? c_up : c_down) || (b & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
keys->U_CBUTTON = (c_flip_y ? c_down : c_up) || (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
// and what they press on the N64 pad
|
||||
bool n64[N64PS5_N64_COUNT] = {};
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
if (on[i] && g_opt.layout[i] < N64PS5_N64_COUNT)
|
||||
n64[g_opt.layout[i]] = true;
|
||||
keys->A_BUTTON = n64[N64PS5_N64_A];
|
||||
keys->B_BUTTON = n64[N64PS5_N64_B];
|
||||
keys->Z_TRIG = n64[N64PS5_N64_Z];
|
||||
keys->L_TRIG = n64[N64PS5_N64_L];
|
||||
keys->R_TRIG = n64[N64PS5_N64_R];
|
||||
keys->START_BUTTON = n64[N64PS5_N64_START];
|
||||
keys->U_CBUTTON = n64[N64PS5_N64_C_UP];
|
||||
keys->D_CBUTTON = n64[N64PS5_N64_C_DOWN];
|
||||
keys->L_CBUTTON = n64[N64PS5_N64_C_LEFT];
|
||||
keys->R_CBUTTON = n64[N64PS5_N64_C_RIGHT];
|
||||
keys->U_DPAD = n64[N64PS5_N64_D_UP];
|
||||
keys->D_DPAD = n64[N64PS5_N64_D_DOWN];
|
||||
keys->L_DPAD = n64[N64PS5_N64_D_LEFT];
|
||||
keys->R_DPAD = n64[N64PS5_N64_D_RIGHT];
|
||||
|
||||
const int x = Axis(p.lx, g_opt.deadzone, g_opt.range);
|
||||
const int y = -Axis(p.ly, g_opt.deadzone, g_opt.range); // the N64's Y grows upwards
|
||||
@@ -214,7 +244,37 @@ extern "C" const m64ps5_library m64ps5_input_lib = {"ps5-dualsense", kSymbols};
|
||||
extern "C" void n64ps5_input_set_options(const n64ps5_input_options* opt)
|
||||
{
|
||||
if (opt)
|
||||
{
|
||||
g_opt = *opt;
|
||||
g_opt_set = true;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT])
|
||||
{
|
||||
static const unsigned char kDefault[N64PS5_PAD_COUNT] = {
|
||||
N64PS5_N64_A, // Cross
|
||||
N64PS5_N64_C_DOWN, // Circle
|
||||
N64PS5_N64_B, // Square
|
||||
N64PS5_N64_C_UP, // Triangle
|
||||
N64PS5_N64_L, // L1
|
||||
N64PS5_N64_R, // R1
|
||||
N64PS5_N64_Z, // L2
|
||||
N64PS5_N64_Z, // R2
|
||||
N64PS5_N64_NONE, // L3
|
||||
N64PS5_N64_NONE, // R3
|
||||
N64PS5_N64_START, // OPTIONS
|
||||
N64PS5_N64_D_UP,
|
||||
N64PS5_N64_D_DOWN,
|
||||
N64PS5_N64_D_LEFT,
|
||||
N64PS5_N64_D_RIGHT,
|
||||
N64PS5_N64_C_UP, // right stick
|
||||
N64PS5_N64_C_DOWN,
|
||||
N64PS5_N64_C_LEFT,
|
||||
N64PS5_N64_C_RIGHT,
|
||||
};
|
||||
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
|
||||
layout[i] = kDefault[i];
|
||||
}
|
||||
|
||||
extern "C" void n64ps5_input_block(bool block)
|
||||
|
||||
+13
-6
@@ -5,7 +5,8 @@
|
||||
# The BC7 encoder is a small one: every 4x4 block in mode 6 (two RGBA endpoints, 4-bit indices), endpoints
|
||||
# from the block's colour bounding box. Plenty for a smooth background.
|
||||
#
|
||||
# make_art.py <out-dir> [font.ttf bold.ttf] (needs Pillow and numpy)
|
||||
# make_art.py <out-dir> [font.ttf bold.ttf] draws its own (needs Pillow and numpy)
|
||||
# make_art.py <out-dir> --images <icon> <bg> from pictures (app/art/icon.png, app/art/background.png)
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import os
|
||||
@@ -13,7 +14,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
|
||||
|
||||
ACCENT = (255, 196, 64)
|
||||
BLUE = (64, 140, 255)
|
||||
@@ -145,11 +146,17 @@ def dds_bc7(width, height, data):
|
||||
|
||||
def main():
|
||||
out = sys.argv[1]
|
||||
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
|
||||
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
|
||||
os.makedirs(out, exist_ok=True)
|
||||
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = make_background(black, bold)
|
||||
if len(sys.argv) > 4 and sys.argv[2] == "--images":
|
||||
# pictures made elsewhere (app/art/): fitted to the icon's and the background's sizes
|
||||
ImageOps.fit(Image.open(sys.argv[3]).convert("RGB"), (512, 512), Image.LANCZOS).save(
|
||||
os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = ImageOps.fit(Image.open(sys.argv[4]).convert("RGB"), (3840, 2160), Image.LANCZOS)
|
||||
else:
|
||||
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
|
||||
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
|
||||
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
|
||||
bg = make_background(black, bold)
|
||||
bg.save(os.path.join(out, "background-source.png"), optimize=True)
|
||||
dds = dds_bc7(bg.width, bg.height, bc7_encode(bg))
|
||||
for name in ("pic0.dds", "pic1.dds"):
|
||||
|
||||
+16
-11
@@ -3,10 +3,11 @@
|
||||
# 1. builds the app from scratch (build-native.bat Clean): build-native\PPSA99064\ and Mupen64PlusPS5.zip. Always
|
||||
# clean: 0.6.1 was published from an incremental build that didn't start on the console;
|
||||
# 2. checks that everything is committed and pushed, that release-notes\<contentVersion>.md exists and that
|
||||
# the tag isn't taken;
|
||||
# 3. creates the GitHub release: tag = contentVersion (NN.NNN.NNN, from VERSION in ps5\Makefile), asset
|
||||
# Mupen64PlusPS5.zip, notes from release-notes\<contentVersion>.md (the app shows them when it offers the
|
||||
# update);
|
||||
# the version isn't released yet (under either tag style);
|
||||
# 3. creates the GitHub release: tag = the version as people write it (0.7.4, VERSION in ps5\Makefile; up to
|
||||
# 0.7.3 the tag was the contentVersion, 00.007.003), asset Mupen64PlusPS5.zip, notes from
|
||||
# release-notes\<contentVersion>.md (the app shows them when it offers the update). Every app since 0.6.0
|
||||
# reads both tag styles as numbers (fe_update.cpp ParseVersion), so they compare correctly;
|
||||
# 4. reads the release back and compares GitHub's SHA-256 of the asset with the local zip (the app checks
|
||||
# the download against that digest).
|
||||
#
|
||||
@@ -42,23 +43,27 @@ try {
|
||||
$ahead = git rev-list --count origin/main..HEAD
|
||||
if ([int]$ahead -gt 0) { throw "push your commits first (git push): $ahead not on GitHub" }
|
||||
|
||||
$tag = $version
|
||||
# "release not found" on stderr is the expected answer: PowerShell 5.1 would make it fatal under 'Stop'
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $gh release view $cv --repo $repo 2>&1 | Out-Null
|
||||
$exists = $LASTEXITCODE -eq 0
|
||||
$exists = $false
|
||||
foreach ($t in @($tag, $cv)) {
|
||||
& $gh release view $t --repo $repo 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $exists = $true }
|
||||
}
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($exists) { throw "release $cv already exists: raise VERSION in ps5\Makefile" }
|
||||
if ($exists) { throw "release $version already exists: raise VERSION in ps5\Makefile" }
|
||||
|
||||
Write-Host "Publishing Mupen64Plus PS5 $version (tag $cv) to $repo"
|
||||
& $gh release create $cv $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
|
||||
Write-Host "Publishing Mupen64Plus PS5 $version (tag $tag) to $repo"
|
||||
& $gh release create $tag $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
|
||||
if ($LASTEXITCODE -ne 0) { throw 'gh release create failed' }
|
||||
|
||||
$local = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
# parsed here: PowerShell 5.1 strips the quotes a --jq filter would need
|
||||
$release = (& $gh api "repos/$repo/releases/tags/$cv") -join "`n" | ConvertFrom-Json
|
||||
$release = (& $gh api "repos/$repo/releases/tags/$tag") -join "`n" | ConvertFrom-Json
|
||||
$remote = ($release.assets | Where-Object { $_.name -eq $asset }).digest
|
||||
if ($remote -ne "sha256:$local") { throw "GitHub's digest '$remote' doesn't match the local zip (sha256:$local)" }
|
||||
Write-Host "Done: https://github.com/$repo/releases/tag/$cv (sha256 $local)"
|
||||
Write-Host "Done: https://github.com/$repo/releases/tag/$tag (sha256 $local)"
|
||||
Write-Host 'Consoles on 0.6.3 or newer will offer it the next time Mupen64Plus PS5 starts.'
|
||||
}
|
||||
finally { Pop-Location }
|
||||
@@ -0,0 +1,7 @@
|
||||
## New
|
||||
- A new icon and background on the home screen.
|
||||
- A new look for the pause menu and Settings: one panel with switches, a volume slider and icons. In the pause menu, the touchpad resumes, Square resets the game (it asks first) and Triangle goes back to the game list; Save State, Load State and Quit are at the top and bottom of the list.
|
||||
- Quick save and load: in a game, press R3 (the right stick) to save in the current state slot, and L3 (the left stick) to load it. The pause menu now opens with the touchpad only.
|
||||
- Cheats: a Cheats page in the pause menu with the GameShark codes of mupen64plus's cheat list (about 560 game versions: infinite lives, unlock everything and more). Switch each one on or off, or pick one of its choices; the game remembers them.
|
||||
- Controller layout (Settings): choose what each DualSense button does on the N64 controller. Cross, Circle, Square, Triangle, L1, R1, L2 and R2 (separately), OPTIONS, the D-pad and the right stick can each press any N64 button, or nothing. From the game list it changes every game's layout; from the pause menu, only the game you are playing, which keeps it.
|
||||
- Settings: Volume (0 to 100%), also from the pause menu while you play.
|
||||
@@ -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