diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5fcb2..b6c6af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,981 +1,106 @@ # CheatRunner — Changelog -## v0.14 +## v0.15 -_In development._ +- **Added a "Switch to Max Compatibility" prompt** when a cheat fails to enable due to an unverifiable address. Doesn't change any defaults — only offers the existing preset at the moment it would help. +- **Added trainer author credits** to the cheat menu ("Cheats By") for JSON, SHN, and MC4 files. +- **Fixed:** SHN/MC4 game names with an apostrophe (e.g. "Assassin's Creed") were truncated in the cheat menu. +- **Fixed:** cheats depending on a "Master Code" mod could resolve to the wrong address and fail silently, including a case where a dependent's own name confused master-code detection. Also fixes the dashboard's MISMATCH badge showing incorrectly for these cheats. +- **Fixed:** cheat/patch writes silently failed on firmwares 12.xx. +- **Fixed:** some valid cheats sharing a code-cave block with other mods were wrongly blocked as "wrong address." +- **Fixed:** cheat entries with a dropped leading hex zero (e.g. `"1"` instead of `"01"`) failed to parse and silently did nothing. +- **Added a network watchdog** that recovers faster after sleep/wake or Wi-Fi reconnects, and notifies you when the console's IP changes. +- **Added an OLED theme** — true black background and panels, and noticeably less glow/decorative lines than the default Dark theme. +- **Session logs are now saved to `/data/cheatrunner/logs.txt`**, not just kept in memory — makes it possible to check what happened after a crash or a restart wiped the in-app log view. +- **Fixed: "Shutdown Payload" could silently hang forever** instead of shutting down CheatRunner, if a cheat apply was stuck at the exact moment shutdown was requested. Shutdown now always completes within a couple seconds either way. +- **Enlarged the header logo** and made its glow match the active theme's color instead of always being red. +- **Fixed:** the Cheats/Repositories/Patches tabs in the trainer modal could render as oversized buttons on the PS5 browser. +- **Fixed:** a Master Code cheat could show a false "Conflicts with active mod" against its own dependent, blocking it from being enabled, when the dependent's own name also mentioned "Master Code." +- **Fixed:** cheats that require a Master Code showed a confusing "VERSION MISMATCH" (and couldn't be toggled) before the Master Code was enabled — now shows a clear "requires Master Code first" state instead. +- **Fixed:** a Master Code's own button showed a false "PARTIAL PATCH" once its dependent cheats were enabled and writing into its shared cave, even though everything was working correctly. +- **Fixed:** the Settings popup could fail to open/render on the PS5 browser the same way the trainer modal did. +- **Game icons are now cached by the browser** instead of being re-downloaded every time the dashboard loads — faster repeat loads, especially on the PS5 browser. +- **Added HTTP keep-alive**, so the dashboard's background polling reuses one connection instead of opening a new one for every single request. +- **Settings theme dropdown now shows proper names** ("Dark", "Crimson", "Midnight", "OLED") instead of raw internal values. -### Home Screen — PKG Moved to Media Tab - -- **CheatRunner's home-screen tile now installs into the Media Players row instead of the Games library.** Confirmed by directly comparing PS5's own `/system_data/priv/mms/app.db` and `appinfo.db` between YouTube (a known Media-tab app) and two real games: the fields that differ are `applicationCategoryType` (`65536`/`0x10000` for Media vs `0` for Games) and `displayLocation` (`188` vs `138`). Both are now set in `pkg-source/sce_sys/param.json` and mirrored into `pkg-source/build_sfo.py`'s hand-built SFO. Verified by rebuilding the PKG through the real `prospero-pub-cmd` publishing tool with no new errors/warnings. -- `APP_TYPE` (`5` for YouTube vs `1` for games) and `HUBAPP_URI` (`psmediahub:main?titleId=…` vs `pshome:gamehub?titleId=…`) also differ in the system record but were left unchanged — no confirmed way to set them through the official `param.json` schema, and they may simply be derived by the system once `applicationCategoryType` is set. Worth re-checking after install if the tile doesn't land in Media as expected. -- **Follow-up `param.json` audit, same YouTube-diff method:** `contentBadgeType` changed from `1` ("Game") to `2` ("Other") — was a leftover from before the Media-tab move. Locale key `"zh-Hans-CN"` corrected to `"zh-Hans"` — not a valid PS5 locale identifier, so the Chinese title was silently never matching and falling back to `en-US`. Added `applicationDrmType: "free"` — found via `prospero-pub-cmd` itself warning "Application DRM Type not found, the default value applied" during a real rebuild, not the wiki. `ageLevel` was deliberately left unset — spec-listed as required but not functionally needed, and there's no non-speculative age value for a homebrew utility. - -### Dashboard — AppCache Version Bump - -- **Bumped the `/cache.appcache` manifest comment from `v0.14` to `v0.14-BETA1`.** All of v0.14's dashboard HTML/CSS/JS changes (Bluesky button, PS5 perf fixes, etc.) were invisible on any PS5 that had already loaded the dashboard once under the unchanged `v0.14` manifest — HTML5 AppCache only re-fetches `/`, `/dashboard.css`, `/dashboard.js`, `/CheatRunner.png` when the manifest file's *byte content* changes, and only the version comment had been changing this whole time it wasn't bumped. Bump this comment on every release/iteration that touches the dashboard, not just on version number changes — already noted as a requirement in the original AppCache changelog entry above, but easy to forget mid-development. -- **Bumping the manifest alone wasn't enough — added the missing AppCache update handler.** Even with the manifest comment changed, the bump still didn't show up on PS5: HTML5 AppCache downloads an updated cache *silently in the background* and never displays it unless the page's own JS listens for the `applicationCache` `updateready` event and calls `swapCache()` + reloads. This project had zero such handling anywhere, so updates were detected but never surfaced — the user could reload indefinitely and keep seeing the old page. Added a small handler at the top of `dashboard_js.inc` (right after the `isPS5` check) that calls `swapCache()` and `window.location.reload()` as soon as `updateready` fires (or immediately if the status is already `UPDATEREADY` when the script runs). This is what was actually blocking the Bluesky button and the PS5 perf CSS fixes from ever appearing — the manifest bump was necessary but not sufficient. - -### Dashboard — PS5 Browser Performance - -- **Removed Google Fonts.** The dashboard `` previously loaded three typefaces (Inter, Space Grotesk, JetBrains Mono) from `fonts.googleapis.com` / `fonts.gstatic.com` via a blocking ``. On PS5 — where the browser has no JIT and no GPU compositing — this triggered external DNS + TCP + HTTP round-trips that blocked first paint for up to 2 seconds depending on network conditions. All three `` tags removed; `--cr-font-base`, `--cr-font-display`, and `--cr-font-mono` CSS variables now resolve to native system-ui stacks (`system-ui / -apple-system / BlinkMacSystemFont` and `ui-monospace`), which the PS5 browser serves instantly from its built-in font cache. - -- **HTML5 AppCache for instant repeat loads.** The `` element now carries `manifest="/cache.appcache"`. A new `/cache.appcache` route serves a `text/cache-manifest` document that lists `/`, `/dashboard.css`, `/dashboard.js`, and `/CheatRunner.png` in the `CACHE` section with `NETWORK: *` to pass all API calls through to the daemon. After the first page load the PS5 browser serves the entire dashboard from its AppCache — zero HTTP requests to the daemon on every subsequent open, making the UI appear near-instantly. Cache is invalidated by bumping the version comment in the manifest; that comment is updated each release. - -- **HTTP caching for static assets.** `dashboard.css` and `dashboard.js` are now served with `Cache-Control: public, max-age=3600` (previously `no-cache`). A dedicated `http_send_response_cached()` helper handles this path; HTML and all API responses remain `no-cache`. - -- **`backdrop-filter` removed on PS5.** A `isPS5` flag is set at JS parse time (`/PlayStation/i.test(navigator.userAgent)`) and adds class `ps5` to ``. The Settings overlay, Support modal, and Crop overlay all used `backdrop-filter: blur(6px)`, which on PS5's software-rendered WebKit requires compositing every pixel behind the overlay as a CPU bitmap before blurring — visibly lagging the overlay open. The `.ps5` CSS block replaces blur with `background: rgba(0,0,0,.92)` on all three overlays at zero compositing cost. - -- **Decorative background layers hidden on PS5.** `.cr-command-bg`, `.cr-scan-grid`, and `.cr-circuit-lines` are three `position: fixed` full-viewport layers (radial gradients, mask-image fades, repeating diagonal lines) that trigger three full-viewport software paint passes at load. On a TV-connected console the effect is imperceptible; the `.ps5` block hides all three with `display: none`. - -- **`crCheatGlow` and `crLivePulse` converted to compositor-only animations.** Both animations previously mutated `box-shadow` on every frame, forcing a full repaint on every cycle — on PS5 this caused periodic CPU stalls proportional to the number of visible tiles with active cheats. `crLivePulse` (running-game pill dot) now animates `opacity` only (`will-change: opacity`). `crCheatGlow` moves to an `::after` pseudo-element whose `opacity` pulses between 0.35 and 1 — the tile itself carries a static `box-shadow`; the visual pulsing comes from compositor-handled opacity with zero repaints. On PS5 both animations are disabled entirely via the `.ps5` block (static indicators remain visible). - -- **`clip-path` stripped from entry animations.** `crPanelDeploy`, `crToastSnap`, and `crToastIn` animated `clip-path` alongside `transform`/`opacity`. PS5's older WebKit may software-composite `clip-path` changes. All three keyframe definitions are now `opacity` + `transform` only — GPU-safe on all targets, and visually indistinguishable at the short durations used (0.22–0.24 s). - -- **Tile stagger animation disabled on PS5.** The game grid entrance animation staggers each tile's delay by `calc(var(--ti) * 26ms)`. With 50+ games this schedules 50+ separate composite tasks spread over ~1.3 s. On PS5, `isPS5` suppresses the `--ti` index (all tiles use `animation-delay: 0ms`), collapsing the stagger to a single simultaneous entrance. - -- **Game tile hover no longer transitions `box-shadow`/`border-color`.** `.cr-game-tile` previously transitioned `transform`, `border-color`, `box-shadow`, and `border-left-color` together on hover — on PS5's software-rendered WebKit, animating `box-shadow` forces a full repaint of every hovered tile, the same class of cost as the already-fixed `crCheatGlow`. The `.ps5` block now restricts the tile's transition to `transform` only. - -- **`crCardPulse` (launching-tile pulse) disabled on PS5.** This animation runs continuously (1.1s loop) on any tile mid-launch, animating `box-shadow` and `border-left-color` every frame — a full-repaint loop with no compositor to offload it to. Disabled entirely via `.ps5 .cr-game-tile.is-launching { animation: none !important; }`. - -- **`crShimmer` (loading-skeleton shimmer) disabled on PS5.** Up to 8 skeleton tiles re-rasterize a gradient sweep every frame during the initial game-list load. Disabled via `.ps5 .cr-skel-tile::after { animation: none !important; }`. - -- **`cr-rgb-cycle` (Support modal supporter-name color cycle) re-enabled on PS5.** Was briefly disabled via `.ps5 .cr-supporter-name { animation: none !important; }` (animates `color` + `text-shadow` on a 3-second loop, only 3 elements, only while the rarely-opened Support modal is up) — re-enabled by request, since the cost is low and the visual effect is wanted on PS5 too. - -- **Game tile background flattened to a solid color on PS5.** `.cr-game-tile` used a 2-stop `linear-gradient` background, rasterized independently per tile by the software renderer — 50+ separate gradient fills across a full grid. `.ps5` now uses a flat `background: var(--cr-panel)` instead. - -- These five fixes extend the same `.ps5` override block used by the original PS5 performance pass — `prefers-reduced-motion: reduce` already disabled transitions/animations on several of these same elements (`.cr-game-tile`, `.cr-skel-tile::after`), but that media feature isn't set by the PS5 browser, so it never applied there until now. - -- **Two more missed animations disabled on PS5**, found by auditing every `@keyframes` against the existing `.ps5` override block: `crSwitchScan` (the sweep on a cheat switch's `.is-pending`/`.is-applying` state) was running on every single cheat toggle, for the full duration of the apply request — exactly the window you want the PS5's CPU most free. `crVertScan` (launch-overlay sweep) was running during every game launch. Both now disabled via `.ps5`. - -- **Tile selection no longer rebuilds the entire game grid.** Clicking a tile to select it only ever needed to move the `.active` CSS class — but the click handler called `renderGames()`, a full `innerHTML` rebuild that destroys and recreates every tile's `` cover element (forcing a re-decode of every visible game's icon on every click). The handler now toggles `.active` directly on the previously/newly selected tile instead. (Favorite-toggle still does a full rebuild for now — it can change which tiles are visible/sorted depending on the active filter, so it needs more care before the same optimization is safe there. Game icons are also still served `Cache-Control: no-cache` and the HTTP server has no keep-alive at all — both identified as bigger remaining levers, not yet addressed.) - -### Memory Engine — mdbg I/O Migration (Game Freeze Fix) - -- **Cheat toggles and patch applies no longer freeze the game.** The read/write engine previously used ptrace (`pt_copyin`/`pt_copyout`) exclusively, which requires `PT_ATTACH`ing the target process — stopping it — for the entire duration of every apply, restore, or dashboard state-check call. Two threads attaching the same pid concurrently (e.g. a dashboard poll racing a cheat apply) was a known failure mode, observed as mid-patch `mprotect`/`copyout` corruption. Confirmed fixed on real hardware: games stay running through cheat applies and patch operations. -- **New `cr_mdbg.c`/`cr_mdbg.h` module.** Plain memory reads/writes (`read_process_memory`, `write_process_memory_forced`, the mask-pattern scanner, patch backup/verify) now go through `mdbg_copyin`/`mdbg_copyout`, which do not require the target to be attached or stopped — the game keeps running while its memory is read or written. -- **Firmware 8.20+ write fallback.** Sony's `mdbg_copyin` (writes only — reads are unaffected) silently stops working on PS5 firmware 8.20+. `cr_mdbg.c` detects this via `kernel_get_fw_version()` and falls back to a manual CR3/page-table walk plus a direct-map kernel write, ported from the `ps-patch-system` project's `proc_rw.c` (credit: idlesauce). -- **Removed the blanket attach/detach wrapping** from 8 call sites across `cr_cheats.c`, `cr_patch_parser.c`, and `cr_api.c` (cheat apply, patch apply/restore, dashboard state checks, address debug, dry-run preview) — the attach-as-liveness-check pattern was replaced with a plain `kill(pid, 0)` check, since mdbg no longer needs the attach to succeed first. -- **Fixed a latent ucred race in the SDK's mdbg implementation.** `mdbg_copyin`/`mdbg_copyout` elevate the calling process's own credentials (authid + caps) around each call with no internal locking — the same process-wide race `pt.c`'s ptrace wrapper already guards against. `pt_ucred_lock()`/`pt_ucred_unlock()` were exposed from `pt.c` so mdbg and ptrace calls from different threads now serialize through the same lock. -- **Went fully mdbg-only in the write path — ptrace removed from both remaining fallbacks (later in v0.14 development, supersedes the two bullet points originally here).** `write_process_memory_forced`'s mprotect-refused branch no longer retries via ptrace; it now writes directly through `mdbg_io_copyin` without ever having obtained RW on the page (no W^X exposure either way, since the page's protection bits are never touched in that branch). Confirmed working on real hardware. `write_via_codecave` (the `pt_mmap` code-cave remap fallback) was deleted entirely — it had no mdbg equivalent — which also removed the `cheat_codecave_fallback` config flag and its dashboard toggle ("Auto-relocate code caves"); any cheat that only ever applied via that remap will now fail to apply instead, with no replacement path. The only ptrace remaining anywhere in the codebase is the dev self-test endpoint (`handle_api_dev_memtest`), which is diagnostic-only and intentionally exercises ptrace itself. - -### Security & Reliability Audit - -A full review of every subsystem (~22k lines, excluding third-party code) turned up several real bugs, fixed the same day: - -- **Removed dead, non-functional HTTP auth plumbing.** The `X-CheatRunner-Token` header was parsed in `cr_http.c` but never compared against anything anywhere — `dev_reload_token` had already been separately deprecated from the config struct earlier, and the dashboard never sent the header either. Deleted the unused parsing/parameter-threading rather than wiring up real auth; CheatRunner's HTTP API remains intentionally unauthenticated, trusted-LAN-only by design. -- **Shutdown could previously interrupt an in-flight memory write.** `cheatrunner_request_shutdown`'s `SIGKILL` could in theory fire between a W^X `mprotect(RW)` and its `mprotect(RX)` restore. All shutdown paths now take the same lock `cr_cheats.c` holds for the duration of a cheat apply/disable before killing the process. -- **Fixed an undefined-behavior shift in patch XML hex-literal parsing.** `0x...`-style `Value` attributes longer than 16 hex digits triggered a `>>` shift of ≥64 bits in `cr_patch_parser.c`; now capped at 8 bytes (the literal can't represent more than 64 bits regardless). -- **Conflict detection no longer compares addresses across unrelated modules.** `cr_conflict.c` previously compared raw offsets between mods without accounting for per-mod `module_name`/`section` resolution, risking false-positive (or missed) conflict pairs for any multi-module cheat file. -- **Avatar upload now rejects oversized source images** before the resize allocations, instead of trusting decoded width/height from an untrusted upload body. -- **Removed a duplicate `MAX_GAMES` constant** (`cr_api.c` and `cr_appdb.c` each independently defined `1024`) in favor of one shared definition. -- **`jb_escalate_pid()`'s return value is now logged on failure** at its `cr_tile_pkg.c` call site instead of being silently discarded. -- **Fixed a signed-overflow UB in cheat address resolution.** `cr_memory.c`'s offset arithmetic now adds in unsigned 64-bit before the final cast, instead of signed pointer arithmetic that could overflow on a huge/garbage offset from a malformed cheat file. The existing `ADDR_IN_USER_RANGE` check still catches out-of-range results identically to before. -- **Fixed an unguarded read of `g_cfg_mtime`** in `cr_config.c`'s hot-reload check — now taken under the same lock every other config access uses. - -### Memory Engine — mprotect Removed From Both the Write and Read Paths (Stability Fixes) - -Two real kernel-panic/console-freeze incidents, both reproduced on hardware, traced back to `kernel_mprotect`/`kernel_get_vmem_protection` itself — not page protection state — corrupting kernel VM structures when called on a wrong or special memory page: - -- **`write_process_memory_forced` no longer calls `kernel_mprotect` at all.** Every write now goes straight through `mdbg_io_copyin`, the same path that was already used as a fallback whenever `mprotect` was refused — and which had already proven safe on real hardware repeatedly, including on addresses that had previously panicked the console when `mprotect` was attempted. mdbg bypasses page protection for the write itself, so the protection toggle was never actually required; PS5's x86-64 CPU also keeps same-core self-modifying code instruction-cache coherent without an explicit cache flush or protection transition, so no W→X step is needed for the patched code to execute correctly either. The cross-page write-splitting logic (a `kernel_mprotect` multi-page quirk workaround) was kept defensively since `mdbg_io_copyin`'s own multi-page behavior hasn't been separately verified. -- **`read_process_memory`'s execute-only (`.text`) read fallback was removed for the same reason.** The v0.13 XOM fix added a slow path that called `kernel_get_vmem_protection` + `kernel_mprotect` to temporarily add `PROT_READ` when a fast `mdbg_io_copyout` read failed. This fallback is hit on *every* dashboard cheat-state refresh (each enabled mod's current bytes are re-read to show on/off/mismatch status), and reproduced a console hang on real hardware when the dashboard's cheat list stopped responding after enabling several mods that all resolved to the same wrong-address-pattern family already known to be risky. The fallback is now gone entirely — a failed fast-path read just returns "unreadable" instead of attempting the kernel call. Trade-off: legitimate execute-only `.text` pages that genuinely need the protection toggle to be read will now show as unreadable in cheat state instead of their real value. -- **`cr_patch_parser.c`'s `mask`-type pattern scanner (`scan_for_pattern`) no longer scans past the first unreadable memory chunk.** It previously scanned up to a flat 384 MB ceiling from the module base regardless of read failures, skipping past any unmapped chunk and continuing deeper into unknown memory — reproduced a kernel panic on real hardware when applying a `mask`-type patch. It now stops at the first read failure (the practical edge of the mapped module) and is bounded by the same canonical user-address-range check (`ADDR_IN_USER_RANGE`, now shared via `cr_memory.h`) used elsewhere in the write/read engine. - -### Daemon Stability — Notification & Request-Buffer Fixes - -Found via a deliberate stress test (rapid repeated cheat toggling), reproduced and fixed the same day: - -- **`notify()` (PS5 system toast notifications) is now fire-and-forget on a detached thread.** It previously called `sceKernelSendNotificationRequest` synchronously from inside the cheat-apply path, while holding `g_cheat_apply_lock`. Under rapid repeated toggling this call stalled, and since it ran while holding that lock, the entire toggle subsystem froze — CheatRunner stopped responding to any request until the payload was re-sent. The actual notification syscall now runs on its own detached thread; a stall there no longer blocks anything else. -- **The HTTP server no longer allocates 4 MB for every request.** `http_handle_client` previously `malloc`'d the full `MAX_REQ_SIZE` (4 MB) up front for every single connection, including the 5 background polling requests the dashboard fires every few seconds. It now starts with a 64 KB buffer and grows (`realloc`, doubling) only when an actual request needs more — the common case (polls, toggles) never grows past the initial allocation; large uploads (cheat files, avatars) still work via growth up to the same 4 MB ceiling. - -### Dashboard — Supporters List - -- Added Maffioh to the "Special thanks" list in the Support modal. - -### Version +
+v0.14 +- **CheatRunner's home-screen tile now installs into Media Players instead of the Games library**, plus a follow-up `param.json` audit (badge type, locale key, DRM type) found by comparing PS5's own app database against a known Media-tab app. +- **Fixed:** dashboard updates (new buttons, PS5 perf fixes) weren't showing up on PS5s that had already loaded the page — the AppCache manifest wasn't being bumped every release, and no JS handler existed to actually swap in an updated cache when one was detected. Both fixed. +- **PS5 browser performance pass.** Removed Google Fonts (blocking external font load), added HTTP caching for static assets, replaced `backdrop-filter` blur with solid backgrounds, hid decorative background layers, converted several `box-shadow`/`clip-path` animations to cheap `opacity`/`transform`-only ones, disabled tile-stagger and a couple of scan-line animations during cheat toggle/launch, and stopped tile-selection from rebuilding the entire game grid on every click. +- **Migrated the memory read/write engine from ptrace to mdbg.** ptrace required stopping the whole game process for every read/write, and two threads attaching at once could corrupt an in-progress patch — this was the root cause of games freezing mid-cheat-apply. Confirmed fixed on real hardware. Added a CR3-walk fallback for firmware 8.20+, where Sony's native mdbg write call silently stops working. +- **Went fully mdbg-only in the write path** — `kernel_mprotect` and the old ptrace-based code-cave remap fallback were removed entirely from writes. +- **Full security/reliability audit (~22k lines).** Removed dead HTTP auth code that was never wired up, closed a shutdown race that could interrupt an in-flight write, and fixed several smaller bugs: an undefined-behavior shift in patch hex parsing, a duplicate constant, a signed-overflow bug in address math, an unguarded config read, and cross-module false positives in conflict detection. +- **Fixed two real kernel-panic/console-freeze incidents**, both reproduced on hardware and traced to `kernel_mprotect` itself (not page-protection state) corrupting kernel memory on certain addresses. `kernel_mprotect` was removed from both the write path and the execute-only-memory read fallback; the patch mask-pattern scanner no longer scans past the first unreadable memory chunk. +- **Fixed a rapid-toggling freeze**, where a stalled PS5 notification call could lock up the entire cheat-apply subsystem because it ran while holding the apply lock. Also reduced the HTTP server's per-request memory allocation from a flat 4 MB to a small buffer that grows only when needed. +- Added Maffioh to the Support modal's "Special thanks" list. - Bumped to **0.14**. ---- - -## v0.13 - - -### Write Engine — Execute-Only (XOM) Memory Reads - -- **`read_process_memory` now reads PS5 execute-only `.text` pages.** The kernel maps game code as `PROT_EXEC` with no `PROT_READ`, so a raw `pt_copyout` on a hook site faults — every baseline/state read of code came back unreadable, and cheats showed permanent **mismatch / partial** state even though the version matched. The reader now detects the read failure, temporarily adds `PROT_READ` via `kernel_mprotect`, re-reads, and restores the page's **original** protection (preserving W^X). -- **Surgical and panic-safe by construction:** - - The fast path (`pt_copyout` succeeds) is unchanged — readable data/heap pages never touch page protection, zero overhead and zero new risk for the common case. - - The slow path runs only after a read already failed, and is gated by `ADDR_IN_USER_RANGE` *before* calling `kernel_get_vmem_protection` — the same guard the write path uses, because that kernel call can panic on a bad pointer. - - Bails out on `op < 0` (genuinely unmapped) and `op & PROT_READ` (already readable, so the read truly failed) without re-reading; restores the original protection on every exit path. `READ` is added without `WRITE`, so the page is never W+X. -- Fixes hook-based cheats (e.g. GoldHEN JSON with `vmovss`/code-cave hooks) that resolved to the correct address but failed every pre-write baseline read. Mirrors the approach used by the sibling elf-arsenal project. - -### Daemon — Crash Log - -- **SIGSEGV and SIGBUS are now caught.** A `sigaction(SA_RESETHAND)` handler writes a one-line entry (`signal=NN CheatRunner crashed`) to `/data/cheatrunner/crash.log` (appended, never overwritten) using only async-signal-safe calls (`open`/`write`/`close`/`_exit`), then terminates. Previously the daemon died silently with no on-disk record; now the crash is visible on the next FTP session without needing klogsrv. -- `CHEATRUNNER_CRASH_LOG_PATH` constant added to `cr_paths.h`. - -### HTTP Server — TCP_NODELAY on Accepted Sockets - -- `setsockopt(IPPROTO_TCP, TCP_NODELAY)` is now called on every accepted client fd immediately after `accept()`. Disables Nagle's algorithm so small JSON API responses are flushed without waiting to fill a segment — reduces per-request latency for the dashboard's continuous polling (fan temps, logs, game state) that each fire a tiny response. - -### Remote Downloads — Live Progress - -- **Download jobs now report byte-level progress.** The `sceHttpReadData` loop in `http_client.c` calls an optional `http_progress_fn_t` callback after each chunk; `received` and `total` (Content-Length, 0 if unknown) are passed through. -- New public API: `http_get_url_ex_timeout_progress(…, pfn, pud)` in `http_client.h`; existing `http_get_url_ex_timeout` delegates to it with `NULL` callbacks — no change for other callers. -- Download jobs (`CR_JOB_CHEAT_DOWNLOAD`) update `dl_recv` / `dl_total` on the job struct under `g_jobs_lock` on every chunk. -- The job status endpoint (`/api/sources/jobs/status`) now includes `dlRecv` and `dlTotal` when `state == "running"` and the server sent a Content-Length. -- **Dashboard**: the per-candidate Download button shows `Downloading… 42%` live while the job is running and resets to `Download` in a `finally` block. The progress callback in `pollSourceJob` now receives the full status object (not just the state string). - -### Write Engine — User-Range Guard - -- `write_process_memory_forced` now checks `ADDR_IN_USER_RANGE` at entry (the same macro already used for probe reads: `0x1000 ≤ addr ≤ 0x7FFFFFFFFFFF`). If the resolved cheat address is outside user space (GPU/MMIO, kernel text, null) the write is rejected with `write_rejected addr=0x… not_in_user_range` at error level and returns `-7` — preventing the `kernel_mprotect` / ptrace / codecave chain from touching memory regions where a ptrace read page-faults and freezes the game. - -### Thermals & Fan — Stale Cache Toast - -- When a remote cheat source fetch falls back to the on-disk index (network unavailable), the dashboard now shows an amber **"Results from cached index (network unavailable). Data may be outdated."** warning toast instead of the normal success message. Previously the user had no indication that the results were stale. -- Implemented by tracking an `any_stale` flag in `remote_cheat_find_work` (set when `source_load_cheat_entries` returns with `lerr == "stale_cache"`) and including `"stale": true` in the job result JSON. - -### Thermals & Fan — Configurable Threshold Range - -- The fan-on threshold slider bounds (`30–90°C`) were compile-time `#define` constants. They are now runtime-configurable via two new config keys: `fan_min_c` (default 30, valid 10–60) and `fan_max_c` (default 90, valid 50–100). -- All six validation sites in `cr_fan.c` read the range through a `fan_get_range()` helper that holds `g_cfg_lock` — no stale reads under concurrent config reload. -- `/api/fan/temp` and `/api/fan` responses include `minC` and `maxC`; the dashboard slider updates its `min`/`max` attributes on each poll. -- Config keys exposed via `GET/POST /api/config` alongside all other settings. - -### Thermals & Fan — °C / °F Toggle - -- A **°C / °F** toggle button (`fanUnitBtn`) was added to the Thermals & Fan action row. -- All three temperature cells, the threshold label, the slider live-drag label, and the Apply success toast convert through `toDispTemp(c)` / `fanDegStr()` based on the active unit. -- **Preference is persisted** to `localStorage('cr_fan_unit')` and restored on page load without a flash. -- The slider's internal value and the API call (`/api/fan/set?temp=`) always stay in Celsius — only what the user reads is converted. -- `/api/fan/temp` now also includes `thresholdF` (alongside the existing `thresholdC`) so the polling endpoint is self-consistent with the set endpoint. -- **Active-unit indicator**: the toggle button now gains `.btn.active` (brand fill + white text) when Fahrenheit is selected, making the current unit visually distinct rather than appearing as a plain label. Celsius shows as the default ghost style. State is applied on page load from `localStorage` so there is no flash. - -### Dashboard — Collapsible Sidebar Panels & Panel Reorder - -- **Every sidebar panel can now be collapsed.** A chevron button in each panel header collapses or expands the panel body with a smooth CSS animation — `max-height` transition (0.3 s cubic-bezier) with a faster opacity fade (0.15 s) that visually hides content before the height animation begins, masking the inherent jank of `max-height` transitions. Clicking the header row or the button both toggle. -- **State is persisted across sessions.** Collapse state for each panel is saved to `localStorage` (key `cr_panel_${id}`) and restored on next load without a visible flash — the transition is temporarily suppressed via `requestAnimationFrame` on first paint so the page does not animate collapsed panels on every load. -- **Setup panel reordered.** The Setup panel now appears immediately below Live Status (was previously fourth). New sidebar order: Live Status → Setup → User Profile → Thermals & Fan → Logs. -- All animations respect `prefers-reduced-motion`. - -### Dashboard — Font Upgrade (Space Grotesk / Inter / JetBrains Mono) - -- **Fonts now actually load.** The CSS had specified `Inter` (base) and `Outfit` (display) as first-choice fonts, but no `` tag was present — the PS5 browser silently fell back to `system-ui`. Added Google Fonts `preconnect` hints and a single combined stylesheet request with `display=swap`. -- **`--cr-font-display`** changed from `Outfit` → **Space Grotesk** — geometric, technical, and clean; used on game titles, panel headers, and modal titles. -- **`--cr-font-base`** stays as **Inter** — was already specified but was never delivered to the browser; now loads correctly. -- **`--cr-font-mono`** changed from `ui-monospace / Menlo / Consolas` → **JetBrains Mono** — purpose-built for technical content; hex addresses, log entries, and code-cave byte sequences now render with it. - -### Bug Fix — Memory Patch Status Always Showed "Not Ready" - -- **Fixed**: the Live Status and Setup panels always displayed **Memory Patch — Not Ready**, even on a fully jailbroken PS5 with `can_patch=1` logged at boot. -- Root cause: the JS read `s.cheats.canPatchGameMemory` from the `/api/state` response, but the `cheats` object in that response never included `canPatchGameMemory` — the field only existed in the `privileges` sub-object of a different endpoint that `refreshGlobalState()` never fetches. The field was always `undefined` → `false`. -- Fix: `canPatchGameMemory` is now included in the `cheats` object of both branches of the `/api/state` response (game running and idle), sourced directly from `cr_priv_can_patch_game_memory()`. - -### Write Engine — Dead Code Removal, Hardening & Heuristic Improvement - -- **Removed `write_process_memory` and `write_process_memory_ex` (dead code).** Every caller in `cr_cheats.c` and `cr_patch_parser.c` already uses `write_process_memory_forced`. The `_ex` path called `kernel_get_vmem_protection`, which panics on PS4 BC and PS5 special vmem entries — a loaded trap for any future caller who found the shorter-named function in the header. Both functions and the static `fmt_hex16` helper used only by `_ex` are removed from `cr_memory.c`; declarations removed from `cr_memory.h`. -- **Removed duplicate `ROUND_PG_DOWN/UP` macro definitions** from the top of `cr_memory.c` — identical definitions already exist in `cr_memory.h`; the per-file copies were redundant. -- **Clarified `write_process_memory_forced` R-X restore failure handling.** When `kernel_mprotect(PROT_READ|PROT_EXEC)` fails after a successful write, the function returns success and logs a warning — on PS4 BC pages the emulation layer manages page protections independently, so `mprotect(RX)` is rejected by the kernel but the hardware PTE still permits execution and the written bytes execute correctly. The warning log is preserved for diagnosis. -- **x86 instruction heuristic upgraded from 1-byte to 2-byte validation.** The Tier-2 address probe previously checked only the first byte of each candidate, giving a ~30–40% false-positive rate — `0x48` ("H") is common in ASCII strings and pointer data, but was treated as a REX prefix indicating executable code. The probe now requires a valid opcode as the second byte for REX-prefixed instructions (`0x40–0x4F`), and validates byte pairs for `0x0F` escapes, operand-size prefix, and ModRM-bearing opcodes. Single-byte unambiguous instructions (PUSH/POP reg, NOP, RET) and control-flow instructions (CALL rel32, JMP rel32/rel8, Jcc rel8) continue to be recognized by first byte alone. - -### Thermals & Fan Control - -- **Live temperatures.** A new **Thermals & Fan** panel in the sidebar shows CPU, SoC, and M.2 NVMe temperatures (read via `sceKernelGetCpuTemperature` / `sceKernelGetSocSensorTemperature`). Each temperature number turns **yellow** past 65°C and **red** past 80°C. The bar below the grid shows overall hottest-sensor level as a width indicator (neutral color). Polls every 4s. -- **Manual fan-on threshold.** A slider (30–90°C) + Apply sets the fan-on threshold through the `/dev/icc_fan` ioctl (`0xC01C8F07`). Lower threshold = fan spins up sooner = cooler/louder. -- **Stays pinned.** A background watcher re-applies the chosen threshold every 15s, because the firmware resets fan state on every game/app launch. The value is persisted to `/data/cheatrunner/fan.json` and re-applied at boot, so it survives a payload redeploy. -- **Graceful when unavailable.** `/dev/icc_fan` requires kstuff to be loaded; if it isn't, temperatures still display where possible and the control surfaces a clear "Needs kstuff" hint instead of failing silently. -- New endpoints: `GET /api/fan/temp`, `GET/POST /api/fan/set?temp=`, `GET /api/fan`. - -### User Profile — Rename & Change Picture - -- **New "User Profile" sidebar panel.** Shows the signed-in (foreground) user's name and ID. -- **Rename.** Type a new name (≤16 chars) and hit Rename — applied directly via `sceUserServiceSetUserName` (no external payload, no reboot for the registry write). -- **Change picture.** Choose any image (PNG/JPG/BMP, ≤4 MB); CheatRunner decodes it (`stb_image`), centre-crops to a square, and encodes the four **DXT5 `.dds`** sizes (64/128/260/440) the firmware expects. A live preview is shown, then **Apply** writes `avatar*/picture*.dds` + `online.json` into `/system_data/priv/cache/profile/0x/`. Re-open the user switcher (or reboot) to see it. -- Bundles the public-domain `stb_image` / `stb_image_write` single-header libs (`src/third_party/`). -- New endpoints: `GET /api/profile`, `POST /api/profile/name?name=`, `POST /api/profile/avatar` (body = image), `POST /api/profile/avatar/apply`, `GET /api/profile/avatar/preview?size=`. - -### Home-Screen Tile PKG — Auto-Install - -- **CheatRunner now installs its own home-screen tile on first boot.** On startup, a background thread (`cr-tile-inst`) checks whether the tile (`CHTR09999`) is already present at `/user/appmeta/CHTR09999`; if not, it waits 30s for the system to settle, writes the embedded PKG to `/data/cheatrunner/cheatrunner-tile.pkg`, and installs it via `sceAppInstUtilAppInstallPkg` (with an `sceAppInstUtilInstallByPackage` `file://` URI fallback). A PS5 notification confirms success or failure. On all subsequent boots the `stat` check short-circuits immediately — no re-install, no SceShellCore crash. -- The tile PKG (`dist/CheatRunner.pkg`, titleId `CHTR09999`) is **embedded at build time** as a C byte array (`cheatrunner_tile_pkg.h`). If `dist/CheatRunner.pkg` is absent when cmake configures, `CHEATRUNNER_HAVE_TILE_PKG` is not defined and the thread exits immediately — every other feature still works. -- New module: `src/cr_tile_pkg.c` / `cr_tile_pkg.h`. Linked against `libSceAppInstUtil`. -- Build order: run `pkg-source/build-pkg.sh` → then `./build-cheatrunner.sh --clean`. - -### Networking — Port Reuse on Payload Redeploy - -- **`SO_REUSEPORT` added to the HTTP listener** (`cr_http.c`). `SO_REUSEADDR` (already present since v0.12) only reclaims dead `TIME_WAIT` sockets. When a prior CheatRunner is stuck in an uninterruptible `accept()` sleep after receiving `SIGKILL`, the port stays bound — `SO_REUSEADDR` cannot reclaim it. `SO_REUSEPORT` allows the new instance to bind immediately; the kernel distributes incoming connections between both sockets. The old instance releases its socket within milliseconds once it wakes from sleep — the overlap is harmless. -- **Kill loop fast-breaks on `EPERM`** (`main.c`). If the old CheatRunner had already escalated itself (uid=0, elevated authid) and the kill loop runs before the new instance completes its own privilege escalation, `kill()` returns `EPERM`. The loop previously spun the full 60 iterations (~9 s) without killing anything, then `bind()` failed. The loop now breaks immediately on `EPERM` and logs *"cannot kill old instance (EPERM) — escalation may have failed"*, surfacing the root cause instead of flooding the retry log. - -### Bug Fix — kinfo_proc Structsize Off-by-One (`find_pid_by_name`) - -- **Fixed**: structsize guard in `find_pid_by_name` (`cr_game_monitor.c`) used `<= 467` to skip records too short to safely read `ki_comm` (kinfo_proc offset +447, 20 bytes, last byte at +466 — minimum valid structsize = 467). The `<=` form incorrectly skipped records of **exactly** 467 bytes — the minimum valid size — causing CheatRunner to miss its own process name on kernels that report the minimum-size record. Changed to strict `< 467`. - -### Bug Fix — kinfo_proc Structsize Off-by-One (`find_pid_for_app_id`) - -- **Fixed**: same off-by-one pattern in `find_pid_for_app_id` (`cr_game_monitor.c`). The guard used `<= 76` for `ki_pid` (offset +72, 4 bytes, last byte at +75 — minimum valid structsize = 76), incorrectly skipping records of exactly 76 bytes. Game pid lookup could silently miss the running game on kernels that return the minimum-size record. Changed to strict `< 76`. - -### Dashboard — Running Game Pinned to Top of List - -- **The active game always appears first in the games grid**, above favorites and alphabetical order. The sort comparator now promotes `running: true` entries before any other key. The live `running` flag is kept current by the existing status poller (`/api/running`), so the tile rises to the top the moment a game is detected — no page reload required. -- Backend also moves the running title to index 0 in `build_games_json_array` so freshly-built cache responses already have the correct order. - -### Dashboard — Profile Picture Crop & Resize UI - -- **Crop modal before upload.** Selecting a new profile picture now opens an interactive crop overlay instead of uploading the raw image immediately. The overlay shows a square crop box with rule-of-thirds grid lines and corner bracket handles. -- **Pan and zoom.** Drag to reposition the image inside the crop box; scroll wheel or pinch gesture to zoom; a **Zoom** slider below the canvas mirrors scroll/pinch and is usable with a D-pad. Minimum zoom is clamped so the image always fills the crop box — no empty borders. -- **Crop & Upload.** Confirming exports the 1:1 square region via `canvas.toBlob()` and POSTs it to `/api/profile/avatar`. The server-side centre-crop is then a no-op because the canvas already delivers a square, but remains in place as a safety net. -- 4 MB size guard and `picInput` reset still apply before the modal opens. - -### Dashboard — Game Playtime & Last Played on Tiles - -- **Each game tile now shows playtime or last-played information** below the title ID, with no additional API round-trips. -- **Last played date** is read directly from `tbl_contentinfo.lastAccessTime` in `/system_data/priv/mms/app.db` — the same timestamp the PS5 system itself maintains. Displayed as a relative label ("Last played: 2 days ago", "Last played: 3h ago") using the unix timestamp returned in the `/api/games` response as `lastAccessTime`. -- **CheatRunner-tracked playtime** (from `cr_activity.c`, accumulated while CheatRunner is running) is shown as "Xh Ym played" when available, taking priority over the last-played label. -- **Display priority:** native `playTime` column (reserved for firmware that exposes it) → CheatRunner session tracker → `lastAccessTime` relative label → nothing. -- Investigation of `/system_data/priv/mms/app.db` confirmed that Sony does not store cumulative playtime locally — it is tracked server-side via PSN. `lastAccessTime` is the best available local signal. -- `game_entry_t` extended with `play_time_seconds` and `last_access_time` fields (`cr_titles.h`). Both are populated via secondary queries in `appdb_collect_games_sqlite` and forwarded as `playTime` / `lastAccessTime` in the `/api/games` JSON response. - -### Dashboard — Supporter Credits - -- **Added "Special thanks" section to the Support modal.** Supporter names display with a continuously cycling RGB gradient animation (`@keyframes cr-rgb-cycle`), each name phase-shifted by 1 s so they cycle through different colors simultaneously. - -## v0.12 - -### Dashboard — Motion & Polish - -- **Animated view switching.** Switching between **All / ★ Favorites / Recent / PS5 / PS4 / Apps** now plays a staggered fade-and-slide entrance on the game tiles instead of an instant, flat swap — the grid visibly re-flows when you change tabs, search, sort, or toggle a favorite. The same entrance plays once on first load. -- **Active-tab accent.** The selected tab draws an animated brand-gradient underline; tabs lift on hover and depress on click for tactile feedback. Clicking the already-active tab is a no-op (no needless re-render). -- **Empty states fade in** rather than popping. -- Motion is scoped so background polls (status/launch refresh) don't re-trigger it, the entrance class self-clears so tile hover-lift keeps working, and everything is fully disabled under `prefers-reduced-motion`. - -### Dashboard — UI Refresh (Icons, Buttons, Loading, Focus) - -- **Crisp SVG icons.** The gear (Settings), heart (Support), and Favorites-tab star are now inline SVG instead of emoji/HTML entities, so they render identically and theme-colored across the PS5 browser and phones (matching the existing Discord/X icons). Game tiles get a ▶ icon on **Play** and a crosshair on **Cheats**; the favorite star is a filled/outline SVG pair that reflects state. -- **Primary-button sheen.** Play/primary buttons get a light sweep on hover for a more tactile feel; secondary buttons stay flat for clear hierarchy. -- **Controller & keyboard focus.** All buttons, tabs, inputs, the favorite star, and game tiles now show a clear brand-colored `:focus-visible` ring — D-pad/keyboard navigation is finally visible on console. (Mouse clicks stay ring-free.) -- **Loading skeletons.** The game grid shows shimmering placeholder tiles on first load instead of a blank area, replaced in place once titles arrive. -- **Live game pill.** When a game is running, the header pill shows a pulsing dot. -- **Games-in-view counter** above the grid count-ups as you filter/search. -- **Typography & color depth.** Inactive tabs use a cooler neutral so the crimson active state reads as "selected"; display headings tightened. All new motion respects `prefers-reduced-motion`. - -### Dashboard — Favorites, Recents & First-Run Onboarding - -- **Favorites** — every game tile now has a star button; favorited games are pinned to the top of the grid and get a dedicated **★ Favorites** tab. -- **Recent** — a **Recent** tab lists games you've recently launched or opened cheats for (most-recent first, capped at 12). Recorded automatically on launch and on opening a game's cheats. -- **Synced server-side** — favorites and recents are stored on the console (`/data/cheatrunner/favorites.json` via the new `/api/favorites` endpoints) instead of per-browser, so they're identical on every phone/PC that opens the dashboard. State updates optimistically and is pushed in the background. -- **First-run onboarding** — on first visit, a short guided overlay runs a system check (privileges / cheat engine / memory patching from `/api/state`), lets you pick a starting profile (Safe / Max Compatibility / Debug presets), and points you to where cheats come from. Shown once (localStorage `cr_onboarded`); dismissible via Skip or Esc. -- Context-aware empty states for the Favorites/Recent tabs. - -### Dashboard — Settings Redesign & UX Improvements - -- **Settings is now a top-level panel.** Added a **gear button** in the header that opens a dedicated Settings overlay. Previously settings were buried as a tab inside a specific game's cheat-trainer modal — you no longer have to open a game to change global config. -- **One-click presets** (`/api/config/preset`): **Safe** (recommended defaults), **Max Compatibility** (allow unverified/unsafe applies for stubborn MC4/SHN cheats), and **Debug** (verbose logging + diagnostics). Each applies a coherent bundle of related settings at once. -- **Plain-language labels + guidance.** Every option now has a human label and a one-line description instead of raw engine jargon. Recommended options show a green **Recommended** badge. -- **Risk signposting.** Dangerous toggles (force-unsafe applies, apply-unverified) are grouped under "Advanced (can crash games)", tagged with a red **Advanced** badge, and require a confirmation before being turned on. -- **Modified indicator.** Settings that differ from their default show a **Modified** badge. -- **Search** box filters settings live; empty groups hide themselves. -- **Reset to defaults** (`/api/config/reset`) — restores all settings (the live web port is preserved so you stay connected). -- **Theme picker** — the `theme` setting is now wired end-to-end with three dark schemes (Dark, Crimson, Midnight); applied on load and persisted. -- **Report Issue** button copies a diagnostics bundle to the clipboard and opens the Discord, turning manual log-gathering into one click. -- Per-setting saves now show a toast confirmation. New config fields exposed to the UI (`allow_unsafe_mc4_apply`, `allow_unsafe_shn_apply`, `cheat_log_candidates`, `cheat_mark_crash_suspect`, `cheat_apply_one_at_a_time`) with matching `set` handlers. -- Mobile: settings rows stack and controls enlarge on narrow screens. - -### Networking — Fix `bind() failed: 48` (port stuck / dashboard disconnects) - -- **Root cause: the "kill previous instance" step ran before privilege escalation.** On startup CheatRunner SIGKILLs any prior `CheatRunner.elf` so it can take the HTTP port — but that loop ran in `main()` *before* `jb_escalate_pid`/`cr_priv_init`. A previously-running instance has already escalated itself (uid 0 + a privileged authid), so the un-escalated newcomer's `kill()` was rejected with `EPERM`; the old process kept listening on port 9999, and the new instance's `bind()` then failed with `EADDRINUSE` (errno 48) on every retry — forever. (`SO_REUSEADDR`, which was already set, only reclaims dead `TIME_WAIT` sockets, not a live listener.) This showed up as an endless `[HTTP] bind() failed: 48` flood and, for users, as the dashboard "disconnecting" when a stale instance was left holding the port. Fix: the kill-old-instance loop now runs **after** privilege escalation, so the SIGKILL actually lands; it also logs if a target refuses to die. -- **Bounded, legible bind retries.** The HTTP bind loop no longer spams an identical failure line + toast every 3 s indefinitely. It retries ~5×, then emits a single clear message — for `EADDRINUSE`, *"port N still in use — is a previous CheatRunner running? Reboot the PS5 or close it."* — and backs off to a quieter 10 s cadence while still retrying, so it recovers automatically if the port frees up. - -### Stability Audit — ptrace Serialization, State-Read & Cache Hardening - -- **Critical: serialized the ptrace credential swap.** `sys_ptrace` in `pt.c` temporarily elevates CheatRunner's process-wide ucred (authid + caps) around each ptrace syscall, then restores it. This ran unserialized while ptrace is invoked from three threads — the cheat-apply thread (`apply_cheat_json`), the game-monitor thread (`patch_restore_all_for_pid`), and HTTP handler threads (dashboard state read, dev endpoints). Concurrent entry let one thread save another's *already-elevated* caps as its baseline and restore the process to a permanently-privileged state, while the other thread's restore dropped privileges mid-syscall (intermittent `pt_attach`/ptrace EPERM failures). Fix: a dedicated `g_ptrace_ucred_lock` now wraps the save→elevate→syscall→restore sequence; also restores `authid` on the partial-failure path (previously left elevated if the caps-set failed) -- **High: game-monitor no longer issues a competing `pt_attach` during an apply.** On game-stop the monitor called `patch_restore_all_for_pid`, which `pt_attach`es the pid. If the game genuinely exited mid-apply, this raced the apply thread's attach and stalled the 500 ms monitor poll for the full 2 s attach timeout (and failed anyway — pid already traced). Fix: the restore is skipped while `g_cheat_applying` is set; backups are still cleared and the next poll re-evaluates once the apply releases. Patched bytes in a dead process are gone with it, so nothing is lost -- **High: dashboard state-read defaults to relative address resolution.** The passive cheat-state poll resolved unverified MC4/SHN addresses via the legacy magnitude heuristic by default, which for large offsets reads the *raw* offset first — on titles where that maps to PS5 GPU/MMIO the ptrace read page-faults and never returns, freezing the game just from opening the cheat page. Fix: the state read now defaults to RELATIVE (`base + offset`, the correct target for these cheats) and treats the deprecated `legacy` value (and any unknown config) as relative; explicit `absolute`/`block` are still honored. Mirrors the relative-first ordering already used by the apply path -- **Medium: address-cache no longer silently truncates.** `addr_cache_save_locked` sized its JSON buffer at 800 B/entry, but a worst-case entry (escaped key + 128-byte orig hex + scaffolding) needs ~1060 B, so the bounded `snprintf` truncated mid-entry → unparseable JSON → the entire learned-address cache was dropped on the next load (forcing re-probing, which is itself the main freeze-exposure path). Fix: buffer sized to 1200 B/entry -- **High (latent): Tier-2 x86 address probe is now relative-first.** The instruction-heuristic probe in `cheat_resolve_write_addr_ex` read *both* address candidates unconditionally, including the raw absolute offset (the GPU/MMIO freeze vector). It is unreachable from the apply and state paths today (both force `auto_detect=0` when there is no reliable baseline), but was a loaded gun for any future caller. Fix: it now reads the relative candidate first and returns immediately when that looks like code, only reading the absolute candidate if the relative one is ruled out — mirroring Tier-1 and the reliable-probe branch -- **Medium: codecave write rolls back on verify failure.** `write_via_codecave` replaces the target page with a `MAP_FIXED` anonymous mapping, writes the original bytes back, then the patch. It freed the saved original *before* verifying, so a failed verify returned −1 while leaving a half-applied patch in the anonymous mapping — which the game could execute and crash. Fix: the original page contents are kept until after the verify and copied back (page re-marked RX) on failure before returning -- **Medium: bounded kinfo_proc offset reads.** `find_pid_by_name` / `find_pid_for_app_id` read fixed offsets into each sysctl process record (`ki_pid` at +72, `ki_tdname` at +447) without checking the record was long enough; a short/malformed final record could read past it. Fix: records shorter than the accessed offset are skipped -- **Low: manual cheat-file selection saved atomically.** `manual_sel_save_locked` used `fopen("w")`+`fwrite`, which can corrupt the selections file on power loss. Fix: switched to `write_file_atomic` (temp + rename), matching every other persisted file -- **Low: `g_ver_warn_last` race fixed.** The version-undetected log-dedup global is touched by `read_running_state` from multiple threads (monitor, apply, HTTP); added a mutex around its read/update (cosmetic correctness — prevents a data race on the dedup state) - -### MC4/SHN — Poisoned addr_cache No Longer Bricks Every Cheat - -- **Fixed**: a stale/poisoned `addr_cache` entry could make *every* mod of a cheat fail with `entry[N] bytes mismatch before ON`, even on titles that previously worked (reproduced on PPSA30803 — all 7 mods failed off cached entries) -- Root cause: on an `addr_cache` hit the cached `orig_bytes` become the validation baseline (`expected_reliable=1`). With `cheat_address_auto_detect=0`, `cheat_resolve_write_addr_ex` early-returns `rel_addr` as `OK_VERIFIED` **without probing**, so `resolve_st` is never `UNRESOLVED` and the existing stale-cache recovery path is bypassed. Validation then compares live memory against the poisoned cached bytes and hard-fails. (Cave entries slipped through via the lenient `cave_overwrite` branch; short hook entries had no escape.) -- Fix: when the pre-ON validation mismatch baseline came from the `addr_cache`, CheatRunner no longer fails the apply — it clears the poisoned cache entry and proceeds as an unverified write at the resolved (`base+offset`) address. Self-heals a bad cache instead of bricking the cheat, and is reached regardless of the `auto_detect` setting -- Instant recovery without rebuilding: delete `/data/cheatrunner/addr_cache.json` and relaunch — the cache rebuilds clean - -### MC4/SHN — Cheat Pages Left RWX After Write (Fixes Freeze-After-Apply) - -- **Fixed**: applying a code-cave MC4/SHN cheat could freeze the game shortly after, even when the cheat wrote successfully to the correct addresses (reproduced on PPSA20560 with ASLR off — `base=0x400000`, so addressing was provably exact, yet it still froze) -- Root cause: `write_process_memory_forced` set the target page `RWX` for the write, then **restored it to `R-X`** (stripping WRITE). That's fine for a hook in a pure code page, but MC4/SHN code caves sit well past `.text` (e.g. `base+0x3725da0`) in pages the **game itself writes to**. Removing write from such a page makes the game's next store there page-fault → freeze on resume -- Fix: both `write_process_memory_forced` and `write_via_codecave` now leave the page `PROT_READ|PROT_WRITE|PROT_EXEC` after writing. This preserves both execution of the cave and the game's own writes to that page. More-permissive only (adds WRITE) — cheats/patches that worked under R-X are unaffected -- `base=0x400000` in the logs is ASLR being off for that launch (not a wrong base) — it confirmed the freeze was page-protection, not addressing - -### MC4/SHN — Code-Cave & Master-Code Defaults Enabled - -- Enabled `cheat_codecave_fallback` and `cheat_master_code_fixup` by default (both were off): - - **codecave_fallback** — when a cheat write doesn't land in real writable memory, CheatRunner `pt_mmap`s an executable page at that address (the cave) and retries, instead of failing the cheat. Needed for MC4/SHN cheats whose code-cave region (e.g. a shared `0x37xxxxx` cave table) isn't backed by the eboot - - **master_code_fixup** — for MC-dependent mods it scans the live master-code region for the dependent's bytes and corrects the offset -- Both are **additive**: codecave_fallback only engages when a write fails/doesn't verify; master_code_fixup only for mods whose name marks them MC-dependent. Cheats that already applied cleanly are unaffected -- Note: this does **not** change CheatRunner's "no reliable baseline" safety gate. CheatRunner still refuses to apply unverified MC4/SHN by default (shown as `BASELINE_UNKNOWN`) unless `allow_legacy_mc4_without_expected` / `allow_legacy_shn_without_expected` is set — that remains an explicit user opt-in because force-applying an unverified cheat at a guessed address can crash the game - -### Shutdown / Reload — Robust SIGKILL - -- **Fixed**: "Shutdown Payload" / reload could appear to do nothing — the new CheatRunner sent from the PC would never come online, or the old one seemed to keep running -- Two root causes, both addressed with a bounded SIGKILL-then-verify approach: - - **New instance hung at boot**: `main()` kills any previous CheatRunner by name in a loop before starting. A process that has been SIGKILL'd but not yet reaped by its loader lingers as a **zombie** that still appears in the process list, so `kill()` keeps "succeeding" on it and the old unbounded `while (find_pid_by_name(...) > 0) { kill; sleep(1); }` spun **forever** — the new instance never reached the HTTP server. Now bounded (≤6 s of 100 ms retries) and then proceeds: a dead/zombie process holds no socket, so the bind still succeeds - - **Self-shutdown had no fallback**: the delayed shutdown thread (and both fallback paths) called `kill(getpid(), SIGKILL)` and, if that was somehow refused/deferred, simply returned — leaving CheatRunner alive. Added `_exit(0)` immediately after each self-SIGKILL so the process always terminates -- Net effect: shutdown reliably kills the running instance, and a freshly-sent CheatRunner.elf reliably takes over even if the previous one is mid-teardown or unreaped - -### Patches — Fixed Mid-Apply Failure from Concurrent ptrace (Bloodborne 60 FPS et al.) - -- **Fixed**: applying a multi-line patch could fail partway through with `mprotect_failed page=0x… rc=-1` → `cave read orig failed` → `write_failed`, then roll back (observed on CUSA00900 "60 FPS (With Deltatime)" at line 47). The backup phase had just read every line's address successfully, so the page was valid — it became inaccessible *during* the write loop -- Root cause: the patch-apply, single-patch-restore, and game-stop restore paths were the only ptrace users that held **no shared lock and set no busy-flag**. Every other path (cheat apply, dashboard state poll) coordinates through `g_cheat_apply_lock` + `g_cheat_applying`. So while a patch apply held the game under ptrace, the periodic dashboard state poll (every ~500 ms) called `pt_attach` on the same pid; that second attach fails (pid already traced) and its cleanup issues `PT_DETACH`, tearing down the patch thread's ptrace session. The next `pt_copyin`/`kernel_mprotect` in the patch loop then failed — whichever line happened to be executing when the poll fired -- Fix: `g_cheat_apply_lock` is now the single global ptrace-session gate (exposed via `cr_cheats.h`). The patch paths participate in it exactly like the cheat path: - - `patch_apply_entry_ex` and `patch_restore_entry` (user actions) take the lock **blocking** across attach→write→detach and set `g_cheat_applying` so the dashboard poll backs off - - `patch_restore_all_for_pid` (game-monitor thread, on game stop) takes it **non-blocking** (trylock) so the 500 ms poll never stalls and can't deadlock — deferring the restore one tick if ptrace is busy -- Net effect: no two threads can ever hold the game pid under ptrace simultaneously, so a patch apply can no longer be torn down by a concurrent state read. Cheat apply was already correct and is unchanged - -### Patches — Large Patches No Longer Time Out (Batched ptrace Credential Elevation) - -- **Fixed**: with the tear-down race resolved, large patches (e.g. Bloodborne "60 FPS (With Deltatime)", ~130 lines) now run to completion instead of failing early — but the apply was so slow it exceeded the HTTP request timeout (observed progressing past line 100 then "request timed out") -- Root cause: every `pt_copyin`/`pt_copyout` routes through `sys_ptrace`, which raises this process's ucred (≈4 kernel credential operations) and restores it **per call**. A 130-line patch performs a write + readback-verify per line — thousands of ptrace ops, hence thousands of kernel credential operations, which on PS5 kernel-RW is slow enough to blow the request timeout -- Fix: added `pt_batch_begin()` / `pt_batch_end()` to the ptrace layer. They raise the credentials **once** and hold the ucred lock; every `pt_*` call on the same thread until `pt_batch_end()` then skips the per-call swap and issues only the bare syscall (begin/end nest via a depth counter and are balanced). This is safe because the patch paths hold the global ptrace gate, so no other thread does ptrace while a batch is open -- The patch-apply, single-patch-restore, and game-stop bulk-restore loops are now wrapped in a batch — cutting roughly `2 × lines × 4` redundant kernel credential operations per apply. The cheat-apply path (few writes) is unchanged -- No behavior change to what gets written or verified — only the per-op credential overhead is eliminated - -### MC4/SHN — Cache-Backed Cheats Can Now Be Re-Enabled After Disable - -- **Fixed**: an MC4/SHN cheat with a learned addr_cache baseline could not be turned back on after being turned off — re-enable failed with `entry[N] bytes mismatch before ON at 0x…`, and the only recovery was relaunching the game (observed on PPSA30803 mod 0 "Untouchable") -- Root cause: on enable, the validator compared current memory against `must = exp_bytes` (the *original* game bytes captured in the addr_cache at first enable). But a disabled cheat legitimately holds its `ValueOff` bytes, and for MC4/SHN cheats ValueOff frequently differs from the captured original. So after a disable cycle `cur_bytes == off_bytes != exp_bytes` → "mismatch before ON" and the apply was rejected -- The bug was instruction-dependent: 5-byte `E9` JMP hooks happened to pass through the existing `hook_redirect` branch (`on_bytes[0]==0xE9 && cur_bytes[0]==0xE9`), so cheats like mod 1 "Infi Health" re-enabled by luck; 6-byte/non-`E9` hooks (mod 0) had no escape and failed every time. Unverified mods (no cache baseline) were unaffected because they skip the strict check -- Fix in `apply_cheat_json`: the pre-ON validation now accepts **either** the captured original bytes **or** the cheat's own `off_bytes` (ValueOff) as a valid baseline — a cheat sitting in its disabled state is a correct precondition for enabling, for any hook width or instruction type. This mirrors the ValueOff-acceptance logic that already existed in the address-resolution stale-cache recovery path - -### Config — Hot-Reload on File Change - -- CheatRunner now detects external edits to `/data/cheatrunner/config.ini` and reloads the config automatically, without requiring a payload restart -- Implementation: `config_check_reload()` is called once per game-monitor tick (~500 ms); it calls `stat()` on the config file and compares `st_mtime` against the last-seen value; reload only fires when the mtime changes -- `config_load()` now captures the file's mtime immediately after a successful read, so the first monitor tick never triggers a spurious reload at boot -- Reload is logged: `[info] [config] config file changed on disk, reloading` followed by the standard `[info] [config] config loaded` line -- Practical use: edit the config over FTP/SSH, changes apply within ~500 ms with no restart - -### Cheats — Source: HEN-PPSA-Cheats - -- Added **HEN-PPSA-Cheats** (`RDX-Sci01/HEN-PPSA-Cheats`, branch `main`, path `cheats`) as the fourth default remote cheat source, enabled by default -- Existing installations with a saved sources config are unaffected; the new source appears automatically on fresh installs or after resetting sources - -### Cheats — 1-Second Delay Between "Disable All" Calls - -- `GET /api/cheats/disable-all` now waits 1 second between each successive cheat disable call — prevents rapid back-to-back memory writes that could destabilise the game when multiple cheats are active simultaneously -- The delay is inserted only between calls, not before the first or after the last; mods that are already disabled are skipped without delay - -### Dashboard — Refresh Button Rescans Cheat and Patch Directories - -- The **Refresh** button now calls `/api/patches/rescan` on every click, invalidating the patch index so any XML files dropped manually into the patches directories are picked up immediately without a restart -- The cheat menu reload (`reloadCheatMenuForCurrentSelection`) now runs unconditionally when a title ID is known — previously it only fired when the cheat modal was already open; new cheat files dropped into the cheats directory are now reflected after a Refresh regardless of modal state - -### Cheats — PARTIAL PATCH No Longer Shown for Disabled MC4/SHN Hook-Codecave Cheats - -- Fixed: after disabling a `hook_codecave` MC4/SHN cheat that has no explicit `expected` bytes, the state probe could show `MIXED` ("PARTIAL PATCH" chip in the UI) instead of `BASELINE_UNKNOWN` (displayed as OFF), permanently blocking re-enable -- Root cause: for a two-entry cheat (cave + hook), the cave entry's ValueOff bytes (zeros) matched current memory → `off_val_matches++`; the hook entry's ValueOff bytes did not match → `baseline_unknown++`; the `BASELINE_UNKNOWN` decision branch required `off_val_matches == 0`, so the mixed `(off_val_matches=1, baseline_unknown=1)` combination fell through to `MIXED` — even though `on_matches == 0` and `mismatch == 0` confirm the cheat is not active -- Fix: removed `&& off_val_matches == 0` from the `BASELINE_UNKNOWN` branch in `cr_api.c`; the invariant is `on_matches == 0 && mismatch == 0` — if no entry matches its ON bytes and no reliable-baseline bytes mismatched, the cheat is not active regardless of how many entries soft-match their ValueOff; `MIXED` is now only reachable when `on_matches > 0 && on_matches < total` (a genuinely partial apply) - -### Patch Engine — Line Limit Raised from 64 to 640 (Critical: 60 FPS Patches Now Apply Fully) - -- **Critical**: `PATCH_MAX_LINES = 64` silently truncated any patch with more than 64 lines — the "60 FPS (With Deltatime)" patch for Bloodborne has 130 lines; the frame-interval write at address `0x0243487e` (around line 90) was never applied, leaving the game at 30fps despite the patch reporting success; other affected patches: "60 FPS" patches in other games (up to 283 lines), "Enable Dev Menu" (371 lines), "Restore Debug Camera" (569 lines) -- Fix: `PATCH_MAX_LINES` raised from 64 → 640 in `cr_patch_parser.h` — covers the largest patch in the current repository (569 lines) with margin; the parser loop already stopped cleanly at the cap, so no structural change was needed -- `PATCH_BACKUP_STORE_MAX` reduced from 64 → 16 in `cr_patch_parser.c` — the static `g_patch_backups` array size is proportional to `PATCH_MAX_LINES`; keeping 64 backup slots at 640 lines each would have grown the BSS section to ~8.9 MB; 16 slots cover all realistic simultaneous-patch scenarios at ~2.8 MB -- `PATCHES_ALL_MAX` in `cr_api_patches.c` changed from `PATCH_MAX_ENTRIES × PATCHES_MAX_SOURCES` (768) to a fixed 128 — bounds the temporary heap allocation for the merged-entry list; with larger `patch_entry_t` (640-line array) the old formula would have required ~272 MB per API request; 128 slots cover 64 entries × 2 source files, more than any game in the current patch repository -- Added `[warn] [patches] line_limit_hit entry='…' file=… cap=… dropped=…` log line when a PatchList has more lines than the cap — truncation is no longer silent - -### MC4/SHN — Multiple Mastercode Group Support - -- Fixed: `find_master_code_mod` always returned the first mastercode in the file, so mods belonging to a second mastercode group (e.g. mod[9] "Unlimited Health" depending on mod[5] "Speed Mastercode") would auto-enable the wrong mastercode (group 0's) before applying — `mc_scan_dep_addr` then scanned the wrong payload and wrote to a wrong address -- Renamed to `find_master_code_mod_for(mods, target_mod_idx)`: instead of returning the first mastercode, it returns the **nearest preceding** mastercode relative to the target mod index; for single-group files the result is identical; for multi-group files each group's mods correctly auto-enable their own mastercode -- Both callers updated: the auto-enable path (`apply_cheat_json` pre-lock) and the MC fixup setup - -### MC4/SHN — State Now Shows OFF_UNVERIFIED Instead of BASELINE_UNKNOWN After Restart - -- Fixed: after a CheatRunner restart, all MC4/SHN cheats that had been disabled showed `BASELINE_UNKNOWN` (displayed as OFF but with uncertain label) because the in-memory session tracking (`g_mods_disabled`) was cleared — the state handler had no way to distinguish "never applied" from "applied and disabled" without that tracking -- Fix: in the per-entry state loop, when `off_reliable == 0` (no explicit expected bytes), current memory is now compared against `ValueOff` bytes; if they match the entry is counted as `off_val_matches` (soft OFF) rather than `baseline_unknown`; when all entries match ValueOff, state reports `OFF_UNVERIFIED` instead of `BASELINE_UNKNOWN` — this survives restarts because the comparison reads live game memory, not ephemeral session state -- Similarly, if ALL entries match `on_bytes`, the state already correctly reports `CHEAT_STATE_ON` (memory read is authoritative); the session tracking is now only needed for the mixed/ambiguous case -- State decision tree updated with `off_val_matches` counter alongside the existing `baseline_unknown`, `off_matches`, and `mismatch` counters - -### MC4/SHN — Conflict Map No Longer Produces False Positives Between Mastercode Groups - -- Fixed: `conflict_map_build` compared all mod pairs by raw offset without any awareness of mastercode groups; two mods from different groups (e.g. "Infinite Stamina" under mastercode A and "Speed 2x" under mastercode B) that happened to share a raw offset were flagged as conflicting, blocking the user from enabling them simultaneously even though at runtime the auto-enable system treats them as independent -- Fix: before the O(n²) pair loop, each mod is assigned a `group_id` = index of its nearest preceding mastercode (−1 = before any mastercode, −2 = IS a mastercode); pairs where both mods have defined but different group IDs are skipped -- Additionally: mastercode vs its own dependents (mod_group[j] == i when mod_group[i] == −2) are no longer flagged as conflicting — enabling a dependent requires the mastercode to be active, not the other way around - -### MC4/SHN — Crash Suspects Auto-Expire After 7 Days - -- Fixed: crash suspects loaded from disk (`crash_suspects.json`) had no TTL and accumulated permanently — a cheat flagged after a bad address resolve (which may have since been fixed) blocked re-enabling indefinitely until the user manually clicked "Clear Crash Flags" -- Fix: `crash_suspects_load()` now computes the current time and discards any suspect whose `ts` timestamp is more than 7 days old; a warn log reports how many were discarded on startup - -### MC4/SHN — `mc_scan_dep_addr` Skips Uniform Off-Bytes - -- Fixed: when a dependent cheat's `ValueOff` bytes are all identical (all zeros, all NOPs, etc.) the scan loop matched the first occurrence of that byte in the MC code cave — a meaningless result that caused the cheat write to land at a random offset inside the mastercode payload -- Fix: before scanning, the function checks if all bytes in `dep_off` are identical; if so, it skips the scan and falls through directly to the low-byte formula fallback, which is more reliable than a false scan match - -### MC4/SHN — Section Field Now Resolves Dynlib Module Base - -- Changed: entries with `section > 0` previously logged a "not yet mapped to module" warning and silently used the eboot base — the cheat wrote to the wrong address for any entry targeting a dynamically loaded library -- Fix: `section_num` is now passed to `kernel_dynlib_mapbase_addr(pid, section_num)` to attempt resolving the module base for that entry; if the call returns a valid base it is used as `entry_base` instead of `mod_base`; if the call fails (module not loaded at that handle index) the code falls back to eboot base and logs a warn explaining the failure -- The section base is per-entry (not per-mod), allowing mixed entries in the same mod - -### MC4/SHN — Address Cache No Longer Poisoned by ValueOff Bytes on Stale Recovery - -- Fixed: when the addr_cache held stale expected bytes (game binary updated) and the recovery path detected that current memory matched `ValueOff` bytes, it updated `exp_bytes = off_bytes` and re-stored them in the cache — subsequent applies treated off_bytes as a reliable baseline; after a game update the new original bytes != off_bytes, causing false MISMATCH and blocking the cheat -- Fix: the `match_off` recovery branch no longer updates `exp_bytes`/`exp_len` or calls `addr_cache_set`; it uses the cached address (confirmed correct by the off_bytes match) and proceeds in unverified mode, which is safe with the default `allow_legacy_mc4_without_expected=1` - -### Mod Session Tracking — Increased Array Cap from 64 to 256 - -- Fixed: `g_mods_disabled` and `g_mods_enabled_arr` were capped at 64 entries; when more than 64 mods across all titles had been toggled in a session, the 65th and beyond were silently dropped from session tracking — `BASELINE_UNKNOWN` could not be promoted to `ON_UNVERIFIED`/`OFF_UNVERIFIED` for those mods, and conflict auto-disable could fail silently -- `MOD_DISABLED_MAX` raised from 64 → 256 in `cr_cheats.h` - -### Format Parser — Warn Instead of Silently Dropping Oversized Cheat Elements - -- Fixed: `shn_xml_to_json` silently skipped `` elements whose header exceeded 2048 bytes and `` elements exceeding 4096 bytes — the user had no indication a cheat was missing from the mod list -- Fix: both skip paths now emit `[warn] [cheat_fmt]` log lines with the element size and the limit, making truncation visible in the log panel - -### Bug Fix — "Disable All Cheats" Button Stuck on "Disabling…" After Re-Enable - -- Fixed: clicking "Disable All Cheats" successfully disabled all active mods, but subsequently re-enabling any cheat caused the button to reappear stuck — showing "Disabling…" and permanently disabled -- Root cause: the success path of the `disableAllCheatsBtn` click handler set `btn.disabled = true` and `btn.textContent = 'Disabling…'` at click time, then called `renderCheatMenu()` which hid the button (`display:none`) because `activeCount` dropped to zero; the DOM `disabled` and `textContent` properties were never reset on success — only the `catch` path restored them; when a cheat was later re-enabled, `renderCheatMenu()` showed the button again (`display:inline-flex`) but the stale `disabled=true` / `textContent='Disabling…'` state was still in the DOM -- Fix: `btn.disabled = false` and `btn.textContent = 'Disable All Cheats'` added to the success path immediately after `renderCheatMenu()`, matching the existing reset already present in the `catch` path - -### Bug Fix — MC Scan Fallback Address Missing `mod_base` - -- Fixed: in `mc_scan_dep_addr()`, the fallback path (reached when the dependent cheat's off-bytes are not found within the live MC region) returned a raw offset `(mc_base_off & ~0xFF) | (dep_raw_off & 0xFF)` without adding `mod_base` — the caller assigned this value directly to `addr` (an absolute process address), so the write landed at a small, wrong address (e.g. `0x8b6bXX` instead of `mod_base + 0x8b6bXX`) -- Impact: when MC fixup (`cheat_master_code_fixup=1`) was active and the byte-scan in the MC region failed to locate the dependent cheat's off-bytes, the cheat write targeted the wrong memory location; depending on what was mapped there, this corrupted unrelated memory or was silently remapped via the codecave path — in either case the intended cheat code was never reached, and the corrupted region could cause a game freeze when executed -- Fix: `mod_base` is now derived as `mc_addr - (intptr_t)mc_base_off` (since `mc_addr` is already the absolute process address of the MC region) and added to the combined offset before returning — producing the same absolute address that `cheat_resolve_write_addr_ex` would have computed for the same offset via its relative path - -### Title Lookup — PlayStation Store Fallback - -- New `cr_store_lookup.c` module: when the local appdb and param.sfo fail to resolve a game name, CheatRunner now fetches the PlayStation Store product page (`store.playstation.com/en-us/product/`) and extracts the title from the JSON-LD `