mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
bd65c814f7
The lobby rematch re-enters the emulator through goto session_reboot, below the first-boot session block. After its commit (offline) or netplay clear it reset mod-owned presentation state, but it never ran mod_runtime_activate_plugins() or the controller, load-acceleration and disc-speed resets. So an offline rematch with mods enabled applied the plan's main-EXE and disc patches and ran its VBlank callbacks, but no activation callback ran, and the function-entry hook table, which the commit empties and only activation rebuilds, stayed empty. It also mounted the stock disc even when the plan had built a derived disc image. start_mod_session() now holds the first-boot sequence, unchanged: clear the controller-mode overrides and presentation policies, load acceleration and disc speed; run reset_mod_owned_presentation(); activate; then apply the netplay local viewport, the controller-mode overrides and load acceleration. The first boot and the rematch both call it after their commit or netplay clear, and both then take the mounted disc from session_disc_path(), which returns the plan's derived image when there is one and otherwise the stock disc. Activation still precedes renderer and window creation on the rematch, because those happen after session_reboot on every session. The rematch re-applies its 4:3 Settings clamp before the session start, so a plugin's activation or the netplay local viewport can still set the aspect. A netplay rematch clears the plan, so activating it runs nothing and the match stays vanilla. The first boot's behaviour is unchanged. Tests: test_mod_owned_display_controls.py now requires activation and the mod-owned reset to happen only inside start_mod_session, in the order reset, activate, apply. It requires both paths to call it after their commit and before the disc mount, and the rematch 4:3 clamp to come before it. mod_runtime_test adds the lobby rematch sequence: activating after a netplay clear runs no callback, VBlank plugin or hook; the offline rematch's commit and activation re-run the activation callback and VBlank plugin and arm exactly the plan's hook. test_mod_load_acceleration.py follows the reset block into the lambda body. Two comments that said a soft return never re-applies the controller-mode override are corrected: test_launcher_pad_mode_wiring.py and test_launcher_pad_mode_resolve.cpp. MOD_PACKAGES.md now describes the rematch as a full session start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
5.4 KiB
C++
107 lines
5.4 KiB
C++
// Pad-mode resolution at the launcher exit seam
|
|
// (PSXRecompV4::resolve_player_mode_after_launcher, runtime/include/launcher_device.h).
|
|
//
|
|
// What this guards, in order of how it broke:
|
|
//
|
|
// 1. A locked title must boot the pad type its game.toml declared, whatever
|
|
// the launcher hands back. Ape Escape sets default_mode = "analog" +
|
|
// lock_mode = true; it booted DIGITAL because the launcher returned 2 for
|
|
// Player 1 (whose device defaults to keyboard on a release install) and the
|
|
// old readback loop took ls.pad_mode[] verbatim. sio.c then answered with
|
|
// the 4-byte digital pad response: right stick dead, left stick folded onto
|
|
// the D-pad, and lock_mode hides the selector that could have fixed it.
|
|
//
|
|
// 2. An ACTIVE mod controller-mode override outranks the lock. The first
|
|
// version of the clamp was an unconditional `if (lock) mode = locked`,
|
|
// which is correct for (1) and silently wrong here: `goto session_reboot`
|
|
// re-enters the emulator BELOW the first-boot block that applied
|
|
// g_mod_controller_mode_override, so a soft return from the lobby did not
|
|
// re-run it (the rematch now runs start_mod_session() itself). The
|
|
// override survived a rematch only because it round-tripped through
|
|
// ls.pad_mode[]; the clamp cut that path.
|
|
//
|
|
// 3. Whatever this returns is what gets persisted (seed.p_mode[] /
|
|
// us.p_mode[]), so a mode the game does not support must never come out of
|
|
// here for a locked title -- not even when the launcher returns garbage.
|
|
//
|
|
// PadMode values are the engine's (recompiler/src/config_loader.h:
|
|
// PAD_MODE_ANALOG = 1, PAD_MODE_DIGITAL = 2; 0 is Hybrid, mod-only). They are
|
|
// hard-coded here rather than dragged in through config_loader.h's dependency
|
|
// chain -- tests/test_launcher_pad_mode_wiring.py pins the enum so this cannot
|
|
// drift.
|
|
|
|
#include "launcher_device.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
|
|
namespace {
|
|
|
|
constexpr int kAnalog = 1; // PAD_MODE_ANALOG
|
|
constexpr int kDigital = 2; // PAD_MODE_DIGITAL
|
|
constexpr int kHybrid = 0; // mod-only, never selectable
|
|
constexpr int kNoOverride = -1;
|
|
|
|
int resolve(int launcher_mode, bool lock, int locked, int override_mode) {
|
|
return PSXRecompV4::resolve_player_mode_after_launcher(
|
|
launcher_mode, lock, locked, override_mode);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
// ---- unlocked: the launcher owns the seat ------------------------------
|
|
assert(resolve(kAnalog, false, kAnalog, kNoOverride) == kAnalog);
|
|
assert(resolve(kDigital, false, kAnalog, kNoOverride) == kDigital);
|
|
// The locked mode is ignored entirely when lock_mode is off, even when it
|
|
// disagrees -- ctrl_locked_mode[] is populated for every title, locked or
|
|
// not (main.cpp repopulates it outside the has_default_mode guard).
|
|
assert(resolve(kDigital, false, kAnalog, kNoOverride) == kDigital);
|
|
assert(resolve(kAnalog, false, kDigital, kNoOverride) == kAnalog);
|
|
|
|
// ---- locked: the game owns the seat (the reported bug) -----------------
|
|
// Ape Escape: ANALOG-locked, launcher returns DIGITAL because Player 1's
|
|
// device defaulted to keyboard. Must come out ANALOG.
|
|
assert(resolve(kDigital, true, kAnalog, kNoOverride) == kAnalog);
|
|
// And the other direction: a digital-only title (X4, Tomba 2) whose
|
|
// settings.toml predates the lock and still says analog.
|
|
assert(resolve(kAnalog, true, kDigital, kNoOverride) == kDigital);
|
|
// Idempotent when the launcher already agrees.
|
|
assert(resolve(kAnalog, true, kAnalog, kNoOverride) == kAnalog);
|
|
assert(resolve(kDigital, true, kDigital, kNoOverride) == kDigital);
|
|
|
|
// ---- persistence safety: a locked seat NEVER yields the launcher's
|
|
// value, so seed.p_mode[]/us.p_mode[] cannot receive an unsupported mode
|
|
// however wrong ls.pad_mode[] is (stale Hybrid, an ABI-width change, a
|
|
// settings.toml hand-edited to nonsense). -------------------------------
|
|
for (int bogus = -8; bogus <= 8; ++bogus) {
|
|
assert(resolve(bogus, true, kAnalog, kNoOverride) == kAnalog);
|
|
assert(resolve(bogus, true, kDigital, kNoOverride) == kDigital);
|
|
}
|
|
// Hybrid specifically: mod-only, and the launcher migrates it away, but a
|
|
// locked title must not adopt it even if one leaks through.
|
|
assert(resolve(kHybrid, true, kAnalog, kNoOverride) == kAnalog);
|
|
|
|
// ---- an active mod override wins over the lock ------------------------
|
|
// psx_mod_set_controller_mode_override() only ever stores ANALOG or
|
|
// DIGITAL (it rejects anything else loudly), so those are the two real
|
|
// override values.
|
|
assert(resolve(kDigital, true, kAnalog, kDigital) == kDigital);
|
|
assert(resolve(kAnalog, true, kDigital, kAnalog) == kAnalog);
|
|
// ...and over an unlocked launcher value too.
|
|
assert(resolve(kAnalog, false, kAnalog, kDigital) == kDigital);
|
|
assert(resolve(kDigital, false, kDigital, kAnalog) == kAnalog);
|
|
|
|
// ---- the "no override" sentinel is negative, matching the direct apply
|
|
// site (`if (g_mod_controller_mode_override[i] >= 0)` in main.cpp). Pinned
|
|
// so the two cannot drift into disagreeing about what "unset" means: a
|
|
// mode value of 0 would count as ACTIVE here, exactly as it does there.
|
|
assert(resolve(kAnalog, true, kDigital, -1) == kDigital);
|
|
assert(resolve(kAnalog, true, kDigital, -2) == kDigital);
|
|
assert(resolve(kDigital, false, kAnalog, -1) == kDigital);
|
|
assert(resolve(kDigital, true, kAnalog, kHybrid) == kHybrid);
|
|
|
|
std::puts("launcher pad-mode resolution guard passed");
|
|
return 0;
|
|
}
|