diff --git a/docs/DEBUG.md b/docs/DEBUG.md index ebe755557..600a23bbf 100644 --- a/docs/DEBUG.md +++ b/docs/DEBUG.md @@ -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 @@ -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 diff --git a/port/include/stubs/subgame.hpp b/port/include/stubs/subgame.hpp new file mode 100644 index 000000000..b4534bfe0 --- /dev/null +++ b/port/include/stubs/subgame.hpp @@ -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"); diff --git a/port/src/debug_mode.cpp b/port/src/debug_mode.cpp index 9a9781dca..15117f3e1 100644 --- a/port/src/debug_mode.cpp +++ b/port/src/debug_mode.cpp @@ -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; @@ -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; diff --git a/port/src/debug_mode.hpp b/port/src/debug_mode.hpp index 59ce7d9a9..cc35fa343 100644 --- a/port/src/debug_mode.hpp +++ b/port/src/debug_mode.hpp @@ -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(); diff --git a/port/src/mainloop.cpp b/port/src/mainloop.cpp index 2b69cfb59..09f2d6227 100644 --- a/port/src/mainloop.cpp +++ b/port/src/mainloop.cpp @@ -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; @@ -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(); @@ -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(); @@ -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); @@ -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(); @@ -500,6 +516,7 @@ PORT_OVERRIDE void MainLoop() { PortDrawQuickChangeBanner(); PortDrawFrameBorder(); mgEndFrame(nullptr); + DebugModeEndMenuKeyTick(tick_frame, mgPortFrameCount()); if (mode_finished != 0) { PauseCancel(); @@ -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); @@ -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(); } diff --git a/port/src/mainloop_port.hpp b/port/src/mainloop_port.hpp index 793b93980..d807a5c1a 100644 --- a/port/src/mainloop_port.hpp +++ b/port/src/mainloop_port.hpp @@ -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. */ diff --git a/port/src/tests/debug_mode_test.cpp b/port/src/tests/debug_mode_test.cpp index 781ce7225..c7d9f719f 100644 --- a/port/src/tests/debug_mode_test.cpp +++ b/port/src/tests/debug_mode_test.cpp @@ -1,3 +1,4 @@ +#include #include #include "debug_fixture.hpp" @@ -5,10 +6,25 @@ #include "test_hooks.hpp" using GamePadDebugChord = dc::test::DebugFixture; +#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 "mg_memory.hpp" +#include "platform/input.hpp" +#include "platform/settings.hpp" +#include "scenesnd.hpp" +#include "sphida.hpp" +#include "subgame.hpp" #include "title.hpp" +// The running sub-game type that SubGameRunning reads (port/include/stubs/subgame.hpp). +extern int PortSubGame asm("SubGame_Type"); + namespace { constexpr int kAllShoulders = PAD_L1 | PAD_R1 | PAD_L2 | PAD_R2; @@ -81,3 +97,299 @@ TEST_F(GamePadDebugChord, PadLockSilencesTheChord) { EXPECT_FALSE(GameDebugEnabled() && DebugModeToggleChord(GamePad__2.GetPadOn(), GamePad__2.GetPadDown())); GamePad__2.key_lock = 0; } + +namespace { + +// Puts the player in control of a dungeon floor and a town walk, the state the debug_menu key reads, and restores the +// game's state afterwards. +class DebugMenuKey : public testing::Test { +protected: + void SetUp() override { + GamePad__2.key_lock = 0; + GamePad__2.pad[0].button = 0; + GamePad__2.previous_pad[0].button = 0; + DebugFlag = 0; + GetMainScene()->event_run = 0; + scene.event_run = 0; + DngMainScene = &scene; + DngStatus = {}; + DngStatus.mode = DNG_STATUS_FIELD; + BattleAreaScene = &area; + dngGetDebugInfo()->active = false; + PortSubGame = SUBGAME_NONE; + Sphida = nullptr; + PortTownLoopMode = EDIT_LOOP_WALK; + PortTownControlMode = EDIT_CONTROL_PLAYER; + PortTownLockChara = 0; + PortTownPause = 0; + EditDebugEnd(); + } + + void TearDown() override { + EditDebugEnd(); + dngGetDebugInfo()->active = false; + GamePad__2 = pad; + DebugFlag = debug_flag; + GetMainScene()->event_run = event_run; + DngMainScene = dng_scene; + DngStatus = status; + BattleAreaScene = battle_area; + PortSubGame = sub_game; + Sphida = sphida; + PortTownLoopMode = town_loop; + PortTownControlMode = town_control; + PortTownLockChara = town_lock; + PortTownPause = town_pause; + } + + // A dropped press must leave DebugFlag and pad 1 as they were. + void ExpectDropped(int loop) { + const int held = GamePad__2.pad[0].button; + const int previous = GamePad__2.previous_pad[0].button; + const int flag = DebugFlag; + EXPECT_EQ(DebugModeMenuButtons(loop), 0) << loop; + EXPECT_FALSE(DebugModeMenuKey(loop)) << loop; + EXPECT_EQ(DebugFlag, flag) << loop; + EXPECT_EQ(GamePad__2.pad[0].button, held) << loop; + EXPECT_EQ(GamePad__2.previous_pad[0].button, previous) << loop; + } + + // Releases every pad 1 button for a tick, as GamePad__2.UpDate would. + static void ReleasePad() { + GamePad__2.previous_pad[0].button = GamePad__2.pad[0].button; + GamePad__2.pad[0].button = 0; + } + + CScene scene; + DNG_BATTLE_AREA area{}; + CGamePad pad = GamePad__2; + int debug_flag = DebugFlag; + int event_run = GetMainScene()->event_run; + CScene *dng_scene = DngMainScene; + DNG_STATUS status = DngStatus; + DNG_BATTLE_AREA *battle_area = BattleAreaScene; + int sub_game = PortSubGame; + CSphida *sphida = Sphida; + int town_loop = PortTownLoopMode; + int town_control = PortTownControlMode; + int town_lock = PortTownLockChara; + int town_pause = PortTownPause; +}; + +// Binds debug_menu to F9 and presses it through the host input, as a keyboard or input script does. +class DebugMenuKeyInput : public testing::Test { +protected: + void SetUp() override { + HostSettings settings; + settings.key_bindings = { + {.action = "debug_menu", .keys = {"F9"}} + }; + InputResetBindings(); + InputApplyConfig(settings); + InputSetScriptedDevices({}); + DebugModeDiscardMenuKey(); + } + + void TearDown() override { + InputSetScriptedDevices({}); + DebugModeDiscardMenuKey(); + InputResetBindings(); + } + + static void Press() { + InputSetScriptedDevices({.keys = {SDL_SCANCODE_F9}}); + InputSetScriptedDevices({}); + } +}; + +} // namespace + +TEST_F(DebugMenuKey, OpensTheTitleOrFieldDebugMenu) { + EXPECT_EQ(DebugModeMenuButtons(LOOP_TITLE), PAD_START | PAD_SELECT); + EXPECT_EQ(DebugModeMenuButtons(LOOP_DUNGEON), PAD_R3); + EXPECT_EQ(DebugModeMenuButtons(LOOP_EDIT), PAD_R3); + + GamePad__2.pad[0].button = PAD_R3; + GamePad__2.previous_pad[0].button = PAD_R3; + EXPECT_TRUE(DebugModeMenuKey(LOOP_DUNGEON)); + EXPECT_EQ(DebugFlag, 1); + EXPECT_EQ(GamePad__2.GetPadDown(), PAD_R3); +} + +TEST_F(DebugMenuKey, DropsThePressInLoopsWithoutADebugMenuKey) { + for (int loop : {LOOP_MENU, LOOP_CHARA_VIEWER, LOOP_TEX_VIEWER, LOOP_MAP_VIEW, LOOP_SOUND_VIEWER, LOOP_MOVIE_VIEW, + LOOP_SV_CONV_VIEW}) { + ExpectDropped(loop); + } +} + +// MenuMainKey would take the R3 as a menu_debug_flag toggle (menumain.cpp:1433). +TEST_F(DebugMenuKey, DropsThePressWhileADungeonMenuIsOpen) { + for (auto mode : {DNG_STATUS_MENU, DNG_STATUS_EVENT_MENU}) { + DngStatus.mode = mode; + ExpectDropped(LOOP_DUNGEON); + } +} + +TEST_F(DebugMenuKey, DropsThePressWhileADungeonEventRunsOrThePlayerIsHeld) { + DngStatus.mode = DNG_STATUS_EVENT; + ExpectDropped(LOOP_DUNGEON); + DngStatus.mode = DNG_STATUS_FIELD; + GetMainScene()->event_run = 1; + ExpectDropped(LOOP_DUNGEON); + GetMainScene()->event_run = 0; + for (u32 flag : {DNG_PAUSE_PLAYER_STEP, DNG_PAUSE_FIELD_STEP, DNG_PAUSE_PLAYER_CONTROL}) { + area.pause_flag = flag; + ExpectDropped(LOOP_DUNGEON); + } +} + +// Fishing and Spheda run in field mode, and DngMainKey reads the debug R3 before either (dng_main.cpp:2176-2275). +TEST_F(DebugMenuKey, DropsThePressWhileADungeonSubGameOrSphedaRuns) { + PortSubGame = SUBGAME_FISHING; + ExpectDropped(LOOP_DUNGEON); + PortSubGame = SUBGAME_NONE; + + CSphida spheda; + Sphida = &spheda; + spheda.play_flag = 1; + ExpectDropped(LOOP_DUNGEON); + // A floor's Spheda object that is not being played leaves the key alone. + spheda.play_flag = 0; + EXPECT_EQ(DebugModeMenuButtons(LOOP_DUNGEON), PAD_R3); +} + +// EditDebugStart would reset MenuBuffer__2 under the open menu (editdebug.cpp:178-181). +TEST_F(DebugMenuKey, DropsThePressWhileATownMenuIsOpen) { + for (int mode : {EDIT_LOOP_WALK_MENU, EDIT_LOOP_EDIT, EDIT_LOOP_EDIT_MENU, EDIT_LOOP_WAIT_READ}) { + PortTownLoopMode = mode; + ExpectDropped(LOOP_EDIT); + } +} + +TEST_F(DebugMenuKey, DropsThePressWhileATownEventRunsOrThePlayerIsHeld) { + PortTownControlMode = EDIT_CONTROL_EVENT; + ExpectDropped(LOOP_EDIT); + PortTownControlMode = EDIT_CONTROL_PLAYER; + GetMainScene()->event_run = 1; + ExpectDropped(LOOP_EDIT); + GetMainScene()->event_run = 0; + PortTownLockChara = 1; + ExpectDropped(LOOP_EDIT); + PortTownLockChara = 0; + PortTownPause = 1; + ExpectDropped(LOOP_EDIT); + PortTownPause = 0; + PortSubGame = SUBGAME_FISHING; + ExpectDropped(LOOP_EDIT); +} + +TEST_F(DebugMenuKey, DropsThePressUnderAPadLockInEveryLoop) { + GamePad__2.key_lock = 1; + for (int loop : {LOOP_TITLE, LOOP_DUNGEON, LOOP_EDIT}) { + ExpectDropped(loop); + } +} + +TEST_F(DebugMenuKey, DropsThePressUnderAPadLockWhileADebugMenuIsOpen) { + dngGetDebugInfo()->active = true; + area.pause_flag = DNG_PAUSE_PLAYER_STEP | DNG_PAUSE_PLAYER_CONTROL | DNG_PAUSE_FIELD_STEP; + mgCMemory buffer; + EditDebugStart(215, &buffer); + PortTownControlMode = EDIT_CONTROL_DEBUG; + ASSERT_EQ(DebugModeMenuButtons(LOOP_DUNGEON), PAD_R3); + ASSERT_EQ(DebugModeMenuButtons(LOOP_EDIT), PAD_R3); + + GamePad__2.key_lock = 1; + ExpectDropped(LOOP_DUNGEON); + ExpectDropped(LOOP_EDIT); +} + +// Both presses go through DngMainKey, which opens the menu on the first R3 and closes it on the second. +TEST_F(DebugMenuKey, SecondPressClosesTheDungeonDebugMenu) { + ASSERT_TRUE(DebugModeMenuKey(LOOP_DUNGEON)); + EXPECT_EQ(DebugFlag, 1); + DngMainKey(); + ASSERT_TRUE(dngGetDebugInfo()->active); + ASSERT_FALSE(PortDungeonFieldControl()); + ReleasePad(); + DngMainKey(); + ASSERT_TRUE(dngGetDebugInfo()->active); + DebugFlag = 0; + + EXPECT_TRUE(DebugModeMenuKey(LOOP_DUNGEON)); + EXPECT_EQ(DebugFlag, 1); + DngMainKey(); + EXPECT_FALSE(dngGetDebugInfo()->active); + EXPECT_EQ(area.pause_flag, 0u); +} + +// EditDebugLoop draws through the renderer and the save data, so this stops at the R3 it closes on. +TEST_F(DebugMenuKey, SecondPressReachesTheOpenTownDebugMenu) { + ASSERT_TRUE(DebugModeMenuKey(LOOP_EDIT)); + // EditLoop opens the menu on that R3 and hands control to it (editloop.cpp:1730-1733). + mgCMemory buffer; + EditDebugStart(215, &buffer); + PortTownControlMode = EDIT_CONTROL_DEBUG; + ASSERT_FALSE(PortTownFieldControl()); + ReleasePad(); + DebugFlag = 0; + + EXPECT_TRUE(DebugModeMenuKey(LOOP_EDIT)); + EXPECT_EQ(DebugFlag, 1); + // EditDebugLoop closes on a new Cross or R3 (editdebug.cpp:458). + EXPECT_TRUE(GamePad__2.Down(PAD_CROSS | PAD_R3)); + // EditLoop does not reopen it on the same press: control stays with the menu until it closes. + EXPECT_EQ(PortTownControlMode, EDIT_CONTROL_DEBUG); +} + +TEST_F(DebugMenuKeyInput, APressWithinAnOrdinaryTickCountsOnce) { + DebugModeEndMenuKeyTick(10, 11); + Press(); + Press(); + EXPECT_TRUE(DebugModeTakeMenuKey(11)); + DebugModeEndMenuKeyTick(11, 12); + EXPECT_FALSE(DebugModeTakeMenuKey(12)); +} + +// The frame pause waits on sceGsSyncV, so no frame tells it apart: MainLoop discards when it resumes. +TEST_F(DebugMenuKeyInput, APressDuringAFramePauseIsDroppedOnResume) { + DebugModeEndMenuKeyTick(10, 11); + Press(); + DebugModeDiscardMenuKey(); + EXPECT_FALSE(DebugModeTakeMenuKey(11)); + DebugModeEndMenuKeyTick(11, 12); + Press(); + EXPECT_TRUE(DebugModeTakeMenuKey(12)); +} + +// PauseLoop ends frames of its own between two ticks. +TEST_F(DebugMenuKeyInput, APressDuringThePauseOverlayIsDropped) { + DebugModeEndMenuKeyTick(10, 11); + Press(); + EXPECT_FALSE(DebugModeTakeMenuKey(40)); + DebugModeEndMenuKeyTick(40, 41); + EXPECT_FALSE(DebugModeTakeMenuKey(41)); +} + +// A loading screen ends frames inside the tick that waits on it. +TEST_F(DebugMenuKeyInput, APressDuringALoadIsDropped) { + DebugModeEndMenuKeyTick(10, 11); + DebugModeEndMenuKeyTick(11, 90); + Press(); + EXPECT_FALSE(DebugModeTakeMenuKey(90)); + DebugModeEndMenuKeyTick(90, 91); + Press(); + EXPECT_TRUE(DebugModeTakeMenuKey(91)); +} + +// MainLoop discards when a mode starts, and the new mode's first tick has no tick before it. +TEST_F(DebugMenuKeyInput, APressAcrossAModeChangeIsDropped) { + DebugModeEndMenuKeyTick(10, 11); + Press(); + DebugModeDiscardMenuKey(); + Press(); + EXPECT_FALSE(DebugModeTakeMenuKey(11)); + DebugModeEndMenuKeyTick(11, 12); + EXPECT_FALSE(DebugModeTakeMenuKey(12)); +}