Skip to content

Mod: Quick Gadget Select - #7

Open
mthsk wants to merge 5 commits into
mstan:masterfrom
mthsk:master
Open

Mod: Quick Gadget Select#7
mthsk wants to merge 5 commits into
mstan:masterfrom
mthsk:master

Conversation

@mthsk

@mthsk mthsk commented Aug 6, 2026

Copy link
Copy Markdown

This is an attempt at recreating the Quick Gadget Switching feature from AE2 JP and AE3 in Ape Escape 1

Repeated presses on an equipped face button opens the menu and cycles over all unlocked gadgets, using the gadget with the right stick or pressing any other face button commits the change.

It's still very much imperfect, but usable.

Replay.2026-08-06.16-21-38.mp4

mthsk and others added 5 commits August 6, 2026 16:38
The slingshot-suppression half of the feature never ran. It was a
psx_mod_write_code_word() in an activation callback, and activation
callbacks fire at main.cpp:12196 -- before SDL_Init and before the run
loop, so the game's own EXE load overwrites 0x80063B6C long afterwards.
The address is inside the boot image (load_address 0x80010000, text_size
0xA5000), so the write was dead every launch and the stock ammo cycle
kept claiming the same second button press the row opens on.

Express it as a declarative [[patch]] instead. Those are applied at first
dispatch of the entry PC, guarded by expected bytes, and reapplied after
a savestate restore -- none of which the plugin path does. The second
plugin id is gone; the feature is one vblank plugin plus one patch.

Also in this change:

- Survive savestate and rewind. Plugin statics are not in a save state,
  so a restore under an open row left the snapshot describing a timeline
  that no longer exists, and cancelling wrote those stale bytes into the
  restored save. Every write now records what it left in guest RAM and
  each frame re-checks it; a mismatch abandons the row without writing.
- Place the row against the real right edge under widescreen via
  psx_mod_widescreen_x_margin() rather than a fixed 4:3 316.
- Stop leaking the icon texture page into the game's next frame: save it
  from GPUSTAT bits 0..10 and put it back, and clear the texture window
  first so a leftover window cannot wrap the icon UVs. Document why the
  drawing offset is deliberately inherited rather than set.
- Swap the right-stick X/Y address names, which were the wrong way round
  against the DualShock report order, and say in the feature description
  that the stick commit needs an analog pad.
- Use psx_mod_read_half() instead of reimplementing it.
- Record what the addresses are and what 0x08018F7C decodes to.

The test now asserts the planned main_exe write -- target, address,
expected and replacement bytes -- and that nothing is patched while the
feature is off. A plugin-count assertion passed against the original
manifest while the patch did nothing; this one fails against it.

Verified: gcc -Wall -Wextra clean on the plugin;
ape_preloaded_mods_test passes on this tree and fails with
"wrong Quick Gadget Select plugin plan" when the manifest is reverted to
the original. Expected bytes D2C24380 read from SCUS_944.23 at file
offset 2048 + 0x53B6C.

Not verified: no in-game run. The row's widescreen placement and the
GPU state handling are reasoned from the GP0/GPUSTAT contract, not
observed on screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on a running guest, at the title screen and inside the attract
demo's playable phase: the pad shadow reads 41 5A FF FF 00 00. The 0x41
is the controller reply id for a DIGITAL pad, which carries one halfword
of buttons and no axes at all -- so the two bytes this code was reading
as right X / right Y are outside the reply and read 0x00.

0x00 is not neutral. ape_axis_is_active() measures against the 0x80
centre, so 0x00 is a full deflection and the right stick reported as
permanently active. Any player whose pad enumerates as digital would
have had the row commit on the very frame it opened, making the feature
unusable rather than merely losing the stick shortcut. The previous
commit's comment claimed the opposite -- that the check "stays false" on
a D-Pad controller -- which was backwards.

Check the reply id first and fail closed: no DualShock, no axis read.

Verified: 41 5A FF FF 00 00 read live over the debug server at
0x800B87A0 on this machine with no gamepad attached, sampled at the title
screen and while transition phase was 0x03. The 0x80-centred analog path
is NOT verified -- it needs a real DualShock, which this machine does not
have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row was pinned to x=316, i.e. 320 minus a 4px margin. Ape Escape does
not run at 320. Measured over the debug server on a live guest, it moves
through three display modes -- 256x240 early in boot, 640x480, then
384x240 for gameplay -- so in game the row stopped 68 pixels short of the
right edge, with visible dead space beside it.

A hardcoded 384 would be no better: it would be wrong in the other two
modes. Take the width from psx_mod_display_width() at draw time instead,
and skip the row entirely while the geometry reports zero rather than
drawing it somewhere guessed.

In the gameplay mode the right edge is now 380 rather than 316.

Requires the psx_mod_display_width() addition to psxrecomp; the width the
presenter uses depends on the GP1(06h) horizontal range, which is
write-only and unreachable from a plugin, so this cannot be derived
locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Temporary pin onto the branch, not master. Quick Gadget Select needs
psx_mod_display_width() to right-align its row to the picture the guest is
actually scanning out, and that API does not exist in any released
framework commit yet.

Ape Escape moves through 256x240, 640x480 and 384x240, so no constant is
correct in every mode; the width has to be asked for at draw time. The
visible width depends on the GP1(06h) horizontal range, which is
write-only, so a plugin cannot derive it and the framework has to hand it
over.

Repoint this at master once mstan/psxrecomp feat/mod-display-width lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mstan

mstan commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Thanks for this — the feature works and the approach is right. I've pushed some commits to your branch rather than sending you a list of review notes; here's what changed and why.

The main thing: the slingshot patch never applied. psx_mod_write_code_word() from an activation callback is a no-op — activation runs before the guest boots, so the game's own EXE load overwrites the patch at 0x80063B6C. I moved it to a declarative [[patch]] in the manifest, which the framework applies at entry dispatch, guards on expected bytes, and reapplies after a savestate restore.

Verified across four builds by reading the site on a live guest:

build mod 0x80063B6C
master (no such mod) d2c24380
your original on d2c24380 ← never applied
declarative patch off d2c24380
declarative patch on 7c8f0108

That's a framework footgun rather than a mistake on your part — it fails completely silently, with no log line and no diagnostic. I've filed it upstream.

Right-stick check on a digital pad. The axis bytes only exist in a DualShock reply. On a digital pad they read 0x00, which against the 0x80 centre is a full deflection — so the stick read as permanently active and the row would commit on the very frame it opened. It now checks the controller reply id first and fails closed.

Row placement. 316 assumes a 320px screen. Ape Escape actually scans out 384 in gameplay (and 256 / 640 during boot), so the row sat 68px short of the right edge with visible dead space beside it. It now queries the width at draw time — which needed a small framework addition, psx_mod_display_width(), because the visible width depends on the GP1(06h) horizontal range, a write-only register a plugin can't read. Your branch temporarily pins that framework branch; it will move back to master once it lands.

Also in there:

  • Savestate / rewind safety. Plugin statics aren't part of a savestate, so a restore under an open row left the snapshot describing an abandoned timeline — cancelling would write stale gadget slots into the restored save. Every write now records what it left in guest RAM and re-checks it each frame, abandoning the row without writing if the world changed underneath it.
  • GPU state. The icon texture page is saved from GPUSTAT and restored, and the texture window is cleared so a leftover window can't wrap the icon UVs. The draw offset is still deliberately inherited from the game — that's what puts the row in the correct buffer half — and there's now a comment saying so, so nobody "fixes" it later.
  • The test now asserts the planned patch — target, address, expected and replacement bytes — plus that nothing is patched while the feature is off. The old plugin-count assertion passed against the original manifest while the patch did nothing.

Could you sanity-check it on your save? Particularly the open/cycle/commit feel, and the right-stick commit — no pad here presents as analog, so that path has still never actually run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants