[Scummvm-git-logs] scummvm master -> 56763c322d0a69c065105df8bb385b4dc330fb22

neuromancer noreply at scummvm.org
Mon Jun 1 16:27:19 UTC 2026


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
56763c322d SCUMM: RA2: allow gamepad navigation


Commit: 56763c322d0a69c065105df8bb385b4dc330fb22
    https://github.com/scummvm/scummvm/commit/56763c322d0a69c065105df8bb385b4dc330fb22
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-01T18:26:45+02:00

Commit Message:
SCUMM: RA2: allow gamepad navigation

Changed paths:
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/metaengine.cpp
    engines/scumm/scumm.h


diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 3b34d0bd1cc..4c363df1be4 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -51,6 +51,47 @@
 
 namespace Scumm {
 
+const int kRA2MenuAxisThreshold = Common::JOYAXIS_MAX / 5;
+const uint32 kRA2MenuGamepadNavigationDebounceMs = 250;
+const uint32 kRA2MenuGamepadMouseSuppressMs = 250;
+
+bool isRebel2RawMenuAxis(int axis) {
+	return axis == Common::JOYSTICK_AXIS_LEFT_STICK_X ||
+	       axis == Common::JOYSTICK_AXIS_LEFT_STICK_Y ||
+	       axis == Common::JOYSTICK_AXIS_RIGHT_STICK_X ||
+	       axis == Common::JOYSTICK_AXIS_RIGHT_STICK_Y ||
+	       axis == Common::JOYSTICK_AXIS_HAT_X ||
+	       axis == Common::JOYSTICK_AXIS_HAT_Y ||
+	       axis >= 8;
+}
+
+Common::KeyCode getRebel2RawMenuAxisKey(int axis, int16 position) {
+	const bool horizontalAxis = axis == Common::JOYSTICK_AXIS_LEFT_STICK_X ||
+	                            axis == Common::JOYSTICK_AXIS_RIGHT_STICK_X ||
+	                            axis == Common::JOYSTICK_AXIS_HAT_X ||
+	                            (axis >= 8 && (axis % 2) == 0);
+	const bool verticalAxis = axis == Common::JOYSTICK_AXIS_LEFT_STICK_Y ||
+	                          axis == Common::JOYSTICK_AXIS_RIGHT_STICK_Y ||
+	                          axis == Common::JOYSTICK_AXIS_HAT_Y ||
+	                          (axis >= 8 && (axis % 2) != 0);
+
+	if (!horizontalAxis && !verticalAxis)
+		return Common::KEYCODE_INVALID;
+
+	if (position >= kRA2MenuAxisThreshold)
+		return horizontalAxis ? Common::KEYCODE_RIGHT : Common::KEYCODE_DOWN;
+	if (position <= -kRA2MenuAxisThreshold)
+		return horizontalAxis ? Common::KEYCODE_LEFT : Common::KEYCODE_UP;
+
+	return Common::KEYCODE_INVALID;
+}
+
+bool isRebel2MenuDirectionKey(Common::KeyCode keycode) {
+	return keycode == Common::KEYCODE_UP ||
+	       keycode == Common::KEYCODE_DOWN ||
+	       keycode == Common::KEYCODE_LEFT ||
+	       keycode == Common::KEYCODE_RIGHT;
+}
 
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_vm = scumm;
@@ -471,6 +512,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_joystickAxisX = 0;
 	_joystickAxisY = 0;
 	_gamepadAimActive = false;
+	_lastGameplayMenuCloseTime = 0;
+	_lastMenuGamepadNavigationTime = 0;
 
 	// Initialize level state tracking for multi-phase levels
 	_currentPhase = 1;
@@ -525,6 +568,23 @@ InsaneRebel2::~InsaneRebel2() {
 	}
 }
 
+void InsaneRebel2::openGameplayMainMenu(SmushPlayer *splayer) {
+	if (!splayer)
+		return;
+
+	if (_pauseOverlayActive) {
+		_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+		_pauseOverlayActive = false;
+	}
+
+	if (!splayer->_paused)
+		splayer->pause();
+
+	_vm->openMainMenuDialog();
+	splayer->unpause();
+	_lastGameplayMenuCloseTime = _vm->_system->getMillis();
+}
+
 // notifyEvent -- EventObserver callback for global input dispatch.
 // Handles ESC (skip) and SPACE (pause) regardless of menu state.
 // Pause behavior matches original FUN_405A21: SPACE pauses, ANY key unpauses.
@@ -558,11 +618,82 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 		}
 	}
 
+	if (_menuInputActive && event.type == Common::EVENT_JOYAXIS_MOTION) {
+		const int axis = event.joystick.axis;
+
+		if (!isRebel2RawMenuAxis(axis))
+			return false;
+
+		const Common::KeyCode keycode = getRebel2RawMenuAxisKey(axis, event.joystick.position);
+		if (keycode == Common::KEYCODE_INVALID)
+			return true;
+
+		const uint32 now = _vm->_system->getMillis();
+		if (_lastMenuGamepadNavigationTime != 0 &&
+				now - _lastMenuGamepadNavigationTime < kRA2MenuGamepadNavigationDebounceMs)
+			return true;
+
+		_lastMenuGamepadNavigationTime = now;
+		Common::Event syntheticEvent = Common::Event();
+		syntheticEvent.type = Common::EVENT_KEYDOWN;
+		syntheticEvent.kbd.keycode = keycode;
+		_menuEventQueue.push(syntheticEvent);
+		return true;
+	}
+
 	// Analog stick → reticle velocity (mirrors RA1's analog model). The mapped
 	// axis actions carry a signed position; a centered stick reports position 0.
 	// We ignore a recentre that would fight the opposite direction so a quick
 	// flick doesn't get cancelled by the trailing zero of the other axis half.
 	if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS) {
+		if (_menuInputActive) {
+			if (event.joystick.position == 0) {
+				switch (event.customType) {
+				case kScummBackendActionRebel2AxisUp:
+				case kScummBackendActionRebel2AxisDown:
+					return true;
+				case kScummBackendActionRebel2AxisLeft:
+				case kScummBackendActionRebel2AxisRight:
+					return true;
+				default:
+					break;
+				}
+				return true;
+			}
+
+			Common::KeyCode keycode = Common::KEYCODE_INVALID;
+			switch (event.customType) {
+			case kScummBackendActionRebel2AxisUp:
+				keycode = Common::KEYCODE_UP;
+				break;
+			case kScummBackendActionRebel2AxisDown:
+				keycode = Common::KEYCODE_DOWN;
+				break;
+			case kScummBackendActionRebel2AxisLeft:
+				keycode = Common::KEYCODE_LEFT;
+				break;
+			case kScummBackendActionRebel2AxisRight:
+				keycode = Common::KEYCODE_RIGHT;
+				break;
+			default:
+				break;
+			}
+
+			if (keycode != Common::KEYCODE_INVALID) {
+				const uint32 now = _vm->_system->getMillis();
+				if (_lastMenuGamepadNavigationTime != 0 &&
+						now - _lastMenuGamepadNavigationTime < kRA2MenuGamepadNavigationDebounceMs)
+					return true;
+
+				_lastMenuGamepadNavigationTime = now;
+				Common::Event syntheticEvent = Common::Event();
+				syntheticEvent.type = Common::EVENT_KEYDOWN;
+				syntheticEvent.kbd.keycode = keycode;
+				_menuEventQueue.push(syntheticEvent);
+				return true;
+			}
+		}
+
 		const int16 axisPosition = (event.joystick.position == Common::JOYAXIS_MIN)
 			? Common::JOYAXIS_MAX : event.joystick.position;
 
@@ -595,6 +726,67 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START ||
 		event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
 		const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+		const bool menuState = _gameState == kStateMainMenu ||
+		                       _gameState == kStatePilotSelect ||
+		                       _gameState == kStateDifficultySelect ||
+		                       _gameState == kStateChapterSelect ||
+		                       _gameState == kStateOptions ||
+		                       _gameState == kStateTopPilots;
+
+		if (event.customType == kScummActionInsaneSkip) {
+			if (!pressed)
+				return true;
+
+			if (_menuInputActive && menuState) {
+				Common::Event syntheticEvent = Common::Event();
+				syntheticEvent.type = Common::EVENT_KEYDOWN;
+				syntheticEvent.kbd.keycode = Common::KEYCODE_ESCAPE;
+				syntheticEvent.kbd.ascii = Common::ASCII_ESCAPE;
+				_menuEventQueue.push(syntheticEvent);
+				return true;
+			}
+
+			if (_gameState == kStateGameplay && _rebelHandler != 0) {
+				debug("Rebel2: Skip/back action ignored during gameplay");
+				return true;
+			}
+
+			debug("Rebel2: Skip/back action - skipping video");
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
+
+		if (event.customType == kScummActionInsaneBack) {
+			if (!pressed)
+				return true;
+
+			if (_menuInputActive && menuState) {
+				Common::Event syntheticEvent = Common::Event();
+				syntheticEvent.type = Common::EVENT_KEYDOWN;
+				syntheticEvent.kbd.keycode = Common::KEYCODE_ESCAPE;
+				syntheticEvent.kbd.ascii = Common::ASCII_ESCAPE;
+				_menuEventQueue.push(syntheticEvent);
+				return true;
+			}
+
+			if (_gameState == kStateGameplay && _rebelHandler != 0) {
+				if (_lastGameplayMenuCloseTime != 0) {
+					const uint32 elapsedSinceMenuClose = _vm->_system->getMillis() - _lastGameplayMenuCloseTime;
+					if (elapsedSinceMenuClose < 500) {
+						debug("Rebel2: Ignoring repeated gameplay menu action (%u ms)", elapsedSinceMenuClose);
+						return true;
+					}
+				}
+
+				debug("Rebel2: Back/menu action during gameplay - opening ScummVM menu");
+				openGameplayMainMenu(splayer);
+				return true;
+			}
+
+			debug("Rebel2: Back/menu action - skipping video");
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
 
 		if (pressed && splayer && splayer->_paused && _gameState == kStateGameplay) {
 			debug("Rebel2: Joystick action while paused - unpausing");
@@ -634,6 +826,12 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 				case kScummActionInsaneSwitch:
 					keycode = Common::KEYCODE_ESCAPE;
 					break;
+				case kScummActionInsaneBack:
+					keycode = Common::KEYCODE_ESCAPE;
+					break;
+				case kScummActionInsaneSkip:
+					keycode = Common::KEYCODE_ESCAPE;
+					break;
 				default:
 					break;
 				}
@@ -642,7 +840,9 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			case kStateTopPilots:
 				if (event.customType == kScummActionInsaneAttack)
 					keycode = Common::KEYCODE_RETURN;
-				else if (event.customType == kScummActionInsaneSwitch)
+				else if (event.customType == kScummActionInsaneSwitch ||
+				         event.customType == kScummActionInsaneBack ||
+				         event.customType == kScummActionInsaneSkip)
 					keycode = Common::KEYCODE_ESCAPE;
 				break;
 
@@ -651,6 +851,14 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			}
 
 			if (keycode != Common::KEYCODE_INVALID) {
+				if (isRebel2MenuDirectionKey(keycode)) {
+					const uint32 now = _vm->_system->getMillis();
+					if (_lastMenuGamepadNavigationTime != 0 &&
+							now - _lastMenuGamepadNavigationTime < kRA2MenuGamepadNavigationDebounceMs)
+						return true;
+					_lastMenuGamepadNavigationTime = now;
+				}
+
 				Common::Event syntheticEvent = Common::Event();
 				syntheticEvent.type = Common::EVENT_KEYDOWN;
 				syntheticEvent.kbd.keycode = keycode;
@@ -669,7 +877,40 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 		// consumed (and returned true for) the cases they handle.
 	}
 
+	const bool gameplayMenuTrigger = (event.type == Common::EVENT_MAINMENU) ||
+		(event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE);
+	if (gameplayMenuTrigger && _gameState == kStateGameplay && _lastGameplayMenuCloseTime != 0) {
+		const uint32 elapsedSinceMenuClose = _vm->_system->getMillis() - _lastGameplayMenuCloseTime;
+		if (elapsedSinceMenuClose < 500) {
+			debug("Rebel2: Ignoring repeated gameplay menu trigger (%u ms)", elapsedSinceMenuClose);
+			return true;
+		}
+	}
+
+	if (event.type == Common::EVENT_MAINMENU && splayer &&
+			_gameState == kStateGameplay && _rebelHandler != 0) {
+		debug("Rebel2: Main menu action during gameplay - opening ScummVM menu");
+		openGameplayMainMenu(splayer);
+		return true;
+	}
+
 	if (event.type == Common::EVENT_KEYDOWN) {
+		// Gamepad buttons mapped to ESC can lose their key-up while the GMM is open,
+		// leaving the key repeat source to synthesize another ESC immediately after
+		// returning to gameplay. Treat repeats as part of the original press.
+		if (event.kbd.keycode == Common::KEYCODE_ESCAPE && event.kbdRepeat) {
+			debug("Rebel2: Ignoring repeated ESC keydown");
+			return true;
+		}
+		if (_menuInputActive && event.kbdRepeat &&
+				(event.kbd.keycode == Common::KEYCODE_UP ||
+				 event.kbd.keycode == Common::KEYCODE_DOWN ||
+				 event.kbd.keycode == Common::KEYCODE_LEFT ||
+				 event.kbd.keycode == Common::KEYCODE_RIGHT)) {
+			debug("Rebel2: Ignoring repeated menu direction keydown");
+			return true;
+		}
+
 		// When paused during gameplay, ANY key unpauses (FUN_405A21 line 360-365).
 		// ESC additionally opens the ScummVM menu (original: quit key exits level).
 		if (splayer && splayer->_paused && _gameState == kStateGameplay) {
@@ -682,9 +923,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			splayer->unpause();
 			if (event.kbd.keycode == Common::KEYCODE_ESCAPE && _rebelHandler != 0) {
 				debug("Rebel2: ESC during pause - opening ScummVM menu");
-				splayer->pause();
-				_vm->openMainMenuDialog();
-				splayer->unpause();
+				openGameplayMainMenu(splayer);
 			}
 			return true;
 		}
@@ -709,12 +948,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 				} else if (_gameState == kStateGameplay && _rebelHandler != 0) {
 					// During active gameplay (handler != 0): pause and open ScummVM menu.
 					debug("Rebel2: ESC pressed during gameplay - opening ScummVM menu");
-					bool wasPaused = splayer->_paused;
-					if (!wasPaused)
-						splayer->pause();
-					_vm->openMainMenuDialog();
-					if (!wasPaused)
-						splayer->unpause();
+					openGameplayMainMenu(splayer);
 				} else {
 					// During cutscenes/intros/mission briefings: skip video
 					debug("Rebel2: ESC pressed - skipping video");
@@ -762,12 +996,16 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	switch (event.type) {
 	case Common::EVENT_KEYDOWN:
 	case Common::EVENT_LBUTTONDOWN:
-	case Common::EVENT_MOUSEMOVE:
 	case Common::EVENT_QUIT:
 	case Common::EVENT_RETURN_TO_LAUNCHER:
 		// Queue these events for processing in processMenuInput()
 		_menuEventQueue.push(event);
 		break;
+	case Common::EVENT_MOUSEMOVE:
+		if (_lastMenuGamepadNavigationTime == 0 ||
+				_vm->_system->getMillis() - _lastMenuGamepadNavigationTime >= kRA2MenuGamepadMouseSuppressMs)
+			_menuEventQueue.push(event);
+		break;
 	default:
 		break;
 	}
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index fefd80a6f96..926c23d69ab 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -510,6 +510,9 @@ public:
 	// recenter the reticle (the cursor is locked ~center during gameplay, so a gamepad
 	// "click" lands at center). See notifyEvent()/updateGameplayAimFromGamepad().
 	bool _gamepadAimActive;
+	uint32 _lastGameplayMenuCloseTime;
+	uint32 _lastMenuGamepadNavigationTime;
+	void openGameplayMainMenu(SmushPlayer *splayer);
 	bool isBitSet(int n) override;
 	void setBit(int n) override;
 
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 93e61776248..c43f81ea35d 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1019,6 +1019,18 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 	Common::String gameId = ConfMan.get("gameid", target);
 	Action *act;
 
+	if (gameId == "rebel2") {
+		for (uint i = 0; i < keymaps.size(); ++i) {
+			if (keymaps[i]->getId() == "engine-default") {
+				delete keymaps.remove_at(i);
+				Keymap *engineKeyMap = new Keymap(Keymap::kKeymapTypeGame, "engine-default", _("Default game keymappings"));
+				engineKeyMap->setPartialMatchAllowed(false);
+				keymaps.insert_at(i, engineKeyMap);
+				break;
+			}
+		}
+	}
+
 	if (gameId == "ft") {
 		Keymap *insaneKeymap = new Keymap(Keymap::kKeymapTypeGame, insaneKeymapId, "SCUMM - Bike Fights");
 
@@ -1191,24 +1203,28 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel2AxisUp);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
 		act->addDefaultInputMapping("JOY_RIGHT_STICK_Y-");
+		act->addDefaultInputMapping("JOY_HAT_Y-");
 		rebel2Keymap->addAction(act);
 
 		act = new Action("RA2STICKDOWN", _("Stick down"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel2AxisDown);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
 		act->addDefaultInputMapping("JOY_RIGHT_STICK_Y+");
+		act->addDefaultInputMapping("JOY_HAT_Y+");
 		rebel2Keymap->addAction(act);
 
 		act = new Action("RA2STICKLEFT", _("Stick left"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel2AxisLeft);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
 		act->addDefaultInputMapping("JOY_RIGHT_STICK_X-");
+		act->addDefaultInputMapping("JOY_HAT_X-");
 		rebel2Keymap->addAction(act);
 
 		act = new Action("RA2STICKRIGHT", _("Stick right"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel2AxisRight);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
 		act->addDefaultInputMapping("JOY_RIGHT_STICK_X+");
+		act->addDefaultInputMapping("JOY_HAT_X+");
 		rebel2Keymap->addAction(act);
 
 		act = new Action("RA2FIRE", _("Fire / select"));
@@ -1218,12 +1234,20 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 
 		act = new Action("RA2COVER", _("Cover / back"));
 		act->setCustomEngineActionEvent(kScummActionInsaneSwitch);
+		act->addDefaultInputMapping("JOY_X");
+		act->addDefaultInputMapping("JOY_Y");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2SKIP", _("Skip / back"));
+		act->setCustomEngineActionEvent(kScummActionInsaneSkip);
 		act->addDefaultInputMapping("JOY_B");
+		act->addDefaultInputMapping("JOY_RIGHT_TRIGGER");
+		act->addDefaultInputMapping("AC_BACK");
 		rebel2Keymap->addAction(act);
 
 		act = new Action("RA2BACK", _("Skip / menu"));
-		act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
-		act->addDefaultInputMapping("JOY_Y");
+		act->setCustomEngineActionEvent(kScummActionInsaneBack);
+		act->addDefaultInputMapping("ESCAPE");
 		act->addDefaultInputMapping("JOY_START");
 		rebel2Keymap->addAction(act);
 
diff --git a/engines/scumm/scumm.h b/engines/scumm/scumm.h
index 94ebcfc4a7a..ae7b4bb7cd1 100644
--- a/engines/scumm/scumm.h
+++ b/engines/scumm/scumm.h
@@ -499,6 +499,8 @@ enum ScummAction {
 	kScummActionInsaneAttack,
 	kScummActionInsaneSwitch,
 	kScummActionInsaneCheat,
+	kScummActionInsaneBack,
+	kScummActionInsaneSkip,
 
 	kScummActionCount
 };




More information about the Scummvm-git-logs mailing list