[Scummvm-git-logs] scummvm master -> 111c6a8596889f4b904a89f90a502ecff1f36c41

neuromancer noreply at scummvm.org
Fri Jul 31 13:24:50 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:
111c6a8596 SCUMM: RA2: allow to load pilots directly from the load menu


Commit: 111c6a8596889f4b904a89f90a502ecff1f36c41
    https://github.com/scummvm/scummvm/commit/111c6a8596889f4b904a89f90a502ecff1f36c41
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-31T15:24:33+02:00

Commit Message:
SCUMM: RA2: allow to load pilots directly from the load menu

Changed paths:
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/saveload.cpp


diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index ec758c094c3..20fe3ba6027 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/events.h"
 #include "common/system.h"
 #include "common/util.h"
@@ -90,30 +91,48 @@ void InsaneRebel2::runGame() {
 		return;
 	}
 
-	playIntroSequence();
+	// Launching straight into a pilot from the launcher skips the intro.
+	if (ConfMan.hasKey("save_slot"))
+		loadGameState(ConfMan.getInt("save_slot"), true);
 
-	while (!_vm->shouldQuit()) {
-		int menuResult = runMainMenu();
-
-		if (menuResult == kMenuQuit || _vm->shouldQuit())
-			break;
+	if (!_pilotLoadRequested)
+		playIntroSequence();
 
-		if (menuResult == kMenuResumeDemo) {
-			playIntroSequence();
-			if (!_vm->shouldQuit())
-				showTopPilots();
-			continue;
-		}
+	while (!_vm->shouldQuit()) {
+		int menuResult = kMenuNewGame;
 
-		if (menuResult == kMenuNewGame) {
-			int pilotResult = runLevelSelect();
+		// A loaded pilot goes straight to the chapter selection.
+		if (_pilotLoadRequested) {
+			_pilotLoadRequested = false;
+		} else {
+			menuResult = runMainMenu();
 
-			if (pilotResult == kLevelSelectQuit || _vm->shouldQuit())
+			if (menuResult == kMenuQuit || _vm->shouldQuit())
 				break;
 
-			if (pilotResult == kLevelSelectBack)
+			if (menuResult == kMenuResumeDemo) {
+				playIntroSequence();
+				if (!_vm->shouldQuit())
+					showTopPilots();
 				continue;
+			}
+
+			if (menuResult == kMenuNewGame) {
+				int pilotResult = runLevelSelect();
 
+				if (pilotResult == kLevelSelectQuit || _vm->shouldQuit())
+					break;
+
+				// A load during the pilot menu already picked the pilot.
+				if (pilotResult == kLevelSelectBack && !_pilotLoadRequested)
+					continue;
+
+				if (_pilotLoadRequested)
+					_pilotLoadRequested = false;
+			}
+		}
+
+		if (menuResult == kMenuNewGame) {
 			int chapterResult = runChapterSelect();
 
 			if (chapterResult == kChapterSelectQuit || _vm->shouldQuit())
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index 59cd9529d1c..a4d1a537894 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -1038,12 +1038,7 @@ int InsaneRebel2::runLevelSelect() {
 		debugC(DEBUG_INSANE, "Pilot selection: %d (numPilots=%d)", _levelSelection, _numPilots);
 
 		if (_levelSelection < _numPilots) {
-			_activePilot = _levelSelection;
-			_difficulty = _pilots[_activePilot].difficulty;
-
-			for (int i = 0; i < 16; i++) {
-				_chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
-			}
+			selectPilot(_levelSelection);
 
 			debugC(DEBUG_INSANE, "Pilot '%s' selected (slot %d, difficulty %d)",
 			      _pilots[_activePilot].name, _activePilot, _difficulty);
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 03ccda4446e..e4c832de509 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -28,6 +28,7 @@
 #include "common/savefile.h"
 #include "common/util.h"
 #include "graphics/paletteman.h"
+#include "graphics/thumbnail.h"
 
 #include "audio/mixer.h"
 
@@ -513,6 +514,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	_numPilots = 0;
 	_activePilot = 0;
+	_pilotLoadRequested = false;
 	for (i = 0; i < kMaxPilots; i++) {
 		_pilots[i].init();
 	}
@@ -1373,6 +1375,51 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
 const uint16 kPilotSaveVersion = 2;
 
+// Pilots occupy the regular ScummVM save slots, so they need the standard SCUMM
+// header too: without it every slot reads as an invalid savegame.
+const uint32 kPilotScummSaveVersion = 124;
+
+bool writeRebel2SavegameHeader(Common::WriteStream *out, const Common::String &desc) {
+	char name[32];
+	memset(name, 0, sizeof(name));
+	Common::strlcpy(name, desc.c_str(), sizeof(name));
+
+	out->writeUint32BE(MKTAG('S', 'C', 'V', 'M'));
+	out->writeUint32LE(0);
+	out->writeUint32LE(kPilotScummSaveVersion);
+	out->write(name, sizeof(name));
+	return !out->err();
+}
+
+// A stream starting with the pilot magic is a pre-header file; rewind it.
+bool readRebel2SavegameHeader(Common::SeekableReadStream *in, uint32 *version) {
+	const uint32 tag = in->readUint32BE();
+	if (tag == kPilotSaveMagic) {
+		in->seek(0);
+		if (version)
+			*version = 0;
+		return true;
+	}
+	if (tag != MKTAG('S', 'C', 'V', 'M'))
+		return false;
+
+	in->readUint32LE(); // size, unused
+	uint32 hdrVersion = in->readUint32LE();
+	if (hdrVersion > 0xFFFFFF)
+		hdrVersion = SWAP_BYTES_32(hdrVersion);
+	if (hdrVersion < VER(52) || hdrVersion > kPilotScummSaveVersion)
+		return false;
+
+	char name[32];
+	in->read(name, sizeof(name));
+	if (in->err())
+		return false;
+
+	if (version)
+		*version = hdrVersion;
+	return true;
+}
+
 bool InsaneRebel2::loadPilots() {
 	_numPilots = 0;
 
@@ -1382,6 +1429,25 @@ bool InsaneRebel2::loadPilots() {
 		if (!sf)
 			break; // Slots are contiguous
 
+		uint32 scummVersion = 0;
+		if (!readRebel2SavegameHeader(sf, &scummVersion)) {
+			delete sf;
+			break;
+		}
+
+		// Only present once the slot carries a SCUMM header.
+		if (scummVersion >= VER(52) && !Graphics::skipThumbnail(*sf)) {
+			delete sf;
+			break;
+		}
+		if (scummVersion >= VER(56)) {
+			SaveStateMetaInfos infos;
+			if (!_vm->loadInfos(sf, &infos)) {
+				delete sf;
+				break;
+			}
+		}
+
 		uint32 magic = sf->readUint32BE();
 		if (magic != kPilotSaveMagic) {
 			delete sf;
@@ -1425,6 +1491,14 @@ bool InsaneRebel2::savePilots() {
 			continue;
 		}
 
+		// The pilot name doubles as the save description in the load screen.
+		Common::String desc(_pilots[i].name);
+		if (desc.empty())
+			desc = Common::String::format("Pilot %d", i + 1);
+		writeRebel2SavegameHeader(sf, desc);
+		Graphics::saveThumbnail(*sf);
+		_vm->saveInfos(sf);
+
 		sf->writeUint32BE(kPilotSaveMagic);
 		sf->writeUint16LE(kPilotSaveVersion);
 
@@ -1500,6 +1574,15 @@ void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives,
 		return;
 
 	PilotData &pilot = _pilots[_activePilot];
+
+	// Entry `levelIndex` holds the state the next level starts from; damage
+	// below 0xFF means it was already reached, so a replay has nothing to add.
+	if (pilot.damage[levelIndex] < 0xFF) {
+		debugC(DEBUG_INSANE, "RA2: level %d replayed, keeping score %d/lives %d over %d/%d",
+			levelIndex, pilot.score[levelIndex], pilot.lives[levelIndex], score, lives);
+		return;
+	}
+
 	pilot.score[levelIndex] = score;
 	pilot.lives[levelIndex] = lives;
 	pilot.damage[levelIndex] = damage;
@@ -1508,6 +1591,42 @@ void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives,
 	savePilots();
 }
 
+bool InsaneRebel2::selectPilot(int index) {
+	if (index < 0 || index >= _numPilots)
+		return false;
+
+	_activePilot = index;
+	_difficulty = _pilots[_activePilot].difficulty;
+
+	// 0xFF is PilotData::init()'s "never played" marker.
+	for (int i = 0; i < 16; i++)
+		_chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
+
+	return true;
+}
+
+Common::Error InsaneRebel2::loadGameState(int slot, bool startupLoad) {
+	// Re-read so a slot written by another session is picked up.
+	loadPilots();
+
+	if (!selectPilot(slot)) {
+		warning("RA2: slot %d does not hold a pilot", slot);
+		return Common::kReadingFailed;
+	}
+
+	_pilotLoadRequested = true;
+
+	// Unwind the running menu video so runGame() reaches the chapter selection.
+	if (!startupLoad) {
+		_menuSelectionConfirmed = false;
+		_vm->_smushVideoShouldFinish = true;
+	}
+
+	debugC(DEBUG_INSANE, "RA2: loaded pilot '%s' from slot %d (difficulty %d)",
+		_pilots[_activePilot].name, slot, _difficulty);
+	return Common::kNoError;
+}
+
 int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
 
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index a9858d8f441..6eaf222a63b 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -220,6 +220,13 @@ public:
 
 	void updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage, int32 rating);
 
+	// Activates a pilot and derives its unlocked chapters, as the pilot menu does.
+	bool selectPilot(int index);
+
+	// Loading a pilot drops straight into the chapter selection.
+	Common::Error loadGameState(int slot, bool startupLoad = false);
+	bool _pilotLoadRequested;
+
 	enum LevelSelectResult {
 		kLevelSelectBack = 0,
 		kLevelSelectPlay = 1,
diff --git a/engines/scumm/saveload.cpp b/engines/scumm/saveload.cpp
index 55af040617a..8bad50a6ddc 100644
--- a/engines/scumm/saveload.cpp
+++ b/engines/scumm/saveload.cpp
@@ -44,6 +44,7 @@
 
 #ifdef ENABLE_SCUMM_7_8
 #include "scumm/insane/rebel1/rebel.h"
+#include "scumm/insane/rebel2/rebel.h"
 #endif
 
 #include "backends/audiocd/audiocd.h"
@@ -86,6 +87,12 @@ Common::Error ScummEngine::loadGameState(int slot) {
 		if (rebel)
 			return rebel->loadGameState(slot);
 	}
+
+	if (_game.id == GID_REBEL2) {
+		InsaneRebel2 *rebel = (InsaneRebel2 *)((ScummEngine_v7 *)this)->getInsane();
+		if (rebel)
+			return rebel->loadGameState(slot);
+	}
 #endif
 
 	requestLoad(slot);
@@ -97,7 +104,7 @@ bool ScummEngine::canLoadGameStateCurrently(Common::U32String *msg) {
 		return false;
 
 #ifdef ENABLE_SCUMM_7_8
-	if (_game.id == GID_REBEL1)
+	if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2)
 		return true;
 #endif
 
@@ -162,6 +169,10 @@ Common::Error ScummEngine::saveGameState(int slot, const Common::String &desc, b
 		if (rebel)
 			return rebel->saveGameState(slot, desc, isAutosave);
 	}
+
+	// RA2 writes the active pilot itself, and pilot slots must stay contiguous.
+	if (_game.id == GID_REBEL2)
+		return Common::kNoError;
 #endif
 
 	requestSave(slot, desc);
@@ -175,6 +186,14 @@ bool ScummEngine::canSaveGameStateCurrently(Common::U32String *msg) {
 #ifdef ENABLE_SCUMM_7_8
 	if (_game.id == GID_REBEL1)
 		return true;
+
+	// No save interface: progress is written as levels are completed.
+	if (_game.id == GID_REBEL2) {
+		if (msg)
+			*msg = _("This game does not support saving from the menu. Progress is saved automatically when a level is completed");
+
+		return false;
+	}
 #endif
 
 	// Disallow saving in v0-v3 games when a 'prequel' to a cutscene is shown.




More information about the Scummvm-git-logs mailing list