|
MAIN MENU
EXTERNAL
|
| Data Model |
|
Two flat, pre-allocated arrays hold everything (allocated once in cheat_init, sized by cfg.max_cheats/cfg.max_blocks from config.bin):
typedef struct Cheat {
char name[32]; /* displayed name */
struct Cheat *parent; /* parent folder, for nesting */
u16 block; /* start index into the block array */
u8 length; /* number of code lines */
u8 flags; /* CheatFlags bitmask */
} Cheat;
typedef struct Block {
u32 address; /* address of the code */
u32 value; /* value of the code */
} Block;
Cheat — one entry per cheat or per folder/comment (folders and comments
are just cheats with length == 0 and specific flag bits set). A cheat's code lines
are blocks[cheat->block .. cheat->block + cheat->length).
Cheat.flags packs several orthogonal concerns into one byte:
| Flag | Meaning |
| CHEAT_HIDDEN | Containing folder is collapsed (display state) |
| CHEAT_FRESH | Just toggled on/off, not yet (re-)applied — used by the auto-off backup/restore logic |
| CHEAT_SELECTED | User has this cheat checked on |
| CHEAT_CONSTANT | Cheat is always-on regardless of the global enable toggle |
| CHEAT_CWCHEAT / CHEAT_PSPAR / CHEAT_PSPAR_EXT | Which engine parses/executes this cheat's code lines |
| CHEAT_FAVORITE | Displayed at top of the list (added in v1.65) |
Folders reuse the same struct with FolderFlags: FOLDER_EXPANDED/FOLDER_COLLAPSED (display state) and FOLDER_SINGLE_SELECT/FOLDER_MULTI_SELECT/FOLDER_COMMENT (behavior — single-select acts like a radio group, multi-select like independent checkboxes, comment folders are visual grouping only).
|
| Two Engines, One File Format |
|
TempAR supports two independently-implemented code interpreters:
- CWCheat — cheat_apply_cwcheat(). Standard CWCheat code types (constant write, pointer chains including multi-level/multi-write, conditionals, etc). On POPS (PS1-on-PSP), CWCheat-flagged cheats are instead run through cheat_apply_psx_gs() (PSX GameShark format).
- PSPAR / PSPAR Extended — cheat_apply_pspar(). Datel's PSP Action Replay code types, plus TempAR's own _N-prefixed extended type set (register operations, safe data store, counters, call-function-with-args, etc).
Which engine parses a cheat is decided at load time based on the code name prefix on
the last line of the code block in the source file (_C0/_C1/_C2/_N/etc), and is stored permanently as one of the CHEAT_CWCHEAT/CHEAT_PSPAR/CHEAT_PSPAR_EXT flag bits. A single code cannot mix both engines' line types.
|
| Load → Memory → Apply Pipeline |
- cheat_init(num_cheats, num_blocks, auto_off) — called once from main_thread, allocates the Cheat/Block arrays via kmalloc and optionally an original_values backup buffer if auto-off is enabled.
- cheat_load(game_id, dbnum, index) — tries, in order: the game's saved .db file, then falls back through cheat_load_db (CWCheat/PSPAR text format), cheat_load_bin (PSPAR .bin database), and cheat_load_npr (NitePR .txt, converted to PSPAR on the fly).
- cheat_load_db — streams the source file through the filebuffer line/word reader, building up Cheat/Block entries as it goes. Favorite markers (_F <index> lines) are collected into a deferred list and applied after all cheats are loaded, since a cheat's final index isn't known until the whole file is parsed.
- cheat_apply(action) — runs every plugin loop tick. For each cheat: if auto-off is compiled in, runs the backup/restore state machine first (lets a plain constant-write code be safely turned off without a matching "off" code, by restoring the original memory value). Then, if the cheat is active, dispatches to the matching engine. Reapplying every tick (rather than once) is what makes memory-patching codes "sticky" against game code that keeps overwriting the same addresses.
- cheat_save(game_id) — writes the current in-memory cheat list back out to the per-game .db file, including _F lines for favorited cheats.
|
| Address Model |
|
address_load/address_set are the single choke point for all cheat memory
reads/writes, taking a type byte encoding 8/16/32-bit width, OR'd with
CHEAT_REAL_ADDRESS to permit addresses outside normal user memory (kernel/hardware
register ranges — used by the "fake addresses" pad-state read feature, and by the
browser/decoder in real-address mode). If you're adding a new code type that touches
memory, go through these functions rather than dereferencing pointers directly.
Both ultimately call real_address(), which as of v1.70.4 lives in a shared,
SDK-independent header (src/include/addr.h) so it can be exercised by the
host-side test suite without a PSP toolchain:
static inline u32 real_address(u32 address) {
address &= 0x0FFFFFFF;
// VRAM is a legal raw target even though it sits below cfg.address_start
if(address >= 0x04000000 && address <= 0x041FFFFF) {
return address;
}
if(address <= cfg.address_end - cfg.address_start) {
return address + cfg.address_start; // relative offset into game memory
}
if(address >= cfg.address_start && address <= cfg.address_end) {
return address; // already absolute, in range
}
// ambiguous gap value or above range - clamp to the nearest valid edge
return (address < cfg.address_start) ? cfg.address_start : cfg.address_end;
}
This was audit issue #36: the previous implementation could return an address outside VRAM
and outside [address_start, address_end] for certain edge-case inputs, which is
exactly the kind of thing the v1.70 security hardening pass (below) was meant to close off.
If you're touching this function, the host tests are the fast feedback loop — no need
to boot a PSP or emulator to verify a clamping change.
|
| Favorites (added v1.65, hardened v1.70) |
|
Added in commit be643f5:
- New CHEAT_FAVORITE flag (0x80).
- Favorited cheats show first, then non-favorites in original order — implemented via cheat_get_by_display_index() and cheat_visible_count(), which compute a display-order index separate from the underlying storage index, rather than physically reordering the Cheat array.
- Toggled from the TRIANGLE cheat menu ("Add/Remove Favorite").
- Persisted in .db files as _F <index> lines — a new, unrecognized-by-old-parsers line type, chosen deliberately so older TempAR builds simply ignore the line rather than failing to parse the file.
| Real bug, real fix |
|
Follow-up fix (8080309): the initial implementation toggled the in-memory flag but
never called cheat_save(), so favorite status was lost on next boot/reload.
One-line fix: call cheat_save(gameid_get(0)) right after
cheat_toggle_favorite() in the menu handler. If you add another piece of
per-cheat mutable state, remember it needs the same explicit save call — there's no
automatic dirty-tracking/autosave in this codebase.
|
| Rendering regression (21dac59) |
|
Introducing display-order indexing changed the cheat list's on-screen scroll-window
calculation, which shrank the visible window to ~13 items near the top of the list
instead of the intended 25. Fixed by reworking the window math in
layout_cheats() to always maintain a full 25-item window. See
Menu System for the general pattern this function
follows.
|
|
| Security Hardening (v1.70, commit 21dac59) |
|
A dedicated pass fixed ten numbered issues, mostly unchecked-length / unchecked-address bugs typical of code from an era before this class of bug was taken as seriously:
- boot_path wasn't guaranteed null-terminated after strncpy — fixed by explicit null-termination.
- memory_copy had no bounds check against the game's valid memory range — now clamped.
- The PSPAR 0x0E "patch" code type didn't validate its destination address/length before writing — now validated.
- All sprintf call sites replaced with snprintf across the codebase.
- game_id wasn't guaranteed null-terminated after UMD/POPS reads — fixed.
- kmalloc.c's partition-ID loop had an out-of-bounds iteration bug — fixed via a sizeof-based bound.
- The file-buffer read buffer wasn't guaranteed null-terminated — now allocated +1 byte and always terminated.
- patch_apply (memory patch file loader) didn't validate address + length before writing — now validated.
- The POPS game-ID address wasn't validated before being dereferenced — now validated.
- config.c gained a checksum plus field clamping/validation on load, so a corrupted or hand-edited config.bin can't push out-of-range values into cheat_init's allocation sizing.
If you're adding a new file parser, memory-writing code path, or fixed-size buffer fill, treat these ten as the checklist: null-terminate after every fixed-size string copy, validate lengths/addresses before any write derived from file/user input, and prefer snprintf over sprintf by default.
|
|