Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/DEBUG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,33 @@ lock. Loading between modes finishes first. The chord acts once per press;
release both buttons before using it again. While frame-paused, modified Start
captures instead. The existing four-shoulder/R3 toggle remains available.

## Debug menu key

`debug_menu` is a one-key shortcut for the routes above. Bind a key under
**PC SETTINGS** > **Bindings** > Debug menu (paddle 1 by default). With
`game.debug_mode` or `--jump`, a press turns `DebugFlag` on and presses the
buttons the current loop opens its debug menu with: Start+Select on the title,
which leaves for the developer menu, and R3 in a dungeon or town while the player
has control. A second press closes the open dungeon or town debug menu.

Presses in ordinary menus, events, Georama edit, sub-games, Spheda, under a pad
lock (even with a debug menu open) and in other loops are dropped before
`DebugFlag` or the pad changes. An open menu would otherwise take the R3 as its
own: the dungeon menu toggles `menu_debug_flag` (`ps2/src/menumain.cpp:1433`),
and the town debug menu would reset the buffer the open menu uses
(`ps2/src/editloop.cpp:1730`, `ps2/src/editdebug.cpp:178`). Dungeon fishing and
Spheda run in field mode but read the pad after the debug R3
(`ps2/src/dng_main.cpp:2176`, `2235`, `2275`). The control checks are the ones
floor and town autosave use (`port/src/mainloop.cpp`, `PortDungeonFieldControl`
and `PortTownFieldControl`); the dungeon adds sub-games, a played Spheda and the
player-control pause for the key alone (`port/src/debug_mode.cpp`).

The host counts presses whenever it polls input, and MainLoop reads them once a
tick. Presses made during the pause overlay, the frame pause, a load or a mode
change are dropped: MainLoop discards them after either pause and when a mode
starts, and a tick that ended more than its own frame (a loading screen) drops
the presses read at the next tick.

## Start+Select transition

The native main loop checks the host chord in the gameplay loop and the blocking
Expand Down Expand Up @@ -88,7 +115,7 @@ These work with the pad-2 Circle toggle. Pad 2 is locked unless the boot unlock
## PC gaps

- The keyboard feeds pad 1 only (`tools/kazarov/src/cpp/platform/input.cpp`, `Compose`); L2+Select makes its debug button reads available too.
- `developer_menu` (paddle 2) and `debug_menu` (paddle 1) are bound in `input.cpp` but nothing reads them.
- `developer_menu` (paddle 2) is bound in `input.cpp` but nothing reads it.
- GyoRace hits the `sgLoopGyoRace` stub (`port/src/gyorace.cpp`).

## Jump replays
Expand Down
4 changes: 4 additions & 0 deletions port/include/stubs/subgame.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

// Expose the running sub-game type, which SubGameRunning reads, so tests can stand a sub-game up without starting one.
extern int SubGame asm("SubGame_Type");
77 changes: 77 additions & 0 deletions port/src/debug_mode.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
#include "debug_mode.hpp"

#include "dng_debug.hpp"
#include "dng_main.hpp"
#include "editctrl_port.hpp"
#include "editdebug.hpp"
#include "editloop.hpp"
#include "gamepad.hpp"
#include "mainloop.hpp"
#include "mainloop_port.hpp"
#include "platform/input.hpp"
#include "scenesnd.hpp"
#include "sphida.hpp"
#include "subgame.hpp"
#include "title.hpp"

namespace {

// The frame the last ordinary tick ended on, or -1 after a wait.
int menu_key_frame = -1;

// Field control for the shortcut: the autosave check, less what runs in field mode but reads the pad after
// DngMainKey's debug R3 (dng_main.cpp:2176) or holds the player: a sub-game (2235), Spheda (2275, sphida.cpp:513) and
// the player-control pause.
bool DungeonDebugKeyControl() {
const CSphida *sphida = GetSphidaPtr();
return PortDungeonFieldControl() && !(BattleAreaScene->pause_flag & DNG_PAUSE_PLAYER_CONTROL) &&
!SubGameRunning() && !(sphida && sphida->play_flag);
}

} // namespace

void DebugModeBoot(bool debug_mode) {
if (!debug_mode) {
return;
Expand All @@ -13,6 +39,57 @@ void DebugModeBoot(bool debug_mode) {
MasterDebugModeOn = 1;
}

int DebugModeMenuButtons(int loop) {
// A locked pad reads no buttons, and the injected press would linger in its history.
if (GamePad__2.key_lock) {
return 0;
}
switch (loop) {
case LOOP_TITLE:
return PAD_START | PAD_SELECT;
case LOOP_DUNGEON:
// DngMainKey reads R3 only in the field. The open menu pauses the player and field steps, so it is matched
// on its own for the press that closes it (dng_debug.cpp:311).
return DungeonDebugKeyControl() || (DngStatus.mode == DNG_STATUS_FIELD && dngGetDebugInfo()->active)
? PAD_R3
: 0;
case LOOP_EDIT:
// The open menu holds control in EDIT_CONTROL_DEBUG, and its R3 closes it (editdebug.cpp:458).
return PortTownFieldControl() || (EditDebugMode() && PortTownControlMode == EDIT_CONTROL_DEBUG) ? PAD_R3 : 0;
default:
return 0;
}
}

bool DebugModeMenuKey(int loop) {
const int buttons = DebugModeMenuButtons(loop);
if (buttons == 0) {
return false;
}
DebugFlag = 1;
GamePad__2.pad[0].button |= buttons;
GamePad__2.previous_pad[0].button &= ~buttons;
return true;
}

bool DebugModeTakeMenuKey(int frame) {
bool pressed = false;
while (InputHostPressed(InputHostAction::DebugMenu)) {
pressed = true;
}
return pressed && menu_key_frame >= 0 && frame == menu_key_frame;
}

void DebugModeEndMenuKeyTick(int begin_frame, int end_frame) {
menu_key_frame = end_frame == begin_frame + 1 ? end_frame : -1;
}

void DebugModeDiscardMenuKey() {
while (InputHostPressed(InputHostAction::DebugMenu)) {
}
menu_key_frame = -1;
}

bool DebugModeToggleChord(int pad1_held, int pad1_pressed) {
constexpr int kShoulders = PAD_L1 | PAD_R1 | PAD_L2 | PAD_R2;
return MasterDebugCode == MASTER_DEBUG_CODE && (pad1_held & kShoulders) == kShoulders && (pad1_pressed & PAD_R3) != 0;
Expand Down
22 changes: 22 additions & 0 deletions port/src/debug_mode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,25 @@ void DebugModeBoot(bool debug_mode);

// Pad 1 is not locked by the debug lock, so this works while pad 2 is locked.
bool DebugModeToggleChord(int pad1_held, int pad1_pressed);

// The pad 1 buttons the bound debug_menu key presses this tick of loop, or 0 to drop the press. Start+Select opens the
// title's debug menu. R3 opens the dungeon's or town's only while the player has control there, and closes that menu
// once it is open. Ordinary menus, events, sub-games, Spheda, a pad lock and other loops get nothing.
int DebugModeMenuButtons(int loop);

// Delivers a debug_menu key press for loop: turns DebugFlag on and presses DebugModeMenuButtons(loop) on pad 1 as a new
// press. A dropped press leaves DebugFlag and the pad alone. Returns whether the press was delivered.
bool DebugModeMenuKey(int loop);

// MainLoop reads the debug_menu key once a tick, but the host counts its presses whenever it polls input. A press made
// during a pause, a frame pause, a load or a mode change would otherwise reach whatever follows, so presses count only
// when they arrive within one ordinary tick.

// Takes the pending debug_menu presses at the start of the tick that begins on frame (mgPortFrameCount), as one press.
// They are dropped unless the previous tick ended on frame: any frame ended since then belongs to a wait.
bool DebugModeTakeMenuKey(int frame);
// Records the tick that began on begin_frame and ended on end_frame. A tick that ended more than its own frame waited
// inside, for a load, so the presses pending at the next take are dropped.
void DebugModeEndMenuKeyTick(int begin_frame, int end_frame);
// Drops the pending debug_menu presses and the last tick, for the end of a blocking wait or a mode change.
void DebugModeDiscardMenuKey();
41 changes: 34 additions & 7 deletions port/src/mainloop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,7 @@ void WriteAutosave(int program_loop_no, AutosaveSpawn *spawn = nullptr) {
*/
void AutosaveFloor() {
const auto *save = GetSaveData();
const auto *scene = GetMainScene();
if (!save || DngStatus.mode != DNG_STATUS_FIELD || !scene || scene->event_run || GamePad__2.key_lock ||
!BattleAreaScene || (BattleAreaScene->pause_flag & (DNG_PAUSE_PLAYER_STEP | DNG_PAUSE_FIELD_STEP)) ||
save->save_dungeon.stage_id < 0 || save->save_dungeon.stage_id >= SAVE_DUNGEON_NUM) {
if (!save || !PortDungeonFieldControl() || save->save_dungeon.stage_id < 0 || save->save_dungeon.stage_id >= SAVE_DUNGEON_NUM) {
return;
}
const auto &dungeon = save->save_dungeon;
Expand All @@ -109,11 +106,10 @@ void AutosaveFloor() {

// Save on the first controllable exterior frame of each town map.
void AutosaveTown() {
CScene *scene = GetMainScene();
if (!GetSaveData() || !scene || IsEditMode() || PortTownLoopMode != EDIT_LOOP_WALK || PortTownControlMode != EDIT_CONTROL_PLAYER ||
PortTownLockChara || PortTownPause || scene->event_run || GamePad__2.key_lock || SubGameRunning() || InInterior()) {
if (!GetSaveData() || !PortTownFieldControl() || InInterior()) {
return;
}
CScene *scene = GetMainScene();
CCharacter2 *chara = scene->GetCharacter(scene->player_chara);
mgCCamera *camera = scene->GetCamera(scene->active_camera);
const int map = scene->GetNowMapNo();
Expand Down Expand Up @@ -167,6 +163,18 @@ void RecordCrashContext(int mode, const INIT_LOOP_ARG &arg) {

} // namespace

bool PortDungeonFieldControl() {
const auto *scene = GetMainScene();
return DngStatus.mode == DNG_STATUS_FIELD && scene && !scene->event_run && !GamePad__2.key_lock && BattleAreaScene &&
!(BattleAreaScene->pause_flag & (DNG_PAUSE_PLAYER_STEP | DNG_PAUSE_FIELD_STEP));
}

bool PortTownFieldControl() {
const CScene *scene = GetMainScene();
return scene && !IsEditMode() && PortTownLoopMode == EDIT_LOOP_WALK && PortTownControlMode == EDIT_CONTROL_PLAYER &&
!PortTownLockChara && !PortTownPause && !scene->event_run && !GamePad__2.key_lock && !SubGameRunning();
}

void PortAutosaveResetArrival() {
autosaved_floor.reset();
autosaved_map.reset();
Expand Down Expand Up @@ -416,6 +424,8 @@ PORT_OVERRIDE void MainLoop() {
LoopInit[PortLoopNo](InitArg);
GameGiveOnArrival(PortLoopNo);
PortAutosaveResetArrival();
// Presses made while the last mode ended and this one loaded belong to neither.
DebugModeDiscardMenuKey();
INIT_LOOP_ARG next_arg;

NextLoop(LOOP_MENU, next_arg);
Expand Down Expand Up @@ -487,7 +497,13 @@ PORT_OVERRIDE void MainLoop() {
DebugFlag = !DebugFlag;
}

// Taken every tick, so a press made where the key does nothing is dropped rather than kept for later.
if (DebugModeTakeMenuKey(mgPortFrameCount()) && GameDebugEnabled()) {
DebugModeMenuKey(PortLoopNo);
}

GameDebugFrame(PortLoopNo);
const int tick_frame = mgPortFrameCount();
mgBeginFrame(nullptr);
mode_finished = LoopMain[PortLoopNo]();
PortFinishQuickChange();
Expand All @@ -500,6 +516,7 @@ PORT_OVERRIDE void MainLoop() {
PortDrawQuickChangeBanner();
PortDrawFrameBorder();
mgEndFrame(nullptr);
DebugModeEndMenuKeyTick(tick_frame, mgPortFrameCount());

if (mode_finished != 0) {
PauseCancel();
Expand All @@ -520,6 +537,8 @@ PORT_OVERRIDE void MainLoop() {
if (GameDebugPauseRequested(GamePad__2)) {
pause = true;
}
// The frame pause waits on sceGsSyncV, which ends no frame, so its presses are dropped here.
const bool frame_paused = pause != 0;

while (pause != 0) {
sceGsSyncV(0);
Expand All @@ -544,14 +563,22 @@ PORT_OVERRIDE void MainLoop() {
break;
}
}
if (frame_paused) {
DebugModeDiscardMenuKey();
}

break;
}
}

bool paused = false;
while (!developer_menu_requested && PauseLoop() != 0) {
paused = true;
request_developer_menu();
}
if (paused) {
DebugModeDiscardMenuKey();
}

PauseCount();
}
Expand Down
11 changes: 11 additions & 0 deletions port/src/mainloop_port.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ void PortAutosaveResetArrival();
*/
void PortAutosaveStep(int program_loop_no);

/**
* Whether the player has control on a dungeon floor: the field is up, with no menu, event or locked input, and the
* player and field are stepping. Floor autosave and the debug menu key read it.
*/
bool PortDungeonFieldControl();
/**
* Whether the player walks a town with control: no Georama edit, menu, event, player lock, pause, locked input or
* sub-game. Town autosave, which also skips interiors, and the debug menu key read it.
*/
bool PortTownFieldControl();

/**
* Saves town edits and releases the active town's event, sub-game and audio state.
*/
Expand Down
Loading
Loading