[Scummvm-git-logs] scummvm master -> 24c327ead16461902940e03dec62a5a90ce1c719

neuromancer noreply at scummvm.org
Sun Sep 27 14:16:33 UTC 2026


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

Summary:
01b37b8a70 SCUMM: RA1: added support for demo v1.51
3113ae98e7 SCUMM: RA1: added support for demo 1994-04-13
ab41e63c79 SCUMM: RA1: added support for mac demo v1.02c
c6a90ffb60 SCUMM: RA2: added support for demo da1.06
6ad8570f34 SCUMM: RA2: added support for demo dg1.15
24c327ead1 SCUMM: RA2: added support for mac demo 1.0


Commit: 01b37b8a7090910cc5b51ec0aa01a06349bd977c
    https://github.com/scummvm/scummvm/commit/01b37b8a7090910cc5b51ec0aa01a06349bd977c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA1: added support for demo v1.51

Changed paths:
  A engines/scumm/insane/rebel1/releases.cpp
  A engines/scumm/insane/rebel1/releases.h
    devtools/scumm-md5.txt
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/insane/rebel1/saveload.cpp
    engines/scumm/module.mk
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index ec53bda19d4..ce415ea562c 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -450,6 +450,7 @@ ft	Full Throttle
 	8ead2c01c48c2e3d15d6a89b86cd84ab	19697	he	DOS	-	-	-	Hebrew fan translation
 
 rebel1	Star Wars: Rebel Assault
+	274f6026d41fa2a2990a6d6b7a561dc2	217559	en	DOS	Demo v1.51	Demo v1.51	CD-ROM demo	-
 	b9ac90e4411da1b6f7dd075cc4b03c2a	214435	en	DOS	-	v1.0	-	-
 	e210a57b756473be705d24b9378a8233	212383	en	DOS	-	v1.3	-	-
 	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 3f3977bc632..5bc9393be02 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -231,6 +231,7 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
 	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
+	{"rebel1", "Demo v1.51", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
@@ -505,6 +506,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 	{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
 
 	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo v1.51" },
 	{ "rebel1", "Rebel Assault", kGenUnchanged, UNK_LANG, Common::kPlatformMacintosh, "" },
 	{ "rebel1", "O1LOGO.AAA", kGenUnchanged, UNK_LANG, Common::kPlatform3DO, "" },
 	{ "rebel1", "SUB_AP.BIN", kGenUnchanged, UNK_LANG, Common::kPlatformSegaCD, "" },
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 86628801b66..0c40047c1ff 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -35,9 +35,8 @@
 namespace Scumm {
 
 const int kRA1MainMenuItemCount = 6;
-const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
 const int kRA1LevelSelectRowsPerCol = 8;
-const int kRA1NumLevels = 15;
+const int kRA1NumLevels = Rebel1Release::kNumLevels;
 const int kRA1MenuAxisThreshold = Common::JOYAXIS_MAX / 2;
 const int kRA1MenuLogicalWidth = 0x140;
 const int kRA1MenuFrameX = 0x32;
@@ -457,16 +456,18 @@ bool InsaneRebel1::handleMenuCommand(RA1MenuCommand command) {
 		return false;
 
 	if (_levelSelectActive) {
+		const int itemCount = _release.getLevelCount() + 1; // Chapters + BACK
 		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
 		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
+		const int rowsInColumn = MIN(kRA1LevelSelectRowsPerCol, itemCount - col * kRA1LevelSelectRowsPerCol);
 
 		switch (command) {
 		case kRA1MenuCommandUp:
-			row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+			row = (row + rowsInColumn - 1) % rowsInColumn;
 			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
 			return true;
 		case kRA1MenuCommandDown:
-			row = (row + 1) % kRA1LevelSelectRowsPerCol;
+			row = (row + 1) % rowsInColumn;
 			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
 			return true;
 		case kRA1MenuCommandLeft:
@@ -474,11 +475,11 @@ bool InsaneRebel1::handleMenuCommand(RA1MenuCommand command) {
 				_levelSelectSel -= kRA1LevelSelectRowsPerCol;
 			return true;
 		case kRA1MenuCommandRight:
-			if (col < 1)
-				_levelSelectSel += kRA1LevelSelectRowsPerCol;
+			if ((col + 1) * kRA1LevelSelectRowsPerCol < itemCount)
+				_levelSelectSel = MIN(_levelSelectSel + kRA1LevelSelectRowsPerCol, itemCount - 1);
 			return true;
 		case kRA1MenuCommandCancel:
-			_levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
+			_levelSelectSel = itemCount - 1; // Back
 			// fall through
 		case kRA1MenuCommandAccept:
 			_menuConfirmed = true;
@@ -626,7 +627,7 @@ bool InsaneRebel1::handleMenuMouse(const Common::Event &event) {
 
 	if (_levelSelectActive) {
 		selection = &_levelSelectSel;
-		for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+		for (int i = 0; i <= _release.getLevelCount(); i++) {
 			const int col = i / kRA1LevelSelectRowsPerCol;
 			const int row = i % kRA1LevelSelectRowsPerCol;
 			const int frameX = (col == 0) ? kRA1LevelSelectLeftX : kRA1LevelSelectRightX;
@@ -1095,18 +1096,19 @@ void InsaneRebel1::renderLevelSelectOverlay(byte *dst, int pitch, int width, int
 	const int leftFrameX = kRA1LevelSelectLeftX;
 	const int rightFrameX = kRA1LevelSelectRightX;
 	const int columnW = kRA1LevelSelectColW;
+	const int levelCount = _release.getLevelCount();
 	int levelTextW = 0;
 
-	for (int i = 0; i < kRA1NumLevels; i++)
-		levelTextW = MAX(levelTextW, getMenuTalkTextWidth(kLevelItems[i]));
+	for (int i = 0; i < levelCount; i++)
+		levelTextW = MAX(levelTextW, getMenuTalkTextWidth(kLevelItems[_release.levels[i] - 1]));
 
-	for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+	for (int i = 0; i <= levelCount; i++) {
 		const int col = i / kRA1LevelSelectRowsPerCol;
 		const int row = i % kRA1LevelSelectRowsPerCol;
 		const int frameX = (col == 0) ? leftFrameX : rightFrameX;
 		const int y = menuY + row * kRA1MenuRowH;
-		const bool levelItem = i < kRA1NumLevels;
-		const char *text = levelItem ? kLevelItems[i] : kBackItem;
+		const bool levelItem = i < levelCount;
+		const char *text = levelItem ? kLevelItems[_release.levels[i] - 1] : kBackItem;
 		const int textW = levelItem ? levelTextW : getMenuTalkTextWidth(text);
 		const int textX = frameX + (columnW - textW) / 2;
 
@@ -1267,7 +1269,7 @@ int InsaneRebel1::runPasscodeEntryDialog() {
 	for (int i = 1; i <= kRA1NumLevels; i++) {
 		const char *password = getChapterCompletePassword(i);
 		if (password && !scumm_stricmp(_textEntryBuffer, password)) {
-			const int targetLevel = getRebel1PasscodeStartLevel(i);
+			const int targetLevel = _release.resolvePasscodeLevel(getRebel1PasscodeStartLevel(i));
 			if (targetLevel == 0)
 				return 0;
 
@@ -1284,7 +1286,7 @@ int InsaneRebel1::runPasscodeEntryDialog() {
 	for (int i = 1; i <= (int)ARRAYSIZE(kRebel1ThreeDOPasswords); i++) {
 		const char *password = kRebel1ThreeDOPasswords[i - 1];
 		if (!scumm_stricmp(_textEntryBuffer, password)) {
-			const int targetLevel = getRebel1ThreeDOPasscodeStartLevel(i);
+			const int targetLevel = _release.resolvePasscodeLevel(getRebel1ThreeDOPasscodeStartLevel(i));
 			if (targetLevel == 0)
 				return 0;
 
@@ -1384,7 +1386,7 @@ int InsaneRebel1::runLevelSelectMenu() {
 	if (!_unlockAllLevels)
 		return 0;
 
-	_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1NumLevels - 1);
+	_levelSelectSel = MAX(_release.findLevel(_startLevel), 0);
 	_levelSelectActive = true;
 
 	while (!shouldAbortGameFlow()) {
@@ -1394,9 +1396,9 @@ int InsaneRebel1::runLevelSelectMenu() {
 			break;
 
 		if (_menuConfirmed) {
-			if (_levelSelectSel < kRA1NumLevels) {
+			if (_levelSelectSel < _release.getLevelCount()) {
 				_levelSelectActive = false;
-				return _levelSelectSel + 1;  // 1-based level number
+				return _release.levels[_levelSelectSel];
 			}
 			// BACK
 			_levelSelectActive = false;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 3f042b3b229..bddeba8c7db 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -288,7 +288,7 @@ const char *InsaneRebel1::uiStr(int id) const {
 // that fails validation keeps its English fallback.
 void InsaneRebel1::loadLocalizedUiStrings() {
 	Common::File f;
-	if (!f.open("ASSAULT.EXE"))
+	if (!f.open(_release.executable))
 		return;
 
 	int32 size = (int32)f.size();
@@ -457,10 +457,10 @@ void InsaneRebel1::loadLocalizedUiStrings() {
 	for (int i = 0; i < kR1StrUiCount; i++)
 		if (!_uiStrings[i].empty())
 			loaded++;
-	debugC(DEBUG_INSANE, "InsaneRebel1: %d/%d UI strings loaded from ASSAULT.EXE", loaded, kR1StrUiCount);
+	debugC(DEBUG_INSANE, "InsaneRebel1: %d/%d UI strings loaded from %s", loaded, kR1StrUiCount, _release.executable);
 }
 
-InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm), _release(getRebel1Release(scumm->_game.variant)) {
 	Insane::_vm = scumm;
 	// Rebel Assault skips ScummEngine::resetScumm(), which normally clears this state.
 	for (int i = 0; i < kScummActionCount; i++)
@@ -524,7 +524,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_activeInputSource = kInputSourceMouse;
 
 	_currentLevel = 0;
-	_resumeLevel = 1;
+	_resumeLevel = _release.levels[0];
 	_activeSaveSlot = -1;
 	_loadRequested = false;
 	_flyControlMode = 0;
@@ -576,6 +576,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_level7WarningFrames = 0;
 	_level7WarningThreshold = 0;
 	_levelGameplayPhase = 0;
+	_level9PathLoopOffset = -1;
+	_level9SelectedPath = -1;
 	_level14Play2BSplicePending = false;
 	_level14Play2BSpliced = false;
 	_level14Play2BSpliceFrame = 0;
@@ -594,7 +596,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_optionsSel = 0;
 	_levelSelectActive = false;
 	_levelSelectSel = 0;
-	_startLevel = 1;
+	_startLevel = _release.levels[0];
 
 	_optRookieOneFemale = false;
 	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 40bd8709062..d96ae57a85d 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -29,6 +29,7 @@
 #include "scumm/insane/rebel/rebel_audio.h"
 #include "scumm/insane/rebel/rebel_gamepad.h"
 #include "scumm/insane/rebel/rebel_touch.h"
+#include "scumm/insane/rebel1/releases.h"
 #include "scumm/smush/rebel/smush_player_ra1.h"
 
 namespace Scumm {
@@ -333,6 +334,7 @@ public:
 private:
 
 	ScummEngine_v7 *_vm;
+	const Rebel1Release &_release;
 
 	// Sprite and font banks.
 	RA1SpriteBank _shipBank;
@@ -418,6 +420,7 @@ private:
 	void updateOnFootSequence();
 	void updateOnFootAimVariant();
 	void finishOnFootFrame();
+	void updateLevel9PathSelector(int32 curFrame, int32 maxFrame);
 	int16 _onFootCharX;
 	int16 _onFootCharY;
 	int16 _onFootAnimCounter;
@@ -529,7 +532,7 @@ private:
 	static const int16 kMaxHealth = 98;
 	static const int16 kDeathTimerInit = 30;
 	static const int16 kDamageCooldownInit = 10;
-	enum { kNumLevels = 15 };
+	enum { kNumLevels = Rebel1Release::kNumLevels };
 
 	// Streamed SMUSH audio.
 	RebelAudio _audio;
@@ -568,6 +571,8 @@ private:
 	int16 _level7WarningFrames;
 	int16 _level7WarningThreshold;
 	int _levelGameplayPhase;
+	int32 _level9PathLoopOffset;
+	int16 _level9SelectedPath;
 	bool _level14Play2BSplicePending;
 	bool _level14Play2BSpliced;
 	int32 _level14Play2BSpliceFrame;
diff --git a/engines/scumm/insane/rebel1/releases.cpp b/engines/scumm/insane/rebel1/releases.cpp
new file mode 100644
index 00000000000..4bdd970e401
--- /dev/null
+++ b/engines/scumm/insane/rebel1/releases.cpp
@@ -0,0 +1,79 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/textconsole.h"
+#include "scumm/insane/rebel1/releases.h"
+
+namespace Scumm {
+
+// Variants match detection_tables.h. Keep release-specific resources and chapter
+// order here so menus, progression and saves use the same content description.
+static const Rebel1Release kReleases[] = {
+	{
+		"", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1OPEN.ANM", "FIN/FNFINAL.ANM", true,
+		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }
+	},
+	{
+		// CD-ROM Demo v1.51: the original dispatcher skips chapters 3-9 and
+		// 11-15, their transitions, and the retail ending.
+		"Demo v1.51", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1OPEN.ANM", nullptr, false,
+		{ 1, 2, 10 }
+	}
+};
+
+int Rebel1Release::getLevelCount() const {
+	int count = 0;
+	while (count < kNumLevels && levels[count])
+		++count;
+	return count;
+}
+
+int Rebel1Release::findLevel(int level) const {
+	for (int i = 0; i < getLevelCount(); ++i) {
+		if (levels[i] == level)
+			return i;
+	}
+	return -1;
+}
+
+int Rebel1Release::resolvePasscodeLevel(int level) const {
+	// The original sampler advances passcodes past omitted chapters. A code
+	// beyond its last chapter returns to the menu instead of playing the ending.
+	if (level < 1 || level > kNumLevels + 1)
+		return 0;
+	for (; level <= kNumLevels; ++level) {
+		if (findLevel(level) >= 0)
+			return level;
+	}
+	return ending ? kNumLevels + 1 : 0;
+}
+
+const Rebel1Release &getRebel1Release(const char *variant) {
+	if (!variant)
+		variant = "";
+	for (const Rebel1Release &release : kReleases) {
+		if (!strcmp(variant, release.variant))
+			return release;
+	}
+	error("Unknown Rebel Assault release '%s'", variant);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel1/releases.h b/engines/scumm/insane/rebel1/releases.h
new file mode 100644
index 00000000000..33c01b43352
--- /dev/null
+++ b/engines/scumm/insane/rebel1/releases.h
@@ -0,0 +1,50 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_INSANE_REBEL1_RELEASES_H
+#define SCUMM_INSANE_REBEL1_RELEASES_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+struct Rebel1Release {
+	enum { kNumLevels = 15 };
+
+	const char *variant;
+	const char *executable;
+	const char *logo;
+	const char *intro;
+	const char *ending;
+	bool chapterTransitions;
+	// Chapter numbers in playback order. Unused entries are zero.
+	byte levels[kNumLevels];
+
+	int getLevelCount() const;
+	int findLevel(int level) const;
+	int resolvePasscodeLevel(int level) const;
+};
+
+const Rebel1Release &getRebel1Release(const char *variant);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index d5acbc79fc2..675f0fd9678 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -630,10 +630,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		shotOverlayHandled = _gameOp0BOverlayRenderedThisFrame;
 		drawGameOp0BTargetingAfterFetch = _gameOp0BOverlayRenderedThisFrame;
 	} else if (onFootMode) {
-		if (_currentLevel == 8 && onFootAimMode && !onFootSequenceMode && _killCount > 0) {
-			_fireCooldown = _playerFired ? 1 : 0;
-			_vm->_smushVideoShouldFinish = true;
-			return;
+		if (_currentLevel == 8 && onFootAimMode && !onFootSequenceMode) {
+			if (_level9PathLoopOffset >= 0) {
+				updateLevel9PathSelector(curFrame, maxFrame);
+			} else if (_killCount > 0) {
+				_fireCooldown = _playerFired ? 1 : 0;
+				_vm->_smushVideoShouldFinish = true;
+				return;
+			}
 		}
 
 		if (onFootSequenceMode)
@@ -899,6 +903,25 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 	_lastHitTarget = 0;
 }
 
+void InsaneRebel1::updateLevel9PathSelector(int32 curFrame, int32 maxFrame) {
+	if (_level9SelectedPath >= 0)
+		return;
+
+	if (_killCount > 0) {
+		// The DOS L9 loop reveals object 8 for the left tunnel (target 3),
+		// or object 7 for the right tunnel (target 4), then hides both targets.
+		// Remember the target hit before further mouse movement can change it.
+		_level9SelectedPath = isFrameObjectPrimarySet(4) ? 1 : 0;
+		clearFrameObjectPrimaryBits(0, _level9SelectedPath == 0 ? 0x01 : 0x02);
+		_frameObjectState[0] |= 0x30;
+		_gameplayFlags75fe |= 4;
+	} else if (maxFrame >= 9 && curFrame >= maxFrame + 1 - 10) {
+		// The original loops ten frames before the end until a path is chosen.
+		// Keep the stored background and input state when seeking back to frame 1.
+		_player->seekSan(nullptr, _level9PathLoopOffset - 8, 1);
+	}
+}
+
 void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
 	if (_currentLevel != 13 || _levelGameplayPhase != 2 || _level14Play2BSpliced ||
 			_level14Play2BSplicePending || maxFrame < 0x0F)
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index c9a28805994..4adc3bf27ea 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -205,6 +205,9 @@ void InsaneRebel1::resetLevelAttemptState(int16 flyControlMode, int16 gameplayPh
 }
 
 void InsaneRebel1::playLevelTransitionCutscene(int level) {
+	if (!_release.chapterTransitions)
+		return;
+
 	switch (level) {
 	case 4:
 		// FALCON/BIGGS/WEDGE passcode group.
@@ -250,11 +253,13 @@ void InsaneRebel1::clearVideoBuffer() {
 }
 
 void InsaneRebel1::playIntroSequence() {
-	playCinematic("OPEN/O1LOGO.ANM");
+	if (_release.logo)
+		playCinematic(_release.logo);
 	if (shouldAbortGameFlow())
 		return;
 
-	playCinematic("OPEN/O1OPEN.ANM");
+	if (_release.intro)
+		playCinematic(_release.intro);
 }
 
 bool InsaneRebel1::runLevel1() {
@@ -812,7 +817,13 @@ bool InsaneRebel1::runLevel9() {
 	const int randPath1 = getOriginalRouteBit();
 	const int randPath2 = getOriginalRouteBit();
 	const int randPath3 = getOriginalRouteBit();
-	auto playLevel9PathSelector = [&](const char *filename) {
+	auto playLevel9PathSelector = [&](const char *filename, bool hasExitAnimation) {
+		// L9PLAY6 stores both exit animations after its idle loop. Frame 1
+		// restores the STOR background without repeating frame 0's GAME reset.
+		const int32 loopOffset = hasExitAnimation ? findAnimFrameChunkOffset(_vm, filename, 1) : -1;
+		if (hasExitAnimation && loopOffset < 0)
+			error("Unable to find the path selector loop in %s", filename);
+
 		while (!shouldAbortGameFlow()) {
 			_onFootCharX = 0;
 			_onFootCharY = 0;
@@ -822,10 +833,15 @@ bool InsaneRebel1::runLevel9() {
 			_posAccumY = 0;
 			_killCount = 0;
 			_lastHitTarget = 0;
+			_level9SelectedPath = -1;
+			_level9PathLoopOffset = loopOffset;
 
 			playInteractiveVideo(filename);
+			_level9PathLoopOffset = -1;
 			if (shouldAbortGameFlow() || _health < 0)
 				return -1;
+			if (_level9SelectedPath >= 0)
+				return (int)_level9SelectedPath;
 			if (_interactiveVideoCheatSkipped)
 				return (_shipPosX < kRA1CenterX) ? 0 : 1;
 			if (_killCount > 0)
@@ -867,7 +883,7 @@ bool InsaneRebel1::runLevel9() {
 				return false;
 
 			loadTuningForLevel(0x0C);
-			const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM");
+			const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM", false);
 			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
@@ -911,7 +927,7 @@ bool InsaneRebel1::runLevel9() {
 				return false;
 
 			loadTuningForLevel(0x0C);
-			const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM");
+			const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM", false);
 			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
@@ -941,7 +957,9 @@ bool InsaneRebel1::runLevel9() {
 					return false;
 
 				loadTuningForLevel(0x0C);
-				const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM");
+				// Sega CD keeps the exit animations in separate L9PLAY6A/B clips.
+				const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM",
+					_vm->_game.platform != Common::kPlatformSegaCD);
 				if (shouldAbortGameFlow())
 					return false;
 				if (_health < 0)
@@ -1359,8 +1377,11 @@ void InsaneRebel1::runGame() {
 		&InsaneRebel1::runLevel15
 	};
 	const int numLevels = ARRAYSIZE(kLevelRunners);
+	const int availableLevels = _release.getLevelCount();
 	auto runLevelsFrom = [&](int startLevel, bool resetRunState) {
-		int firstLevel = CLIP<int>(startLevel, 1, numLevels);
+		int firstLevelIndex = _release.findLevel(startLevel);
+		if (firstLevelIndex < 0)
+			return;
 
 		while (!_vm->shouldQuit()) {
 			_loadRequested = false;
@@ -1375,9 +1396,10 @@ void InsaneRebel1::runGame() {
 			}
 			resetRunState = false;
 
-			for (int level = firstLevel;
-				 level <= numLevels && completed && !shouldAbortGameFlow();
-				 ++level) {
+			for (int levelIndex = firstLevelIndex;
+				 levelIndex < availableLevels && completed && !shouldAbortGameFlow();
+				 ++levelIndex) {
+				const int level = _release.levels[levelIndex];
 				_resumeLevel = level;
 				playLevelTransitionCutscene(level);
 				if (shouldAbortGameFlow())
@@ -1390,21 +1412,24 @@ void InsaneRebel1::runGame() {
 				completed = (this->*kLevelRunners[level - 1])();
 				if (completed) {
 					lastCompletedLevel = level;
-					if (level < numLevels) {
-						_startLevel = level + 1;
+					if (levelIndex + 1 < availableLevels) {
+						_startLevel = _release.levels[levelIndex + 1];
 						_resumeLevel = _startLevel;
 						autosaveProgress();
+					} else if (!_release.ending) {
+						_startLevel = _release.levels[0];
 					}
 				}
 			}
 
 			if (_loadRequested) {
-				firstLevel = getCurrentSaveLevel();
+				firstLevelIndex = _release.findLevel(getCurrentSaveLevel());
 				continue;
 			}
 
-			if (!shouldAbortGameFlow() && completed && lastCompletedLevel == numLevels)
-				playCinematic("FIN/FNFINAL.ANM");
+			if (!shouldAbortGameFlow() && completed &&
+					lastCompletedLevel == _release.levels[availableLevels - 1] && _release.ending)
+				playCinematic(_release.ending);
 			if (!shouldAbortGameFlow())
 				runHighScoreNameEntry();
 			_currentLevel = 0;
@@ -1448,12 +1473,12 @@ void InsaneRebel1::runGame() {
 			const int passcodeLevel = runPasscodeEntryDialog();
 			if (passcodeLevel >= 1 && passcodeLevel <= numLevels)
 				runLevelsFrom(passcodeLevel, true);
-			else if (passcodeLevel == numLevels + 1) {
+			else if (passcodeLevel == numLevels + 1 && _release.ending) {
 				_health = kMaxHealth;
 				_lives = 3;
 				_score = 0;
 				_prevScore = 0;
-				playCinematic("FIN/FNFINAL.ANM");
+				playCinematic(_release.ending);
 				if (!shouldAbortGameFlow())
 					runHighScoreNameEntry();
 				_currentLevel = 0;
@@ -1468,8 +1493,8 @@ void InsaneRebel1::runGame() {
 		}
 		case 5:
 			showHighScores();
-			if (!shouldAbortGameFlow())
-				playCinematic("OPEN/O1OPEN.ANM");
+			if (!shouldAbortGameFlow() && _release.intro)
+				playCinematic(_release.intro);
 			break;
 		case 6:
 			return;
@@ -1514,6 +1539,8 @@ void InsaneRebel1::setupInteractiveVideoState(int32 startFrame) {
 		resetFrameObjectState();
 		resetGamepadReticleAim();
 	}
+	if (_level9PathLoopOffset >= 0)
+		_frameObjectState[0] |= 0x03; // Hide L9PLAY6's mutually exclusive exit objects 7 and 8.
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setPreserveVideoStateOnNextPlay(preserveVideoState);
 	splayer->setCurVideoFlags(0x28);
diff --git a/engines/scumm/insane/rebel1/saveload.cpp b/engines/scumm/insane/rebel1/saveload.cpp
index 61d7ab8bef5..4bc3cac8c42 100644
--- a/engines/scumm/insane/rebel1/saveload.cpp
+++ b/engines/scumm/insane/rebel1/saveload.cpp
@@ -159,7 +159,11 @@ bool InsaneRebel1::readSaveState(int slot, SaveState &state, Common::String *des
 		return false;
 	}
 
-	state.resumeLevel = CLIP<int>(in->readSint16LE(), 1, kNumLevels);
+	state.resumeLevel = in->readSint16LE();
+	if (_release.findLevel(state.resumeLevel) < 0) {
+		warning("RA1: chapter %d is not available in this release", state.resumeLevel);
+		return false;
+	}
 	state.lives = MAX<int>(in->readSint16LE(), 0);
 	state.score = in->readSint32LE();
 	state.prevScore = in->readSint32LE();
@@ -236,7 +240,7 @@ void InsaneRebel1::autosaveProgress() {
 	SaveState oldState;
 	Common::String oldDesc;
 	if (readSaveState(slot, oldState, &oldDesc)) {
-		if (oldState.resumeLevel > state.resumeLevel ||
+		if (_release.findLevel(oldState.resumeLevel) > _release.findLevel(state.resumeLevel) ||
 				(oldState.resumeLevel == state.resumeLevel && oldState.lives >= state.lives)) {
 			debugC(DEBUG_INSANE, "skipping autosave slot=%d level=%d lives=%d; existing level=%d lives=%d",
 				slot, state.resumeLevel, state.lives, oldState.resumeLevel, oldState.lives);
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 46c1673a742..5436864add9 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -151,6 +151,7 @@ MODULE_OBJS += \
 	insane/rebel/rebel_gamepad.o \
 	insane/rebel/rebel_touch.o \
 	insane/rebel1/rebel.o \
+	insane/rebel1/releases.o \
 	insane/rebel1/audio.o \
 	insane/rebel1/iact.o \
 	insane/rebel1/levels.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 7b499df44fe..f275d63b600 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Thu Jul 23 05:28:13 2026
+  This file was generated by the md5table tool on Sun Sep 27 07:03:59 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -150,6 +150,7 @@ static const MD5Table md5table[] = {
 	{ "25a7ce1cda4c7181c33c1c87a7cf6c7a", "monkey", "VGA", "VGA", 8357, Common::FR_FRA, Common::kPlatformDOS },
 	{ "261cd0da05b12917458d1c816a9d2ec9", "monkey", "CD", "CD", 8955, Common::PL_POL, Common::kPlatformDOS },
 	{ "2723fea3dae0cb47768c424b145ae0e7", "tentacle", "Floppy", "Floppy", 7932, Common::EN_ANY, Common::kPlatformDOS },
+	{ "274f6026d41fa2a2990a6d6b7a561dc2", "rebel1", "Demo v1.51", "Demo v1.51", 217559, Common::EN_ANY, Common::kPlatformDOS },
 	{ "275786f0f6894d1fe1ab5f1d103bb814", "indy3", "VGA", "VGA", 6295, Common::RU_RUS, Common::kPlatformDOS },
 	{ "27b2ef1653089fe5b897d9cc89ce784f", "balloon", "HE 80", "", -1, Common::RU_RUS, Common::kPlatformWindows },
 	{ "27b3a4224ad63d5b04627595c1c1a025", "zak", "V2", "V2", -1, Common::IT_ITA, Common::kPlatformAmiga },


Commit: 3113ae98e7cd273828f001c0167f80d4ead16dad
    https://github.com/scummvm/scummvm/commit/3113ae98e7cd273828f001c0167f80d4ead16dad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA1: added support for demo 1994-04-13

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection.h
    engines/scumm/detection_internal.h
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/releases.cpp
    engines/scumm/insane/rebel1/releases.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/metaengine.cpp
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index ce415ea562c..159d286c16c 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -451,6 +451,7 @@ ft	Full Throttle
 
 rebel1	Star Wars: Rebel Assault
 	274f6026d41fa2a2990a6d6b7a561dc2	217559	en	DOS	Demo v1.51	Demo v1.51	CD-ROM demo	-
+	551a7c06bf5098b32d5fe7641d695df8	226750	en	DOS	Demo 1994-04-13	Demo 1994-04-13	CD-ROM demo (chapters 8 and 10)	-
 	b9ac90e4411da1b6f7dd075cc4b03c2a	214435	en	DOS	-	v1.0	-	-
 	e210a57b756473be705d24b9378a8233	212383	en	DOS	-	v1.3	-	-
 	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 8f5d11f0dc6..87c12d3b401 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -44,6 +44,7 @@ namespace Scumm {
 #define GAMEOPTION_REBEL2_NO_DAMAGE                          GUIO_GAMEOPTIONS13
 #define GAMEOPTION_REBEL2_YODA_MODE                          GUIO_GAMEOPTIONS14
 #define GAMEOPTION_REBEL1_NO_DAMAGE                          GUIO_GAMEOPTIONS15
+#define GAMEOPTION_REBEL1_RESTORED_CONTENT                   GUIO_GAMEOPTIONS16
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index e6b00513396..ed243123194 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -647,6 +647,11 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 		if (gfp->genMethod == kGenRoomNumSteam || gfp->genMethod == kGenDiskNumSteam)
 			continue;
 
+		// REBEL.EXE is also the launcher for retail Rebel Assault and other
+		// demos. Require a known executable before using a demo's embedded tables.
+		if (!scumm_stricmp(gfp->gameid, "rebel1") && file.equalsIgnoreCase("REBEL.EXE"))
+			continue;
+
 		//  ____            _     ____
 		// |  _ \ __ _ _ __| |_  |___ \ *
 		// | |_) / _` | '__| __|   __) |
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 5bc9393be02..5d5815d2214 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -232,6 +232,7 @@ static const GameSettings gameVariantsTable[] = {
 
 	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 	{"rebel1", "Demo v1.51", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
+	{"rebel1", "Demo 1994-04-13", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO4(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE, GAMEOPTION_REBEL1_RESTORED_CONTENT)},
 
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
@@ -507,6 +508,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 
 	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo v1.51" },
+	{ "rebel1", "REBEL.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo 1994-04-13" },
 	{ "rebel1", "Rebel Assault", kGenUnchanged, UNK_LANG, Common::kPlatformMacintosh, "" },
 	{ "rebel1", "O1LOGO.AAA", kGenUnchanged, UNK_LANG, Common::kPlatform3DO, "" },
 	{ "rebel1", "SUB_AP.BIN", kGenUnchanged, UNK_LANG, Common::kPlatformSegaCD, "" },
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 683657b0946..df21534b7b7 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -461,9 +461,10 @@ inline bool isLevel14Phase2DamageLatch(uint16 code) {
 	}
 }
 
-inline bool hasLevel8WalkerHazardRoute0(uint16 frame, int16 viewX, int16 viewY) {
+inline bool hasLevel8WalkerHazardRoute0(uint16 frame, int16 viewX, int16 viewY, bool earlyShot) {
 	switch (frame) {
 	case 0x00CD:
+		return earlyShot && viewX <= 0x28;
 	case 0x00EF:
 		return viewX <= 0x28;
 	case 0x0294:
@@ -512,10 +513,10 @@ inline bool hasLevel8WalkerHazardRoute2(uint16 frame, int16 viewX, int16 viewY)
 	}
 }
 
-inline bool hasLevel8WalkerPlayerHit(int route, uint16 frame, int16 viewX, int16 viewY) {
+inline bool hasLevel8WalkerPlayerHit(int route, uint16 frame, int16 viewX, int16 viewY, bool earlyShot) {
 	switch (CLIP<int>(route, 0, 2)) {
 	case 0:
-		return hasLevel8WalkerHazardRoute0(frame, viewX, viewY);
+		return hasLevel8WalkerHazardRoute0(frame, viewX, viewY, earlyShot);
 	case 1:
 		return hasLevel8WalkerHazardRoute1(frame, viewX, viewY);
 	case 2:
@@ -1517,7 +1518,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	if (_currentLevel == 7) {
 		const uint16 walkerFrame = (uint16)_currentSmushFrame;
 		level8WalkerPlayerHit = hasLevel8WalkerPlayerHit(_levelRouteIndex, walkerFrame,
-			_perspectiveX, _perspectiveY);
+			_perspectiveX, _perspectiveY, _release.walker->earlyShot);
 		// Player collision and boss damage are tracked separately.
 		if (level8WalkerPlayerHit)
 			_damageFlags |= 0x20;
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 0c40047c1ff..157d4d37c48 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -297,15 +297,19 @@ static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int
 }
 
 int InsaneRebel1::getMainMenuItemCount() const {
-	return _unlockAllLevels ? kRA1MainMenuItemCount : kRA1MainMenuItemCount - 1;
+	return kRA1MainMenuItemCount - (_unlockAllLevels ? 0 : 1) - (_release.passcodes ? 0 : 1);
 }
 
 int InsaneRebel1::getMainMenuResultForSelection(int selection) const {
 	if (selection < 0 || selection >= getMainMenuItemCount())
 		return kRA1MainMenuItemCount;
-	if (_unlockAllLevels || selection < 3)
-		return selection + 1;
-	return selection + 2;
+	for (int result = 1; result <= kRA1MainMenuItemCount; ++result) {
+		if ((result == 3 && !_release.passcodes) || (result == 4 && !_unlockAllLevels))
+			continue;
+		if (selection-- == 0)
+			return result;
+	}
+	return kRA1MainMenuItemCount;
 }
 
 void InsaneRebel1::setVirtualKeyboardVisible(bool visible) {
@@ -1129,14 +1133,6 @@ void InsaneRebel1::renderMainMenuItems(byte *dst, int pitch, int width, int heig
 		uiStr(kR1StrMenuContinueDemo),
 		uiStr(kR1StrMenuExitToDos)
 	};
-	const char *kMenuItemsLocked[kRA1MainMenuItemCount - 1] = {
-		uiStr(kR1StrMenuNewGame),
-		uiStr(kR1StrMenuGameOptions),
-		uiStr(kR1StrMenuEnterPasscode),
-		uiStr(kR1StrMenuContinueDemo),
-		uiStr(kR1StrMenuExitToDos)
-	};
-	const char *const *menuItems = _unlockAllLevels ? kMenuItems : kMenuItemsLocked;
 	const int mainMenuItemCount = getMainMenuItemCount();
 
 	const char *menuTitle = uiStr(kR1StrMainMenuTitle);
@@ -1145,11 +1141,12 @@ void InsaneRebel1::renderMainMenuItems(byte *dst, int pitch, int width, int heig
 	drawMenuTitleText(dst, pitch, width, height, titleX, 30, menuTitle);
 
 	for (int i = 0; i < mainMenuItemCount; i++) {
-		const int textW = getMenuTalkTextWidth(menuItems[i]);
+		const char *text = kMenuItems[getMainMenuResultForSelection(i) - 1];
+		const int textW = getMenuTalkTextWidth(text);
 		const int textX = getRebel1MenuCenteredX(textW);
 		const int y = 0x3c + i * kRA1MenuRowH;
 
-		drawMenuTalkText(dst, pitch, width, height, textX, y, menuItems[i]);
+		drawMenuTalkText(dst, pitch, width, height, textX, y, text);
 
 		if (i == _menuSelection)
 			drawRebel1MenuFrame(dst, pitch, width, height,
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index bddeba8c7db..e90075a2695 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -41,7 +41,7 @@ namespace Scumm {
 // Per-difficulty tuning tables from assault_data_3.bin (also loadable from C:\rebltune.txt)
 // 21 sub-levels x 3 difficulties x 13 fields
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
-const int16 kTuningTable[21][3][13] = {
+const Rebel1TuningTable kTuningTable = {
 	// Sub-level 0: "1A" (Flight Training part 1)
 	{
 		{ 100, 100,  60, 110,   0,   0,  15,   0,   0,   5,  500,  100, 2048 },  // Easy
@@ -169,25 +169,38 @@ const int16 kTuningTable[21][3][13] = {
 		{   0,   0,   0,   0,   2,  22,  35,   4,  75,  10, 1500,  500, 2050 },  // Hard
 	},
 };
-const int kNumTunedLevels = 21;
+const int kNumTunedLevels = ARRAYSIZE(kTuningTable);
+
+void InsaneRebel1::loadTuningData() {
+	memcpy(_tuningTable, kTuningTable, sizeof(_tuningTable));
+	if (!_release.tuning.offset)
+		return;
+
+	Common::File f;
+	if (!f.open(_release.executable))
+		error("Unable to open %s for Rebel Assault tuning data", _release.executable);
+
+	if (!_release.readTuningData(f, _tuningTable))
+		error("Unable to read Rebel Assault tuning data from %s", _release.executable);
+}
 
 
 void InsaneRebel1::loadTuningForLevel(int level) {
 	int d = CLIP(_difficulty, 0, 2);
 	int l = CLIP(level, 0, kNumTunedLevels - 1);
-	_tuning.roll     = kTuningTable[l][d][0];
-	_tuning.lift     = kTuningTable[l][d][1];
-	_tuning.slide    = kTuningTable[l][d][2];
-	_tuning.drift    = kTuningTable[l][d][3];
-	_tuning.snap     = kTuningTable[l][d][4];
-	_tuning.miss     = kTuningTable[l][d][5];
-	_tuning.wham     = kTuningTable[l][d][6];
-	_tuning.shot     = kTuningTable[l][d][7];
-	_tuning.kill     = kTuningTable[l][d][8];
-	_tuning.time     = kTuningTable[l][d][9];
-	_tuning.levelPts = kTuningTable[l][d][10];
-	_tuning.bonus    = kTuningTable[l][d][11];
-	_tuning.flags    = kTuningTable[l][d][12];
+	_tuning.roll     = _tuningTable[l][d][0];
+	_tuning.lift     = _tuningTable[l][d][1];
+	_tuning.slide    = _tuningTable[l][d][2];
+	_tuning.drift    = _tuningTable[l][d][3];
+	_tuning.snap     = _tuningTable[l][d][4];
+	_tuning.miss     = _tuningTable[l][d][5];
+	_tuning.wham     = _tuningTable[l][d][6];
+	_tuning.shot     = _tuningTable[l][d][7];
+	_tuning.kill     = _tuningTable[l][d][8];
+	_tuning.time     = _tuningTable[l][d][9];
+	_tuning.levelPts = _tuningTable[l][d][10];
+	_tuning.bonus    = _tuningTable[l][d][11];
+	_tuning.flags    = _tuningTable[l][d][12];
 	resetGameplayFlagsFromTuning();
 	_protectedTargetA = 0;
 	_protectedTargetB = 0;
@@ -282,7 +295,7 @@ const char *InsaneRebel1::uiStr(int id) const {
 	return kRebel1UiFallback[id];
 }
 
-// All localized DOS releases keep their UI text in ASSAULT.EXE as NUL-terminated
+// DOS releases keep their UI text in the executable as NUL-terminated
 // strings interleaved with the (language-neutral) data file names, in the same
 // local order across builds. Extract them anchored on those neighbors; anything
 // that fails validation keeps its English fallback.
@@ -348,12 +361,18 @@ void InsaneRebel1::loadLocalizedUiStrings() {
 		return true;
 	};
 
-	// Main menu: title and the five original items follow OPEN\O1OPTION.ANM.
+	// The early demo has only START GAME, GAME OPTIONS and CONTINUE DEMO.
 	int idx = find("OPEN\\O1OPTION.ANM", 0);
 	if (idx >= 0) {
 		assign(kR1StrMainMenuTitle, idx + 1);
-		for (int i = 0; i < 5; i++)
-			assign(kR1StrMenuNewGame + i, idx + 2 + i);
+		assign(kR1StrMenuNewGame, idx + 2);
+		assign(kR1StrMenuGameOptions, idx + 3);
+		if (_release.passcodes) {
+			for (int i = 2; i < 5; i++)
+				assign(kR1StrMenuNewGame + i, idx + 2 + i);
+		} else {
+			assign(kR1StrMenuContinueDemo, idx + 4);
+		}
 	}
 
 	// High score table title follows OPEN\O1SCORE.ANM.
@@ -406,6 +425,14 @@ void InsaneRebel1::loadLocalizedUiStrings() {
 		// calibration block.
 		if (!assign(kR1StrNewHighScore, pw - 4) && paren >= 0)
 			assign(kR1StrNewHighScore, paren - 7);
+	} else if (!_release.passcodes) {
+		// The early demo places these three strings after the options movie.
+		idx = find("OPEN\\O1HWARE.ANM", 0);
+		if (idx >= 0) {
+			assign(kR1StrCompletionBonusFmt, idx + 1);
+			assign(kR1StrBonusFmt, idx + 2);
+			assign(kR1StrChapterComplete, idx + 3);
+		}
 	}
 
 	// Level result strings, each anchored on the video played before them.
@@ -460,7 +487,8 @@ void InsaneRebel1::loadLocalizedUiStrings() {
 	debugC(DEBUG_INSANE, "InsaneRebel1: %d/%d UI strings loaded from %s", loaded, kR1StrUiCount, _release.executable);
 }
 
-InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm), _release(getRebel1Release(scumm->_game.variant)) {
+InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm),
+		_release(getRebel1Release(scumm->_game.variant, ConfMan.getBool("rebel1_restored_content"))) {
 	Insane::_vm = scumm;
 	// Rebel Assault skips ScummEngine::resetScumm(), which normally clears this state.
 	for (int i = 0; i < kScummActionCount; i++)
@@ -492,6 +520,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm), _relea
 	_driftParam = 0;
 
 	_difficulty = 0;
+	loadTuningData();
 	loadTuningForLevel(0);
 
 	_perspectiveX = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d96ae57a85d..9de3046f833 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -334,7 +334,7 @@ public:
 private:
 
 	ScummEngine_v7 *_vm;
-	const Rebel1Release &_release;
+	const Rebel1Release _release;
 
 	// Sprite and font banks.
 	RA1SpriteBank _shipBank;
@@ -502,7 +502,9 @@ private:
 		int16 flags;
 	};
 	TuningParams _tuning;
+	Rebel1TuningTable _tuningTable;
 
+	void loadTuningData();
 	void loadTuningForLevel(int level);
 	void resetGameplayFlagsFromTuning();
 
@@ -722,11 +724,6 @@ private:
 	int16 _walkerBranchChoice;
 	bool _walkerRoundReplay;
 
-	// Attack window frame numbers per route.
-	static const int16 kWalkerAttackWindow1[3];
-	static const int16 kWalkerAttackWindow2[3];
-	static const int16 kWalkerAttackWindow3[3];
-
 	static const int kFrameObjectStateBytes = 300;
 	byte _frameObjectState[kFrameObjectStateBytes];
 };
diff --git a/engines/scumm/insane/rebel1/releases.cpp b/engines/scumm/insane/rebel1/releases.cpp
index 4bdd970e401..aace005d32f 100644
--- a/engines/scumm/insane/rebel1/releases.cpp
+++ b/engines/scumm/insane/rebel1/releases.cpp
@@ -19,23 +19,46 @@
  *
  */
 
+#include "common/stream.h"
 #include "common/textconsole.h"
 #include "scumm/insane/rebel1/releases.h"
 
 namespace Scumm {
 
+static const Rebel1WalkerData kRetailWalker = {
+	{ { 2588, 2323, 877 }, { 1709, 1444, -2 }, { 262, -2, -2 } },
+	2, true
+};
+
+static const Rebel1WalkerData kDemo940413Walker = {
+	{ { 2591, 2324, 878 }, { 1712, 1445, -1 }, { 265, -1, -1 } },
+	1, false
+};
+
+static const byte kDemo940413RestoredLevels[Rebel1Release::kNumLevels] = { 3, 6, 8, 10 };
+
 // Variants match detection_tables.h. Keep release-specific resources and chapter
 // order here so menus, progression and saves use the same content description.
 static const Rebel1Release kReleases[] = {
 	{
-		"", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1OPEN.ANM", "FIN/FNFINAL.ANM", true,
-		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }
+		"", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", nullptr, "OPEN/O1OPEN.ANM", "FIN/FNFINAL.ANM", true, true,
+		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, nullptr,
+		0x7fff, false, { 0, 0, 0 }, &kRetailWalker
 	},
 	{
 		// CD-ROM Demo v1.51: the original dispatcher skips chapters 3-9 and
 		// 11-15, their transitions, and the retail ending.
-		"Demo v1.51", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1OPEN.ANM", nullptr, false,
-		{ 1, 2, 10 }
+		"Demo v1.51", "ASSAULT.EXE", "OPEN/O1LOGO.ANM", nullptr, "OPEN/O1OPEN.ANM", nullptr, false, true,
+		{ 1, 2, 10 }, nullptr,
+		0x7fff, false, { 0, 0, 0 }, &kRetailWalker
+	},
+	{
+		// The dispatcher at 0x3466 starts at chapter 8, skips 9, and returns
+		// to the menu after 10. Chapters 3 and 6 have complete resources and
+		// handlers, but are only reachable through the original debug keys.
+		"Demo 1994-04-13", "REBEL.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1DEMO.ANM", "OPEN/O1OPEN.ANM", nullptr, false, false,
+		{ 8, 10 }, kDemo940413RestoredLevels,
+		1 << (6 - 1), true, { 0x289dc, 0x1a, 0x222 }, &kDemo940413Walker
 	}
 };
 
@@ -57,7 +80,7 @@ int Rebel1Release::findLevel(int level) const {
 int Rebel1Release::resolvePasscodeLevel(int level) const {
 	// The original sampler advances passcodes past omitted chapters. A code
 	// beyond its last chapter returns to the menu instead of playing the ending.
-	if (level < 1 || level > kNumLevels + 1)
+	if (!passcodes || level < 1 || level > kNumLevels + 1)
 		return 0;
 	for (; level <= kNumLevels; ++level) {
 		if (findLevel(level) >= 0)
@@ -66,12 +89,36 @@ int Rebel1Release::resolvePasscodeLevel(int level) const {
 	return ending ? kNumLevels + 1 : 0;
 }
 
-const Rebel1Release &getRebel1Release(const char *variant) {
+bool Rebel1Release::readTuningData(Common::SeekableReadStream &in, Rebel1TuningTable &table) const {
+	const int64 requiredSize = (int64)tuning.offset +
+		(ARRAYSIZE(table) - 1) * tuning.levelStride +
+		(ARRAYSIZE(table[0]) - 1) * tuning.difficultyStride + sizeof(table[0][0]);
+	if (!tuning.offset || in.size() < requiredSize)
+		return false;
+
+	for (uint level = 0; level < ARRAYSIZE(table); ++level) {
+		for (uint difficulty = 0; difficulty < ARRAYSIZE(table[level]); ++difficulty) {
+			const int64 offset = (int64)tuning.offset +
+				level * tuning.levelStride + difficulty * tuning.difficultyStride;
+			if (!in.seek(offset))
+				return false;
+			for (uint field = 0; field < ARRAYSIZE(table[level][difficulty]); ++field)
+				table[level][difficulty][field] = in.readSint16LE();
+		}
+	}
+	return !in.err() && !in.eos();
+}
+
+Rebel1Release getRebel1Release(const char *variant, bool restoredContent) {
 	if (!variant)
 		variant = "";
 	for (const Rebel1Release &release : kReleases) {
-		if (!strcmp(variant, release.variant))
-			return release;
+		if (!strcmp(variant, release.variant)) {
+			Rebel1Release result = release;
+			if (restoredContent && release.restoredLevels)
+				memcpy(result.levels, release.restoredLevels, sizeof(result.levels));
+			return result;
+		}
 	}
 	error("Unknown Rebel Assault release '%s'", variant);
 }
diff --git a/engines/scumm/insane/rebel1/releases.h b/engines/scumm/insane/rebel1/releases.h
index 33c01b43352..05487a303f2 100644
--- a/engines/scumm/insane/rebel1/releases.h
+++ b/engines/scumm/insane/rebel1/releases.h
@@ -24,26 +24,57 @@
 
 #include "common/scummsys.h"
 
+namespace Common {
+class SeekableReadStream;
+}
+
 namespace Scumm {
 
+typedef int16 Rebel1TuningTable[21][3][13];
+
+struct Rebel1TuningLayout {
+	uint32 offset; // File offset in the executable; zero uses the retail defaults.
+	uint16 levelStride;
+	uint16 difficultyStride;
+};
+
+struct Rebel1WalkerData {
+	// Window ends indexed by [window][route], in ANM-local frames. Negative entries are disabled.
+	int16 attackWindows[3][3];
+	// Destination frame after the extra source frame at the splice.
+	int16 routeStartFrame;
+	bool earlyShot;
+};
+
 struct Rebel1Release {
 	enum { kNumLevels = 15 };
 
 	const char *variant;
 	const char *executable;
 	const char *logo;
+	const char *introNotice;
 	const char *intro;
 	const char *ending;
 	bool chapterTransitions;
+	bool passcodes;
 	// Chapter numbers in playback order. Unused entries are zero.
 	byte levels[kNumLevels];
+	// Optional replacement chapter order when restored content is enabled.
+	const byte *restoredLevels;
+	// Bit N enables the target/path bonus for chapter N + 1.
+	uint16 chapterBonusMask;
+	// Early chapter 6 awards one or three tuning bonuses at 20/30 kills.
+	bool tieredAsteroidChaseBonus;
+	Rebel1TuningLayout tuning;
+	const Rebel1WalkerData *walker;
 
 	int getLevelCount() const;
 	int findLevel(int level) const;
 	int resolvePasscodeLevel(int level) const;
+	bool readTuningData(Common::SeekableReadStream &in, Rebel1TuningTable &table) const;
 };
 
-const Rebel1Release &getRebel1Release(const char *variant);
+Rebel1Release getRebel1Release(const char *variant, bool restoredContent = false);
 
 } // End of namespace Scumm
 
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 675f0fd9678..59104dd4653 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1443,7 +1443,7 @@ static const char *const kChapterCompletePasswords[] = {
 };
 
 const char *InsaneRebel1::getChapterCompletePassword(int passwordIndex) const {
-	if (passwordIndex < 1 || passwordIndex > (int)ARRAYSIZE(kChapterCompletePasswords))
+	if (!_release.passcodes || passwordIndex < 1 || passwordIndex > (int)ARRAYSIZE(kChapterCompletePasswords))
 		return nullptr;
 
 	return kChapterCompletePasswords[passwordIndex - 1];
@@ -1802,11 +1802,6 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	_hudDirtyFlag = 0xFF;
 }
 
-// Each route has up to 3 attack windows, indexed by ANM-local frame. -2 means disabled.
-const int16 InsaneRebel1::kWalkerAttackWindow1[3] = { 2588, 2323, 877 };
-const int16 InsaneRebel1::kWalkerAttackWindow2[3] = { 1709, 1444, -2 };
-const int16 InsaneRebel1::kWalkerAttackWindow3[3] = { 262, -2, -2 };
-
 void InsaneRebel1::updateLevel8WalkerState() {
 	if (_walkerHealth >= 11) {
 		_walkerHealth = (int16)(100 - (_killCount + (_killCount >> 2)));
@@ -1822,16 +1817,12 @@ void InsaneRebel1::updateLevel8WalkerState() {
 	int route = CLIP(_levelRouteIndex, 0, 2);
 	uint16 fc = (uint16)_currentSmushFrame;
 
-	const int16 *windows[3] = {
-		&kWalkerAttackWindow1[route],
-		&kWalkerAttackWindow2[route],
-		&kWalkerAttackWindow3[route]
-	};
+	const int16 (*windows)[3] = _release.walker->attackWindows;
 	int16 frameNum = (int16)fc;
 
 	bool inWindow = false;
 	for (int w = 0; w < 3; w++) {
-		int16 windowEnd = *windows[w];
+		int16 windowEnd = windows[w][route];
 		if (windowEnd < 0) continue;
 		if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
 			inWindow = true;
@@ -1847,7 +1838,7 @@ void InsaneRebel1::updateLevel8WalkerState() {
 
 		bool inDirectionalPhase = false;
 		for (int w = 0; w < 3; w++) {
-			int16 windowEnd = *windows[w];
+			int16 windowEnd = windows[w][route];
 			if (windowEnd < 0) continue;
 			if (frameNum > windowEnd - 0x32 && frameNum <= windowEnd) {
 				inDirectionalPhase = true;
@@ -1866,7 +1857,7 @@ void InsaneRebel1::updateLevel8WalkerState() {
 	}
 
 	for (int w = 0; w < 3; w++) {
-		int16 windowEnd = *windows[w];
+		int16 windowEnd = windows[w][route];
 		if (windowEnd < 0) continue;
 		if (fc != (uint16)windowEnd) continue;
 
@@ -1886,9 +1877,10 @@ void InsaneRebel1::updateLevel8WalkerState() {
 			_pendingRouteIndex = newRoute;
 			// The original makes this choice in the post-frame callback, after
 			// that frame's splice check. It renders one more source frame before
-			// switching, advancing the destination's frame-1 start by that frame.
+			// switching. The early demo requests destination frame 0, retail 1;
+			// both starts advance by the extra source frame.
 			_pendingRouteCutoverFrame = _currentSmushFrame + 1;
-			_pendingRouteStartFrame = 2;
+			_pendingRouteStartFrame = _release.walker->routeStartFrame;
 			debugC(DEBUG_INSANE, "L8 branch: route=%d -> %d at localFrame=%u shipX=%d resumeLocalFrame=%d cutoverFrame=%d",
 				route, newRoute, (unsigned)fc, _shipPosX,
 				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
@@ -1916,17 +1908,13 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 	}
 
 	int route = CLIP(_levelRouteIndex, 0, 2);
-	const int16 *windows[3] = {
-		&kWalkerAttackWindow1[route],
-		&kWalkerAttackWindow2[route],
-		&kWalkerAttackWindow3[route]
-	};
+	const int16 (*windows)[3] = _release.walker->attackWindows;
 	int16 frameNum = (int16)(uint16)_currentSmushFrame;
 
 	bool inWindow = false;
 	bool inDirectionalPhase = false;
 	for (int w = 0; w < 3; w++) {
-		int16 windowEnd = *windows[w];
+		int16 windowEnd = windows[w][route];
 		if (windowEnd < 0) continue;
 		if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
 			inWindow = true;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 4adc3bf27ea..14bae7650f7 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -115,6 +115,13 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
 		const char *bonusLabel1, const char *detailText1, int bonusValue1,
 		const char *bonusLabel2, const char *detailText2, int bonusValue2,
 		int passwordIndex) {
+	if (!_release.passcodes)
+		passwordIndex = 0;
+	if (!(_release.chapterBonusMask & (1u << (unlockedChapter - 1)))) {
+		bonusLabel1 = bonusLabel2 = nullptr;
+		detailText1 = detailText2 = nullptr;
+		bonusValue1 = bonusValue2 = 0;
+	}
 	beginChapterSummaryOverlay(revealOffsetFromEnd, stopOffsetFromEnd,
 		bonusLabel1, detailText1, bonusValue1,
 		bonusLabel2, detailText2, bonusValue2,
@@ -258,6 +265,11 @@ void InsaneRebel1::playIntroSequence() {
 	if (shouldAbortGameFlow())
 		return;
 
+	if (_release.introNotice)
+		playCinematic(_release.introNotice);
+	if (shouldAbortGameFlow())
+		return;
+
 	if (_release.intro)
 		playCinematic(_release.intro);
 }
@@ -623,8 +635,10 @@ bool InsaneRebel1::runLevel6() {
 
 		if (_health >= 0) {
 			char accuracyText[80];
-			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, true);
-			const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
+			const bool tieredBonus = _release.tieredAsteroidChaseBonus;
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, !tieredBonus);
+			const int bonus = tieredBonus ? (_killCount >= 30 ? 3 : _killCount >= 20 ? 1 : 0) * _tuning.bonus :
+				calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
 			playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
 				" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 4);
 			return !shouldAbortGameFlow();
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index b04e07257be..73fff74df48 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -958,6 +958,15 @@ const ExtraGuiOption enableRebel1NoDamage = {
 	0
 };
 
+const ExtraGuiOption enableRebel1RestoredContent = {
+	_s("Restored content"),
+	_s("Play extra chapters included in the demo's game data but omitted from its original sequence"),
+	"rebel1_restored_content",
+	false,
+	0,
+	0
+};
+
 const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
 	ExtraGuiOptions options;
 	// Query the GUI options
@@ -1017,6 +1026,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_NO_DAMAGE)) {
 		options.push_back(enableRebel1NoDamage);
 	}
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_RESTORED_CONTENT)) {
+		options.push_back(enableRebel1RestoredContent);
+	}
 	if (target.empty() || gameid == "comi") {
 		options.push_back(comiObjectLabelsOption);
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index f275d63b600..7aec3765e19 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Sun Sep 27 07:03:59 2026
+  This file was generated by the md5table tool on Sun Sep 27 07:57:06 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -292,6 +292,7 @@ static const MD5Table md5table[] = {
 	{ "52a4bae0746a11d7b1e8554e91a6645c", "zak", "V2", "V2", 1916, Common::FR_FRA, Common::kPlatformDOS },
 	{ "53e94115b55dd51d4b8ff0871aa1df1e", "spyfox", "", "Demo", 20103, Common::EN_USA, Common::kPlatformUnknown },
 	{ "54a936ad06161ff7bfefcb96200f7bff", "monkey", "VGA Demo", "Demo", 7617, Common::EN_ANY, Common::kPlatformAmiga },
+	{ "551a7c06bf5098b32d5fe7641d695df8", "rebel1", "Demo 1994-04-13", "Demo 1994-04-13", 226750, Common::EN_ANY, Common::kPlatformDOS },
 	{ "55518cd73cf9c6d23ea29c51ee06bdfe", "ft", "", "", -1, Common::IT_ITA, Common::kPlatformUnknown },
 	{ "55923e34363c8ec6541368101d84a3bf", "soccer", "", "", 67178, Common::EN_GRB, Common::kPlatformWindows },
 	{ "55b4494051b06e95bcaa0d6d8f6fb757", "atlantis", "Steam", "Humble", 286720, Common::EN_ANY, Common::kPlatformWindows },


Commit: ab41e63c79ad8d88ca1c9b52ee6736722b03b905
    https://github.com/scummvm/scummvm/commit/ab41e63c79ad8d88ca1c9b52ee6736722b03b905
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA1: added support for mac demo v1.02c

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/releases.cpp
    engines/scumm/insane/rebel1/releases.h
    engines/scumm/scumm-md5.h
    engines/scumm/scumm.cpp


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 159d286c16c..276c3b6a7b1 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -452,6 +452,7 @@ ft	Full Throttle
 rebel1	Star Wars: Rebel Assault
 	274f6026d41fa2a2990a6d6b7a561dc2	217559	en	DOS	Demo v1.51	Demo v1.51	CD-ROM demo	-
 	551a7c06bf5098b32d5fe7641d695df8	226750	en	DOS	Demo 1994-04-13	Demo 1994-04-13	CD-ROM demo (chapters 8 and 10)	-
+	4e9f4571f5859ea50f1e505a1f15c408	451096	en	Mac	Demo v1.02c	Demo v1.02c	68k demo application (chapters 6 and 9)	-
 	b9ac90e4411da1b6f7dd075cc4b03c2a	214435	en	DOS	-	v1.0	-	-
 	e210a57b756473be705d24b9378a8233	212383	en	DOS	-	v1.3	-	-
 	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 5d5815d2214..564d3213c51 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -233,6 +233,7 @@ static const GameSettings gameVariantsTable[] = {
 	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 	{"rebel1", "Demo v1.51", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 	{"rebel1", "Demo 1994-04-13", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO4(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE, GAMEOPTION_REBEL1_RESTORED_CONTENT)},
+	{"rebel1", "Demo v1.02c", 0, GID_REBEL1, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index e90075a2695..17452f84537 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -300,6 +300,9 @@ const char *InsaneRebel1::uiStr(int id) const {
 // local order across builds. Extract them anchored on those neighbors; anything
 // that fails validation keeps its English fallback.
 void InsaneRebel1::loadLocalizedUiStrings() {
+	if (!_release.executable)
+		return;
+
 	Common::File f;
 	if (!f.open(_release.executable))
 		return;
diff --git a/engines/scumm/insane/rebel1/releases.cpp b/engines/scumm/insane/rebel1/releases.cpp
index aace005d32f..e79d581a4f9 100644
--- a/engines/scumm/insane/rebel1/releases.cpp
+++ b/engines/scumm/insane/rebel1/releases.cpp
@@ -59,6 +59,14 @@ static const Rebel1Release kReleases[] = {
 		"Demo 1994-04-13", "REBEL.EXE", "OPEN/O1LOGO.ANM", "OPEN/O1DEMO.ANM", "OPEN/O1OPEN.ANM", nullptr, false, false,
 		{ 8, 10 }, kDemo940413RestoredLevels,
 		1 << (6 - 1), true, { 0x289dc, 0x1a, 0x222 }, &kDemo940413Walker
+	},
+	{
+		// Macintosh v1.02c: CODE 3's dispatcher redirects chapters 1-5 to 6,
+		// 7-8 to 9, and 10-15 to the menu. The included ending is not played.
+		// Chapter assets and the tuning rows for 6/9 match the retail defaults.
+		"Demo v1.02c", nullptr, "OPEN/O1LOGO.ANM", nullptr, "OPEN/O1OPEN.ANM", nullptr, false, false,
+		{ 6, 9 }, nullptr,
+		0x7fff, false, { 0, 0, 0 }, &kRetailWalker
 	}
 };
 
diff --git a/engines/scumm/insane/rebel1/releases.h b/engines/scumm/insane/rebel1/releases.h
index 05487a303f2..1dd29af0583 100644
--- a/engines/scumm/insane/rebel1/releases.h
+++ b/engines/scumm/insane/rebel1/releases.h
@@ -50,6 +50,7 @@ struct Rebel1Release {
 	enum { kNumLevels = 15 };
 
 	const char *variant;
+	// DOS executable supplying UI strings/tuning; null uses built-in defaults.
 	const char *executable;
 	const char *logo;
 	const char *introNotice;
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 7aec3765e19..ab62d96030c 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Sun Sep 27 07:57:06 2026
+  This file was generated by the md5table tool on Sun Sep 27 12:22:39 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -273,6 +273,7 @@ static const MD5Table md5table[] = {
 	{ "4dc780f1bc587a193ce8a97652791438", "loom", "EGA", "EGA", 5748, Common::EN_ANY, Common::kPlatformAmiga },
 	{ "4e5867848ee61bc30d157e2c94eee9b4", "PuttTime", "HE 90", "Demo", 18394, Common::EN_USA, Common::kPlatformUnknown },
 	{ "4e859d3ef1e146b41e7d93c35cd6cc62", "puttzoo", "HE 100", "Lite", -1, Common::EN_ANY, Common::kPlatformIOS },
+	{ "4e9f4571f5859ea50f1e505a1f15c408", "rebel1", "Demo v1.02c", "Demo v1.02c", 451096, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "4edbf9d03550f7ba01e7f34d69b678dd", "spyfox", "HE 98.5", "Demo", 20141, Common::NL_NLD, Common::kPlatformUnknown },
 	{ "4f04b321a95d4315ce6d65f8e1dd0368", "maze", "HE 80", "", 23479, Common::EN_USA, Common::kPlatformUnknown },
 	{ "4f138ac6f9b2ac5a41bc68b2c3296064", "freddi4", "HE 99", "", 44233, Common::FR_FRA, Common::kPlatformUnknown },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index d816ca77d5f..f4ec7c544a2 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1069,8 +1069,10 @@ Common::Error ScummEngine::init() {
 		SearchMan.addSubDirectoryMatching(gameDataDir, "data");
 	}
 
-	if (_game.id == GID_REBEL1 && _game.platform == Common::kPlatformMacintosh)
+	if (_game.id == GID_REBEL1 && _game.platform == Common::kPlatformMacintosh) {
 		SearchMan.addSubDirectoryMatching(gameDataDir, "REBEL", 0, 2);
+		SearchMan.addSubDirectoryMatching(gameDataDir, "REBELMAC", 0, 2);
+	}
 #endif
 
 	// Extra directories needed for the Steam versions


Commit: c6a90ffb60f6cb8db238ba647cdf7b31b9df219b
    https://github.com/scummvm/scummvm/commit/c6a90ffb60f6cb8db238ba647cdf7b31b9df219b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA2: added support for demo da1.06

Changed paths:
  A engines/scumm/insane/rebel2/releases.cpp
  A engines/scumm/insane/rebel2/releases.h
    devtools/scumm-md5.txt
    engines/scumm/detection.h
    engines/scumm/detection_internal.h
    engines/scumm/detection_tables.h
    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/insane/rebel2/runlevels.cpp
    engines/scumm/metaengine.cpp
    engines/scumm/module.mk
    engines/scumm/scumm-md5.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 276c3b6a7b1..28439b7b4e0 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -469,6 +469,7 @@ rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	ae607f6ed270311d7dbc033d3e943d3e	750723120	us	PSX	PlayStation	-	Disc 1 raw image	-
 
 	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
+	967409fc4c12b39fb3cf47f45de2623c	646373	en	DOS	Demo DA1.06	Demo DA1.06	Playable demo (chapter 6)	-
 
 dig	The Dig
 	d8323015ecb8b10bf53474f6e6b0ae33	16304	All	All	-	-	-	Fingolfin
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 87c12d3b401..2214472fef6 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -45,6 +45,7 @@ namespace Scumm {
 #define GAMEOPTION_REBEL2_YODA_MODE                          GUIO_GAMEOPTIONS14
 #define GAMEOPTION_REBEL1_NO_DAMAGE                          GUIO_GAMEOPTIONS15
 #define GAMEOPTION_REBEL1_RESTORED_CONTENT                   GUIO_GAMEOPTIONS16
+#define GAMEOPTION_REBEL2_RESTORED_CONTENT                   GUIO_GAMEOPTIONS17
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index ed243123194..812cc76c550 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -652,6 +652,11 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 		if (!scumm_stricmp(gfp->gameid, "rebel1") && file.equalsIgnoreCase("REBEL.EXE"))
 			continue;
 
+		// RA2.EXE is shared by retail and several playable demos. Only a
+		// known executable identifies the available chapters and tuning.
+		if (!scumm_stricmp(gfp->gameid, "rebel2") && file.equalsIgnoreCase("RA2.EXE"))
+			continue;
+
 		//  ____            _     ____
 		// |  _ \ __ _ _ __| |_  |___ \ *
 		// | |_) / _` | '__| __|   __) |
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 564d3213c51..68205b5c85f 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -51,6 +51,7 @@ static const char *const directoryGlobs[] = {
 	"Resources", // Mac SE/Remastered versions
 	"OPEN",     // 3DO version of rebel1
 	"Rebel Assault PowerPC", // Mac version of rebel1
+	"LAUNCH",   // DOS versions of rebel2
 	0
 };
 
@@ -237,6 +238,7 @@ static const GameSettings gameVariantsTable[] = {
 
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
+	{"rebel2", "Demo DA1.06", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
 	{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -516,6 +518,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
+	{ "rebel2", "RA2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo DA1.06" },
 	{ "rebel2", "SLUS_003.81", kGenUnchanged, Common::EN_USA, Common::kPlatformPSX, "PlayStation" },
 
 	{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 20fe3ba6027..0538023cdd7 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -86,8 +86,8 @@ Common::String InsaneRebel2::getLevelPrefix(int levelId) {
 void InsaneRebel2::runGame() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
-	if (_vm->_game.features & GF_DEMO) {
-		splayer->play("OPEN/O_DEMO.SAN", 15);
+	if (_release.nonInteractiveVideo) {
+		splayer->play(_release.nonInteractiveVideo, 15);
 		return;
 	}
 
@@ -142,7 +142,7 @@ void InsaneRebel2::runGame() {
 				int selectedLevel = _selectedChapter + 1;
 				debugC(DEBUG_INSANE, "InsaneRebel2: Starting chapter %d (level %d)", _selectedChapter + 1, selectedLevel);
 
-				if (selectedLevel == 16) {
+				if (selectedLevel == 16 && _release.ending) {
 					playEndingSequence();
 				}
 
@@ -150,10 +150,12 @@ void InsaneRebel2::runGame() {
 					int result = runLevel(selectedLevel);
 
 					if (result == kLevelNextLevel) {
-						updatePilotProgress(selectedLevel,
+						selectedLevel = _release.getNextChapter(selectedLevel);
+						if (!selectedLevel)
+							break;
+						updatePilotProgress(selectedLevel - 1,
 							_playerScore, _playerLives, 0, _playerRating);
-						selectedLevel++;
-						if (selectedLevel > 15) {
+						if (selectedLevel == 16) {
 							playEndingSequence();
 							break;
 						}
@@ -325,7 +327,11 @@ void InsaneRebel2::prepareLevelEndStats(int levelId, int accuracy, int flightErr
 		return;
 	}
 
-	const Rebel2LevelEndParams &p = kRebel2LevelEndParams[levelId];
+	Rebel2LevelEndParams p = kRebel2LevelEndParams[levelId];
+	if (levelId == 6 && _release.skipMiningFacilityAttack) {
+		p.titleStartBeforeEnd = 120;
+		p.titleEndBeforeEnd = 10;
+	}
 	const bool hasAccuracy = (accuracy >= 0 && p.accLow >= 0 && p.accHigh >= 0);
 	const bool hasFlightErrors = (flightErrors >= 0 && p.errLow >= 0 && p.errHigh >= 0);
 	const int ratingAward = calculateLevelEndRating(
@@ -382,6 +388,8 @@ void InsaneRebel2::playLevelEnd(int levelId, int accuracy, int flightErrors, boo
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
+	if (levelId == 6 && _release.skipMiningFacilityAttack)
+		filename = "LEV06/06END_B.SAN";
 
 	debugC(DEBUG_INSANE, "Playing level %d end: %s", levelId, filename.c_str());
 
@@ -510,7 +518,7 @@ int InsaneRebel2::runLevel(int levelId) {
 
 	debugC(DEBUG_INSANE, "Starting level %d", levelId);
 
-	if (levelId < 1 || levelId > 15) {
+	if (levelId < 1 || levelId > 15 || !_release.isChapterAvailable(levelId)) {
 		warning("Rebel2: Invalid level ID %d", levelId);
 		return kLevelReturnToMenu;
 	}
@@ -536,7 +544,8 @@ int InsaneRebel2::runLevel(int levelId) {
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
 
-	if (_activePilot >= 0 && _activePilot < _numPilots && _pilots[_activePilot].damage[levelId - 1] < 0xFF) {
+	if (!_release.unlockAvailableLevels && _activePilot >= 0 && _activePilot < _numPilots &&
+			_pilots[_activePilot].damage[levelId - 1] < 0xFF) {
 		_playerLives = _pilots[_activePilot].lives[levelId - 1];
 		_playerScore = _pilots[_activePilot].score[levelId - 1];
 		_playerRating = _pilots[_activePilot].rating[levelId - 1];
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index b20bccd4d8a..92f064b51e1 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -69,6 +69,7 @@ bool InsaneRebel2::isMenuTextInputActive() const {
 	return _gameState == kStateChapterSelect &&
 	       _chapterSelection >= 0 &&
 	       _chapterSelection < 16 &&
+	       _release.isChapterAvailable(_chapterSelection + 1) &&
 	       !_chapterUnlocked[_chapterSelection];
 }
 
@@ -90,8 +91,18 @@ void InsaneRebel2::updateMenuVirtualKeyboard() {
 void InsaneRebel2::unlockAllChapters() {
 	debugC(DEBUG_INSANE, "Unlocking all chapters for testing");
 	for (int i = 0; i < 16; i++) {
-		_chapterUnlocked[i] = true;
-		_levelUnlocked[i] = true;
+		_chapterUnlocked[i] = _release.isChapterAvailable(i + 1);
+		_levelUnlocked[i] = _chapterUnlocked[i];
+	}
+}
+
+void InsaneRebel2::updateChapterUnlocks() {
+	for (int i = 0; i < 16; i++) {
+		// 0xFF is PilotData::init()'s "never played" marker.
+		const bool reached = (_activePilot >= 0 && _activePilot < _numPilots) ?
+			_pilots[_activePilot].damage[i] < 0xFF : i + 1 == _release.levels[0];
+		_chapterUnlocked[i] = _release.isChapterAvailable(i + 1) &&
+			(_debugUnlockAll || _release.unlockAvailableLevels || reached);
 	}
 }
 
@@ -664,6 +675,12 @@ int InsaneRebel2::runChapterSelect() {
 		}
 
 		if (_chapterSelection >= 0 && _chapterSelection < 16) {
+			if (!_release.isChapterAvailable(_chapterSelection + 1)) {
+				_passwordInput.clear();
+				playSfx(3, 127, 0);
+				continue;
+			}
+
 			if (_chapterUnlocked[_chapterSelection]) {
 				_selectedChapter = _chapterSelection;
 				debugC(DEBUG_INSANE, "Chapter %d selected (unlocked)", _selectedChapter + 1);
@@ -915,6 +932,8 @@ Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
 void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
 	if (_chapterSelection < 0 || _chapterSelection >= 16)
 		return;
+	if (!_release.isChapterAvailable(_chapterSelection + 1))
+		return;
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	if (!splayer)
@@ -930,7 +949,9 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 		int32 pilotLives = 0;
 		int32 pilotScore = 0;
 		int16 pilotRating = 0;
-		if (_activePilot >= 0 && _activePilot < _numPilots) {
+		if (_release.unlockAvailableLevels) {
+			pilotLives = 3;
+		} else if (_activePilot >= 0 && _activePilot < _numPilots) {
 			pilotLives = _pilots[_activePilot].lives[_chapterSelection];
 			pilotScore = _pilots[_activePilot].score[_chapterSelection];
 			pilotRating = _pilots[_activePilot].rating[_chapterSelection];
@@ -996,7 +1017,7 @@ int InsaneRebel2::runLevelSelect() {
 
 	_levelSelection = 0;
 	_levelItemCount = _numPilots + 4;
-	_selectedLevel = 1;
+	_selectedLevel = _release.levels[0];
 	_menuRepeatDelay = 0;
 	_gameState = kStatePilotSelect;
 	_pilotMenuMode = kPilotModeSelect;
@@ -1032,9 +1053,7 @@ int InsaneRebel2::runLevelSelect() {
 				savePilots();
 				_activePilot = _pilotEditIndex;
 
-				for (int i = 0; i < 16; i++) {
-					_chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
-				}
+				updateChapterUnlocks();
 			}
 			_pilotMenuMode = kPilotModeSelect;
 			_levelItemCount = _numPilots + 4;
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 9761d3aa758..91d08b83082 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -159,7 +159,8 @@ bool InsaneRebel2::isSkippableVideoState() const {
 	return _gameState != kStateGameplay || _rebelHandler == 0;
 }
 
-InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
+InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) :
+		_release(getRebel2Release(scumm->_game.variant, ConfMan.getBool("rebel2_restored_content"))) {
 	_vm = scumm;
 	// Rebel Assault II skips ScummEngine::resetScumm(), which normally clears this state.
 	for (int i = 0; i < kScummActionCount; i++)
@@ -508,7 +509,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_menuRepeatDelay = 0;
 	_menuSelectionConfirmed = false;
 	for (i = 0; i < 16; i++) {
-		_levelUnlocked[i] = (i == 0);
+		_levelUnlocked[i] = (i + 1 == _release.levels[0]);
 	}
 
 	_chapterSelection = 0;
@@ -520,10 +521,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_noDamage = ConfMan.getBool("rebel2_no_damage");
 	_rebelYodaMode = ConfMan.getBool("rebel2_yoda_mode");
 
-	for (i = 0; i < 16; i++) {
-		_chapterUnlocked[i] = _debugUnlockAll || (i == 0);
-	}
-
 	_previewOffsetX = -90;
 	_previewOffsetY = 75;
 
@@ -534,10 +531,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_pilots[i].init();
 	}
 	loadPilots();
+	updateChapterUnlocks();
 
 	_levelSelection = 0;
 	_levelItemCount = _numPilots + 4;
-	_selectedLevel = 1;
+	_selectedLevel = _release.levels[0];
 	_difficultySelection = 2;
 	_pilotMenuMode = kPilotModeSelect;
 	_pilotNameInput = "";
@@ -1327,7 +1325,10 @@ int InsaneRebel2::getDifficultyRow() const {
 }
 
 InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
-	return kDifficultyTable[CLIP(_difficulty, 0, 5)][getDifficultyRow()];
+	const int difficulty = CLIP(_difficulty, 0, 5);
+	const int levelType = getDifficultyRow();
+	const LevelDifficultyParams *params = _release.getDifficultyOverride(difficulty, levelType);
+	return params ? *params : kDifficultyTable[difficulty][levelType];
 }
 
 bool InsaneRebel2::applyPlayerDamage(int damage) {
@@ -1547,7 +1548,7 @@ int InsaneRebel2::createNewPilot() {
 		return -1;
 
 	int idx = _numPilots;
-	_pilots[idx].init();
+	_pilots[idx].init(_release.levels[0], _release.unlockAvailableLevels ? 3 : 4);
 	_numPilots++;
 	return idx;
 }
@@ -1587,6 +1588,8 @@ void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives,
 		return;
 	if (levelIndex < 0 || levelIndex >= kNumLevels)
 		return;
+	if (!_release.isChapterAvailable(levelIndex + 1))
+		return;
 
 	PilotData &pilot = _pilots[_activePilot];
 
@@ -1613,9 +1616,7 @@ bool InsaneRebel2::selectPilot(int index) {
 	_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);
+	updateChapterUnlocks();
 
 	return true;
 }
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 880451c70a8..cf6f3adbbf6 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -30,6 +30,7 @@
 #include "scumm/insane/rebel/rebel_audio.h"
 #include "scumm/insane/rebel/rebel_gamepad.h"
 #include "scumm/insane/rebel/rebel_touch.h"
+#include "scumm/insane/rebel2/releases.h"
 
 #include "common/keyboard.h"
 #include "common/list.h"
@@ -55,6 +56,8 @@ public:
 	InsaneRebel2(ScummEngine_v7 *scumm);
 	~InsaneRebel2();
 
+	const Rebel2Release _release;
+
 	bool notifyEvent(const Common::Event &event) override;
 
 	Common::Queue<Common::Event> _menuEventQueue;
@@ -131,6 +134,7 @@ public:
 	bool _noDamage;
 
 	void unlockAllChapters();
+	void updateChapterUnlocks();
 	int runChapterSelect();
 	void drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
 	int processChapterSelectInput();
@@ -195,18 +199,19 @@ public:
 		int16 rating[kNumLevels];
 		int16 difficulty;
 
-		void init() {
+		void init(int firstLevel = 1, int initialLives = 4) {
 			memset(name, 0, sizeof(name));
 			memset(rating, 0, sizeof(rating));
 			difficulty = 2;
-			score[0] = 0;
-			lives[0] = 4;
-			damage[0] = 0;
-			for (int i = 1; i < kNumLevels; i++) {
+			for (int i = 0; i < kNumLevels; i++) {
 				score[i] = 0;
 				lives[i] = 0xFF;
 				damage[i] = 0xFF;
 			}
+			if (firstLevel >= 1 && firstLevel <= kNumLevels) {
+				lives[firstLevel - 1] = initialLives;
+				damage[firstLevel - 1] = 0;
+			}
 		}
 	};
 
@@ -890,25 +895,7 @@ public:
 	int _difficulty;
 
 	// Per-level difficulty parameters.
-	struct LevelDifficultyParams {
-		int16 laserDelay;
-		int16 snapDistance;
-		int16 missDamage;
-		int16 dodgeDamage;
-		int16 shotDamage;
-		int16 specialDamage;
-		int16 shotAccuracy;
-		int16 hitPoints;
-		int16 dodgePoints;
-		int16 timePoints;
-		int16 levelPoints;
-		int16 specialPoints;
-		int16 flags;
-		int16 rollRate;
-		int16 liftRate;
-		int16 slideRate;
-		int16 driftRate;
-	};
+	typedef Rebel2DifficultyParams LevelDifficultyParams;
 
 	static const LevelDifficultyParams kDifficultyTable[6][17];
 
diff --git a/engines/scumm/insane/rebel2/releases.cpp b/engines/scumm/insane/rebel2/releases.cpp
new file mode 100644
index 00000000000..75eb507a2d5
--- /dev/null
+++ b/engines/scumm/insane/rebel2/releases.cpp
@@ -0,0 +1,117 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "scumm/insane/rebel2/releases.h"
+
+namespace Scumm {
+
+// DA1.06's difficulty table starts at offset 0x4798 in LE object 3.
+// Rows have 17 words; each difficulty occupies 0x242 bytes. Types 5 and 6
+// cover the shield attack and reactor attack respectively.
+static const Rebel2DifficultyOverride kDemoDA106Difficulty[] = {
+	{ 5, {
+		{ 5, 5, -1, 19, 2, 15, 75, 25, 50, 2, 500, -1, 8, -1, -1, -1, -1 },
+		{ 5, 3, -1, 25, 4, 15, 75, 50, 100, 4, 1000, -1, 16, -1, -1, -1, -1 },
+		{ 5, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1 },
+		{ 5, 0, 220, 50, 15, 15, 79, 100, 200, 8, 2000, -1, 4, -1, -1, -1, -1 },
+		{ 5, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1 },
+		{ 5, 1, -1, 30, 6, 15, 75, 75, 150, 6, 1500, -1, 0, -1, -1, -1, -1 }
+	} },
+	{ 6, {
+		{ 5, 5, 180, 27, 3, -1, 75, 25, 50, 2, 500, 250, 8, 120, 120, 120, 75 },
+		{ 5, 3, 190, 35, 4, -1, 75, 50, 100, 4, 1000, 500, 16, 140, 140, 140, 90 },
+		{ 5, 1, 200, 50, 4, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105 },
+		{ 5, 0, 220, 90, 15, -1, 90, 100, 200, 8, 2000, 1000, 4, 180, 180, 180, 140 },
+		{ 5, 1, 200, 50, 4, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105 },
+		{ 5, 1, 200, 50, 4, -1, 75, 75, 150, 6, 1500, 750, 0, 160, 160, 160, 105 }
+	} }
+};
+
+// Variants match detection_tables.h. Availability also limits passwords,
+// saved pilot progress and the unlock-all option.
+static const Rebel2Release kReleases[] = {
+	{
+		"", "SYSTM/GAME.TRS", nullptr,
+		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+		true, false, false, nullptr, 0
+	},
+	{
+		"Demo", "SYSTM/GAME.TRS", "OPEN/O_DEMO.SAN", { 0 },
+		false, false, false, nullptr, 0
+	},
+	{
+		// RBL2DEMO.EXE selects GAME_E.TRS. The original chapter-6 handler
+		// (LE object 1, 0x10d10) skips the shield attack and uses 06END_B.
+		"Demo DA1.06", "SYSTM/GAME_E.TRS", nullptr, { 6 },
+		false, true, true, kDemoDA106Difficulty, ARRAYSIZE(kDemoDA106Difficulty)
+	}
+};
+
+bool Rebel2Release::isChapterAvailable(int chapter) const {
+	if (chapter == kFinale)
+		return ending;
+	if (chapter < 1 || chapter > kNumLevels)
+		return false;
+	for (int i = 0; i < kNumLevels && levels[i]; ++i) {
+		if (levels[i] == chapter)
+			return true;
+	}
+	return false;
+}
+
+int Rebel2Release::getNextChapter(int chapter) const {
+	for (int i = 0; i < kNumLevels && levels[i]; ++i) {
+		if (levels[i] == chapter) {
+			if (i + 1 < kNumLevels && levels[i + 1])
+				return levels[i + 1];
+			return ending ? kFinale : 0;
+		}
+	}
+	return 0;
+}
+
+const Rebel2DifficultyParams *Rebel2Release::getDifficultyOverride(int difficulty, int levelType) const {
+	if (difficulty < 0 || difficulty >= 6)
+		return nullptr;
+	for (uint i = 0; i < difficultyOverrideCount; ++i) {
+		if (difficultyOverrides[i].levelType == levelType)
+			return &difficultyOverrides[i].difficulty[difficulty];
+	}
+	return nullptr;
+}
+
+Rebel2Release getRebel2Release(const char *variant, bool restoredContent) {
+	if (!variant)
+		variant = "";
+	for (const Rebel2Release &release : kReleases) {
+		if (!strcmp(variant, release.variant)) {
+			Rebel2Release result = release;
+			if (restoredContent)
+				result.skipMiningFacilityAttack = false;
+			return result;
+		}
+	}
+	error("Unknown Rebel Assault II release '%s'", variant);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/releases.h b/engines/scumm/insane/rebel2/releases.h
new file mode 100644
index 00000000000..d2eabfb9884
--- /dev/null
+++ b/engines/scumm/insane/rebel2/releases.h
@@ -0,0 +1,78 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_INSANE_REBEL2_RELEASES_H
+#define SCUMM_INSANE_REBEL2_RELEASES_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+struct Rebel2DifficultyParams {
+	int16 laserDelay;
+	int16 snapDistance;
+	int16 missDamage;
+	int16 dodgeDamage;
+	int16 shotDamage;
+	int16 specialDamage;
+	int16 shotAccuracy;
+	int16 hitPoints;
+	int16 dodgePoints;
+	int16 timePoints;
+	int16 levelPoints;
+	int16 specialPoints;
+	int16 flags;
+	int16 rollRate;
+	int16 liftRate;
+	int16 slideRate;
+	int16 driftRate;
+};
+
+struct Rebel2DifficultyOverride {
+	byte levelType;
+	Rebel2DifficultyParams difficulty[6];
+};
+
+struct Rebel2Release {
+	enum { kNumLevels = 15, kFinale = 16 };
+
+	const char *variant;
+	const char *strings;
+	const char *nonInteractiveVideo;
+	// Chapter numbers in playback order; unused entries are zero.
+	byte levels[kNumLevels];
+	bool ending;
+	bool unlockAvailableLevels;
+	// The DA1.06 demo starts chapter 6 at the attack on the reactor.
+	bool skipMiningFacilityAttack;
+	const Rebel2DifficultyOverride *difficultyOverrides;
+	uint difficultyOverrideCount;
+
+	bool isChapterAvailable(int chapter) const;
+	int getNextChapter(int chapter) const;
+	const Rebel2DifficultyParams *getDifficultyOverride(int difficulty, int levelType) const;
+};
+
+Rebel2Release getRebel2Release(const char *variant, bool restoredContent = false);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index ca223022b9e..9f01bf9382c 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -894,7 +894,7 @@ int InsaneRebel2::runLevel5() {
 }
 
 int InsaneRebel2::runLevel6() {
-	int phase1Score = 0;
+	int phase1Score = _playerScore;
 	int phase1FlightErrors = 0;
 
 	playLevelBegin(6);
@@ -907,57 +907,58 @@ int InsaneRebel2::runLevel6() {
 
 		_rebelPhaseState = 0xffffffff;
 
-		_rebelLevelType = 5;
-		_currentPhase = 1;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-
-		debugC(DEBUG_INSANE, "Level 6 Phase 1 (shield attack run)");
-		resetShieldGauge();
-		_rebelShieldGateActive = true;
-
-		if (!playLevelSegment("LEV06/06PLAY1.SAN", 0x28)) {
-			_rebelShieldGateActive = false;
-			return kLevelQuit;
-		}
+		if (!_release.skipMiningFacilityAttack) {
+			_rebelLevelType = 5;
+			_currentPhase = 1;
+			_rebelKillCounter = 0;
+			_rebelHitCounter = 0;
 
-		while (_playerShield > 0 && !_rebelShieldDestroyed && !_vm->shouldQuit()) {
+			debugC(DEBUG_INSANE, "Level 6 Phase 1 (shield attack run)");
 			resetShieldGauge();
-			if (!playLevelSegment("LEV06/06PLAY1B.SAN", 0x468)) {
+			_rebelShieldGateActive = true;
+
+			if (!playLevelSegment("LEV06/06PLAY1.SAN", 0x28)) {
 				_rebelShieldGateActive = false;
 				return kLevelQuit;
 			}
-		}
-		_rebelShieldGateActive = false;
 
-		if (_playerShield <= 0) {
-			debugC(DEBUG_INSANE, "Level 6 Phase 1 death at frame %d", _deathFrame);
-			playLevelDeathVariant(6, 1, _deathFrame);
-			if (_vm->shouldQuit())
-				return kLevelQuit;
+			while (_playerShield > 0 && !_rebelShieldDestroyed && !_vm->shouldQuit()) {
+				resetShieldGauge();
+				if (!playLevelSegment("LEV06/06PLAY1B.SAN", 0x468)) {
+					_rebelShieldGateActive = false;
+					return kLevelQuit;
+				}
+			}
+			_rebelShieldGateActive = false;
 
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(6);
-				return kLevelGameOver;
+			if (_playerShield <= 0) {
+				debugC(DEBUG_INSANE, "Level 6 Phase 1 death at frame %d", _deathFrame);
+				playLevelDeathVariant(6, 1, _deathFrame);
+				if (_vm->shouldQuit())
+					return kLevelQuit;
+
+				_playerLives--;
+				if (_playerLives <= 0) {
+					playLevelGameOver(6);
+					return kLevelGameOver;
+				}
+
+				playLevelRetryVariant(6, 1);
+				if (_vm->shouldQuit())
+					return kLevelQuit;
+				_playerShield = 255;  // full shield on retry (original resets damage before replaying)
+				_playerDamage = 0;
+				continue;
 			}
 
-			playLevelRetryVariant(6, 1);
-			if (_vm->shouldQuit())
+			phase1Score = _playerScore;
+			phase1FlightErrors = _rebelHitCounter;
+			_rebelHandler = 0;
+			_rebelStatusBarSprite = 0;
+			if (!playLevelSegment("LEV06/06POST1.SAN", 0x28, false))
 				return kLevelQuit;
-			_playerShield = 255;  // full shield on retry (original resets damage before replaying)
-			_playerDamage = 0;
-			continue;
 		}
 
-		phase1Score = _playerScore;
-		phase1FlightErrors = _rebelHitCounter;
-
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		if (!playLevelSegment("LEV06/06POST1.SAN", 0x28, false))
-			return kLevelQuit;
-
 		while (!_vm->shouldQuit()) {
 			_rebelLevelType = 6;
 			_currentPhase = 2;
@@ -973,7 +974,7 @@ int InsaneRebel2::runLevel6() {
 
 			if (_playerShield > 0) {
 				int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
-				int flightErrors = phase1FlightErrors + _rebelHitCounter;
+				int flightErrors = _release.skipMiningFacilityAttack ? -1 : phase1FlightErrors + _rebelHitCounter;
 				debugC(DEBUG_INSANE, "Level 6 completed!");
 				playLevelEnd(6, accuracy, flightErrors, false);
 				_levelUnlocked[6] = true;
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 73fff74df48..d6bf12f63d6 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -940,6 +940,15 @@ static const ExtraGuiOption enableRebel2YodaMode = {
 	0
 };
 
+static const ExtraGuiOption enableRebel2RestoredContent = {
+	_s("Restored content"),
+	_s("Play additional sections included in the demo's game data but omitted from its original sequence"),
+	"rebel2_restored_content",
+	false,
+	0,
+	0
+};
+
 const ExtraGuiOption enableRebel1UnlockAll = {
 	_s("Unlock all levels"),
 	_s("All levels will be available without requiring passwords"),
@@ -1020,6 +1029,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_YODA_MODE)) {
 		options.push_back(enableRebel2YodaMode);
 	}
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_RESTORED_CONTENT)) {
+		options.push_back(enableRebel2RestoredContent);
+	}
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_UNLOCK_ALL)) {
 		options.push_back(enableRebel1UnlockAll);
 	}
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 5436864add9..c8ab8c301ff 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -160,6 +160,7 @@ MODULE_OBJS += \
 	insane/rebel1/runlevels.o \
 	insane/rebel1/saveload.o \
 	insane/rebel2/rebel.o \
+	insane/rebel2/releases.o \
 	insane/rebel2/audio.o \
 	insane/rebel2/iact.o \
 	insane/rebel2/levels.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index ab62d96030c..20a80064dd4 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Sun Sep 27 12:22:39 2026
+  This file was generated by the md5table tool on Sun Sep 27 12:38:49 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -511,6 +511,7 @@ static const MD5Table md5table[] = {
 	{ "95818b178d473c989ac753574e8892aa", "readtime", "", "Demo", 15052, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "95b3806e043be08668c54c3ffe98650f", "BluesABCTime", "", "", 26391, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "95be99181bd0f10fef4872c2d4a771cb", "zak", "V1", "", 1914, Common::DE_DEU, Common::kPlatformC64 },
+	{ "967409fc4c12b39fb3cf47f45de2623c", "rebel2", "Demo DA1.06", "Demo DA1.06", 646373, Common::EN_ANY, Common::kPlatformDOS },
 	{ "9684c161258d68e0d464d6cab7024b9c", "spyfox2", "", "Mini Game", 14689, Common::IT_ITA, Common::kPlatformWindows },
 	{ "96a3069a3c63caa7329588ce1fef41ee", "spyozon", "", "", -1, Common::RU_RUS, Common::kPlatformUnknown },
 	{ "9708cf716ed8bcc9ff3fcfc69413b746", "puttputt", "HE 62", "", 8117, Common::EN_ANY, Common::kPlatformDOS },
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index a346b4896ce..1d9168639e3 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -632,7 +632,7 @@ public:
 
 bool SmushPlayerRebel2::handleGameSetupStrings() {
 	ScummFile *theFile = _vm->instantiateScummFile();
-	_vm->openFile(*theFile, "SYSTM/GAME.TRS");
+	_vm->openFile(*theFile, static_cast<InsaneRebel2 *>(_insane)->_release.strings);
 	if (!theFile->isOpen()) {
 		delete theFile;
 		return true; // handled (no strings available)


Commit: 6ad8570f34888b7d2f510d79cfbf1c710f11be8a
    https://github.com/scummvm/scummvm/commit/6ad8570f34888b7d2f510d79cfbf1c710f11be8a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA2: added support for demo dg1.15

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/releases.cpp
    engines/scumm/insane/rebel2/releases.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 28439b7b4e0..a7664f07b51 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -470,6 +470,7 @@ rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 
 	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
 	967409fc4c12b39fb3cf47f45de2623c	646373	en	DOS	Demo DA1.06	Demo DA1.06	Playable demo (chapter 6)	-
+	8e0788210af982fbbc43c244ac6f9581	663561	en	DOS	Demo DG1.15	Demo DG1.15	The Dreighton Encounter (chapters 1, 2 and 4)	-
 
 dig	The Dig
 	d8323015ecb8b10bf53474f6e6b0ae33	16304	All	All	-	-	-	Fingolfin
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 68205b5c85f..7fdba161d30 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -239,6 +239,7 @@ static const GameSettings gameVariantsTable[] = {
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo DA1.06", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
+	{"rebel2", "Demo DG1.15", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 0538023cdd7..9e53b9b390e 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -151,8 +151,11 @@ void InsaneRebel2::runGame() {
 
 					if (result == kLevelNextLevel) {
 						selectedLevel = _release.getNextChapter(selectedLevel);
-						if (!selectedLevel)
+						if (!selectedLevel) {
+							if (_release.completionVideo && !_vm->shouldQuit())
+								playCinematic(_release.completionVideo, 0x08);
 							break;
+						}
 						updatePilotProgress(selectedLevel - 1,
 							_playerScore, _playerLives, 0, _playerRating);
 						if (selectedLevel == 16) {
@@ -200,7 +203,7 @@ void InsaneRebel2::playMissionBriefing() {
 	splayer->play("OPEN/O_LEVEL.SAN", 15);
 }
 
-void InsaneRebel2::playCinematic(const char *filename) {
+void InsaneRebel2::playCinematic(const char *filename, int16 flags) {
 	restoreDamageFlashPalette();
 	resetVideoAudio();
 	_gameplaySectionActive = false;
@@ -208,7 +211,7 @@ void InsaneRebel2::playCinematic(const char *filename) {
 	_rebelStatusBarSprite = 0;
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x28);
+	splayer->setCurVideoFlags(flags);
 	splayer->play(filename, 15);
 }
 
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index cf6f3adbbf6..cce7e1b0b59 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -335,7 +335,7 @@ public:
 
 	int getRandomVariant(int max);
 	Common::String selectDeathVideoVariant(int levelId, int phase, int frame);
-	void playCinematic(const char *filename);
+	void playCinematic(const char *filename, int16 flags = 0x28);
 
 	// Text is progressively revealed during [fadeInFrame, fadeOutFrame)
 	void playVideoWithText(const char *filename, int textID, int textX, int textY,
diff --git a/engines/scumm/insane/rebel2/releases.cpp b/engines/scumm/insane/rebel2/releases.cpp
index 75eb507a2d5..080e672d815 100644
--- a/engines/scumm/insane/rebel2/releases.cpp
+++ b/engines/scumm/insane/rebel2/releases.cpp
@@ -51,19 +51,26 @@ static const Rebel2DifficultyOverride kDemoDA106Difficulty[] = {
 // saved pilot progress and the unlock-all option.
 static const Rebel2Release kReleases[] = {
 	{
-		"", "SYSTM/GAME.TRS", nullptr,
+		"", "SYSTM/GAME.TRS", nullptr, nullptr,
 		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
-		true, false, false, nullptr, 0
+		true, false, false, false, nullptr, 0
 	},
 	{
-		"Demo", "SYSTM/GAME.TRS", "OPEN/O_DEMO.SAN", { 0 },
-		false, false, false, nullptr, 0
+		"Demo", "SYSTM/GAME.TRS", "OPEN/O_DEMO.SAN", nullptr, { 0 },
+		false, false, false, false, nullptr, 0
 	},
 	{
 		// RBL2DEMO.EXE selects GAME_E.TRS. The original chapter-6 handler
 		// (LE object 1, 0x10d10) skips the shield attack and uses 06END_B.
-		"Demo DA1.06", "SYSTM/GAME_E.TRS", nullptr, { 6 },
-		false, true, true, kDemoDA106Difficulty, ARRAYSIZE(kDemoDA106Difficulty)
+		"Demo DA1.06", "SYSTM/GAME_E.TRS", nullptr, nullptr, { 6 },
+		false, true, true, false, kDemoDA106Difficulty, ARRAYSIZE(kDemoDA106Difficulty)
+	},
+	{
+		// GAME.TRS selects chapters 1, 2 and 4 in playable-demo mode.
+		// LE object 1, 0xf860 advances passwords past missing chapters;
+		// 0x12eb0 plays O_PLAYDE instead of the retail finale.
+		"Demo DG1.15", "SYSTM/GAME.TRS", nullptr, "OPEN/O_PLAYDE.SAN", { 1, 2, 4 },
+		false, false, false, true, nullptr, 0
 	}
 };
 
@@ -90,6 +97,16 @@ int Rebel2Release::getNextChapter(int chapter) const {
 	return 0;
 }
 
+int Rebel2Release::getCompletionPasswordChapter(int chapter) const {
+	if (!advanceCompletionPasswords || !isChapterAvailable(chapter))
+		return chapter;
+
+	// Passwords unlock the chapter following their retail chapter number.
+	// DG1.15 shows the finale password after its last included chapter.
+	const int nextChapter = getNextChapter(chapter);
+	return nextChapter ? nextChapter - 1 : kNumLevels;
+}
+
 const Rebel2DifficultyParams *Rebel2Release::getDifficultyOverride(int difficulty, int levelType) const {
 	if (difficulty < 0 || difficulty >= 6)
 		return nullptr;
diff --git a/engines/scumm/insane/rebel2/releases.h b/engines/scumm/insane/rebel2/releases.h
index d2eabfb9884..f979c1ba12a 100644
--- a/engines/scumm/insane/rebel2/releases.h
+++ b/engines/scumm/insane/rebel2/releases.h
@@ -57,17 +57,20 @@ struct Rebel2Release {
 	const char *variant;
 	const char *strings;
 	const char *nonInteractiveVideo;
+	const char *completionVideo;
 	// Chapter numbers in playback order; unused entries are zero.
 	byte levels[kNumLevels];
 	bool ending;
 	bool unlockAvailableLevels;
 	// The DA1.06 demo starts chapter 6 at the attack on the reactor.
 	bool skipMiningFacilityAttack;
+	bool advanceCompletionPasswords;
 	const Rebel2DifficultyOverride *difficultyOverrides;
 	uint difficultyOverrideCount;
 
 	bool isChapterAvailable(int chapter) const;
 	int getNextChapter(int chapter) const;
+	int getCompletionPasswordChapter(int chapter) const;
 	const Rebel2DifficultyParams *getDifficultyOverride(int difficulty, int levelType) const;
 };
 
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 656ad55fc71..6680e061f5c 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2707,7 +2707,8 @@ void InsaneRebel2::renderLevelEndStatsOverlay(byte *renderBitmap, int pitch, int
 	if (!titleFmt || !titleFmt[0])
 		titleFmt = getRebel2LevelEndFallbackString(190);
 
-	Common::String password = getChapterPassword(_levelEndStats.levelId, _difficulty);
+	const int passwordChapter = _release.getCompletionPasswordChapter(_levelEndStats.levelId);
+	Common::String password = getChapterPassword(passwordChapter, _difficulty);
 	Common::String titleText = Common::String::format(titleFmt, password.c_str());
 	int visibleChars = curFrame + 10 - titleStart;
 	if (visibleChars > 0xbe)
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 20a80064dd4..2c40e90565f 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Sun Sep 27 12:38:49 2026
+  This file was generated by the md5table tool on Sun Sep 27 12:52:41 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -481,6 +481,7 @@ static const MD5Table md5table[] = {
 	{ "8d6a88fc58b72785621712fcffc03141", "atlantis", "", "CD", 12035, Common::HE_ISR, Common::kPlatformDOS },
 	{ "8dd4d590685c19bf651b5016e749ead2", "PuttTime", "HE 99", "Mini Game", 18458, Common::FR_FRA, Common::kPlatformWindows },
 	{ "8de13897f0121c79d29a2377159f9ad0", "socks", "HE 99", "Updated", 13668, Common::EN_ANY, Common::kPlatformWindows },
+	{ "8e0788210af982fbbc43c244ac6f9581", "rebel2", "Demo DG1.15", "Demo DG1.15", 663561, Common::EN_ANY, Common::kPlatformDOS },
 	{ "8e1272e5c12ac35e29e3e4c50ec4a525", "rebel1", "", "", 526296, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "8e3241ddd6c8dadf64305e8740d45e13", "balloon", "HE 100", "Updated", -1, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "8e4ee4db46954bfe2912e259a16fad82", "monkey2", "", "", 11135, Common::FR_FRA, Common::kPlatformDOS },


Commit: 24c327ead16461902940e03dec62a5a90ce1c719
    https://github.com/scummvm/scummvm/commit/24c327ead16461902940e03dec62a5a90ce1c719
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-27T16:14:25+02:00

Commit Message:
SCUMM: RA2: added support for mac demo 1.0

Changed paths:
  A engines/scumm/insane/rebel2/mac_archive.cpp
  A engines/scumm/insane/rebel2/mac_archive.h
    devtools/scumm-md5.txt
    engines/scumm/detection_internal.h
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/releases.cpp
    engines/scumm/insane/rebel2/releases.h
    engines/scumm/module.mk
    engines/scumm/scumm-md5.h
    engines/scumm/scumm.cpp


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index a7664f07b51..02562e81c87 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -471,6 +471,7 @@ rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
 	967409fc4c12b39fb3cf47f45de2623c	646373	en	DOS	Demo DA1.06	Demo DA1.06	Playable demo (chapter 6)	-
 	8e0788210af982fbbc43c244ac6f9581	663561	en	DOS	Demo DG1.15	Demo DG1.15	The Dreighton Encounter (chapters 1, 2 and 4)	-
+	d89f31e209c0642b4d20ccf615840401	128658792	en	Mac	Demo v1.0	Demo v1.0	Mac bundle (chapter 6)	-
 
 dig	The Dig
 	d8323015ecb8b10bf53474f6e6b0ae33	16304	All	All	-	-	-	Fingolfin
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index 812cc76c550..8c59c384ce8 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -653,8 +653,9 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 			continue;
 
 		// RA2.EXE is shared by retail and several playable demos. Only a
-		// known executable identifies the available chapters and tuning.
-		if (!scumm_stricmp(gfp->gameid, "rebel2") && file.equalsIgnoreCase("RA2.EXE"))
+		// known executable or Mac bundle identifies the chapters and tuning.
+		if (!scumm_stricmp(gfp->gameid, "rebel2") &&
+			(baseFile.equalsIgnoreCase("RA2.EXE") || platform == Common::kPlatformMacintosh))
 			continue;
 
 		//  ____            _     ____
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 7fdba161d30..6c3dbdd7a6a 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -240,6 +240,7 @@ static const GameSettings gameVariantsTable[] = {
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo DA1.06", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
 	{"rebel2", "Demo DG1.15", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
+	{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
 	{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -520,6 +521,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
 	{ "rebel2", "RA2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo DA1.06" },
+	{ "rebel2", "Rebel Assault II Demo Data", kGenUnchanged, UNK_LANG, Common::kPlatformMacintosh, "Demo v1.0" },
 	{ "rebel2", "SLUS_003.81", kGenUnchanged, Common::EN_USA, Common::kPlatformPSX, "PlayStation" },
 
 	{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/insane/rebel2/mac_archive.cpp b/engines/scumm/insane/rebel2/mac_archive.cpp
new file mode 100644
index 00000000000..7bba0ad204e
--- /dev/null
+++ b/engines/scumm/insane/rebel2/mac_archive.cpp
@@ -0,0 +1,112 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/archive.h"
+#include "common/array.h"
+#include "scumm/file.h"
+#include "scumm/insane/rebel2/mac_archive.h"
+
+namespace Scumm {
+
+// RA2 uses the SCUMM Mac container format, but its members are flat names
+// rather than the DOS paths used by its movies, fonts and sounds.
+class Rebel2MacArchive : public Common::Archive {
+public:
+	Rebel2MacArchive(const ScummEngine *vm, const Common::Path &filename) : _vm(vm), _filename(filename) {}
+	bool load();
+	bool hasFile(const Common::Path &path) const override;
+	int listMembers(Common::ArchiveMemberList &list) const override;
+	const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
+	Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
+
+private:
+	const ScummEngine *_vm;
+	Common::Path _filename;
+	Common::Array<Common::String> _members;
+};
+
+bool Rebel2MacArchive::load() {
+	ScummFile file(_vm);
+	if (!file.open(_filename) || file.size() < 8)
+		return false;
+
+	const uint32 offset = file.readUint32BE();
+	const uint32 length = file.readUint32BE();
+	if (length % 40 || uint64(offset) + length > uint64(file.size()) || !file.seek(offset))
+		return false;
+
+	// Index the names before mounting the archive. hasFile() must not open
+	// the bundle through SearchMan, which will also search this archive.
+	for (uint32 i = 0; i < length / 40; ++i) {
+		const uint32 start = file.readUint32BE();
+		const uint32 size = file.readUint32BE();
+		char name[33];
+		if (file.read(name, 32) != 32 || uint64(start) + size > uint64(file.size()))
+			return false;
+		name[32] = 0;
+		if (!name[0])
+			return false;
+		_members.push_back(name);
+	}
+	return !file.err();
+}
+
+bool Rebel2MacArchive::hasFile(const Common::Path &path) const {
+	const Common::String name = path.baseName();
+	for (const Common::String &member : _members) {
+		if (member.equalsIgnoreCase(name))
+			return true;
+	}
+	return false;
+}
+
+int Rebel2MacArchive::listMembers(Common::ArchiveMemberList &list) const {
+	for (const Common::String &member : _members)
+		list.push_back(Common::ArchiveMemberPtr(new Common::GenericArchiveMember(member, *this)));
+	return _members.size();
+}
+
+const Common::ArchiveMemberPtr Rebel2MacArchive::getMember(const Common::Path &path) const {
+	if (!hasFile(path))
+		return Common::ArchiveMemberPtr();
+	return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this));
+}
+
+Common::SeekableReadStream *Rebel2MacArchive::createReadStreamForMember(const Common::Path &path) const {
+	if (!hasFile(path))
+		return nullptr;
+
+	ScummFile *file = new ScummFile(_vm);
+	if (file->open(_filename) && file->openSubFile(Common::Path(path.baseName())))
+		return file;
+	delete file;
+	return nullptr;
+}
+
+Common::Archive *createRebel2MacArchive(const ScummEngine *vm, const Common::Path &filename) {
+	Rebel2MacArchive *archive = new Rebel2MacArchive(vm, filename);
+	if (archive->load())
+		return archive;
+	delete archive;
+	return nullptr;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/mac_archive.h b/engines/scumm/insane/rebel2/mac_archive.h
new file mode 100644
index 00000000000..23a9e09738b
--- /dev/null
+++ b/engines/scumm/insane/rebel2/mac_archive.h
@@ -0,0 +1,39 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_INSANE_REBEL2_MAC_ARCHIVE_H
+#define SCUMM_INSANE_REBEL2_MAC_ARCHIVE_H
+
+#include "common/path.h"
+
+namespace Common {
+class Archive;
+}
+
+namespace Scumm {
+
+class ScummEngine;
+
+Common::Archive *createRebel2MacArchive(const ScummEngine *vm, const Common::Path &filename);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 91d08b83082..c4c591033b0 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -22,6 +22,7 @@
 
 
 #include "engines/engine.h"
+#include "common/archive.h"
 #include "common/system.h"
 #include "common/config-manager.h"
 #include "common/events.h"
@@ -45,6 +46,7 @@
 #include "scumm/smush/rebel/font_rebel2.h"
 
 #include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel2/mac_archive.h"
 #include "scumm/insane/rebel2/shared.h"
 
 #include "common/config-manager.h"
@@ -162,6 +164,13 @@ bool InsaneRebel2::isSkippableVideoState() const {
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) :
 		_release(getRebel2Release(scumm->_game.variant, ConfMan.getBool("rebel2_restored_content"))) {
 	_vm = scumm;
+	if (_release.container) {
+		Common::Archive *archive = createRebel2MacArchive(_vm, _release.container);
+		if (!archive)
+			error("Cannot open Rebel Assault II data bundle '%s'", _release.container);
+		SearchMan.add("rebel2-mac-data", archive, 1);
+	}
+
 	// Rebel Assault II skips ScummEngine::resetScumm(), which normally clears this state.
 	for (int i = 0; i < kScummActionCount; i++)
 		_vm->_actionMap[i] = false;
@@ -627,6 +636,9 @@ InsaneRebel2::~InsaneRebel2() {
 		free(_rebelEmbeddedHud[i].pixels);
 		_rebelEmbeddedHud[i].pixels = nullptr;
 	}
+
+	if (_release.container)
+		SearchMan.remove("rebel2-mac-data");
 }
 
 bool InsaneRebel2::isHiRes() const {
diff --git a/engines/scumm/insane/rebel2/releases.cpp b/engines/scumm/insane/rebel2/releases.cpp
index 080e672d815..29488f52c06 100644
--- a/engines/scumm/insane/rebel2/releases.cpp
+++ b/engines/scumm/insane/rebel2/releases.cpp
@@ -28,7 +28,9 @@ namespace Scumm {
 // DA1.06's difficulty table starts at offset 0x4798 in LE object 3.
 // Rows have 17 words; each difficulty occupies 0x242 bytes. Types 5 and 6
 // cover the shield attack and reactor attack respectively.
-static const Rebel2DifficultyOverride kDemoDA106Difficulty[] = {
+// Mac v1.0 has the same combat values at 0x5a70 in its unpacked PEF data
+// section. Its unused flight columns are -1; neither section uses flight controls.
+static const Rebel2DifficultyOverride kChapter6DemoDifficulty[] = {
 	{ 5, {
 		{ 5, 5, -1, 19, 2, 15, 75, 25, 50, 2, 500, -1, 8, -1, -1, -1, -1 },
 		{ 5, 3, -1, 25, 4, 15, 75, 50, 100, 4, 1000, -1, 16, -1, -1, -1, -1 },
@@ -51,26 +53,32 @@ static const Rebel2DifficultyOverride kDemoDA106Difficulty[] = {
 // saved pilot progress and the unlock-all option.
 static const Rebel2Release kReleases[] = {
 	{
-		"", "SYSTM/GAME.TRS", nullptr, nullptr,
+		"", nullptr, "SYSTM/GAME.TRS", nullptr, nullptr,
 		{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
 		true, false, false, false, nullptr, 0
 	},
 	{
-		"Demo", "SYSTM/GAME.TRS", "OPEN/O_DEMO.SAN", nullptr, { 0 },
+		"Demo", nullptr, "SYSTM/GAME.TRS", "OPEN/O_DEMO.SAN", nullptr, { 0 },
 		false, false, false, false, nullptr, 0
 	},
 	{
 		// RBL2DEMO.EXE selects GAME_E.TRS. The original chapter-6 handler
 		// (LE object 1, 0x10d10) skips the shield attack and uses 06END_B.
-		"Demo DA1.06", "SYSTM/GAME_E.TRS", nullptr, nullptr, { 6 },
-		false, true, true, false, kDemoDA106Difficulty, ARRAYSIZE(kDemoDA106Difficulty)
+		"Demo DA1.06", nullptr, "SYSTM/GAME_E.TRS", nullptr, nullptr, { 6 },
+		false, true, true, false, kChapter6DemoDifficulty, ARRAYSIZE(kChapter6DemoDifficulty)
 	},
 	{
 		// GAME.TRS selects chapters 1, 2 and 4 in playable-demo mode.
 		// LE object 1, 0xf860 advances passwords past missing chapters;
 		// 0x12eb0 plays O_PLAYDE instead of the retail finale.
-		"Demo DG1.15", "SYSTM/GAME.TRS", nullptr, "OPEN/O_PLAYDE.SAN", { 1, 2, 4 },
+		"Demo DG1.15", nullptr, "SYSTM/GAME.TRS", nullptr, "OPEN/O_PLAYDE.SAN", { 1, 2, 4 },
 		false, false, false, true, nullptr, 0
+	},
+	{
+		// Macintosh v1.0's chapter-6 handler (PEF code section, 0x17fe8)
+		// also skips the shield attack in demo mode and uses 06END_B.
+		"Demo v1.0", "Rebel Assault II Demo Data", "SYSTM/GAME_E.TRS", nullptr, nullptr, { 6 },
+		false, true, true, false, kChapter6DemoDifficulty, ARRAYSIZE(kChapter6DemoDifficulty)
 	}
 };
 
diff --git a/engines/scumm/insane/rebel2/releases.h b/engines/scumm/insane/rebel2/releases.h
index f979c1ba12a..a5049575842 100644
--- a/engines/scumm/insane/rebel2/releases.h
+++ b/engines/scumm/insane/rebel2/releases.h
@@ -55,6 +55,7 @@ struct Rebel2Release {
 	enum { kNumLevels = 15, kFinale = 16 };
 
 	const char *variant;
+	const char *container;
 	const char *strings;
 	const char *nonInteractiveVideo;
 	const char *completionVideo;
@@ -62,7 +63,7 @@ struct Rebel2Release {
 	byte levels[kNumLevels];
 	bool ending;
 	bool unlockAvailableLevels;
-	// The DA1.06 demo starts chapter 6 at the attack on the reactor.
+	// The chapter-6 demos start at the attack on the reactor.
 	bool skipMiningFacilityAttack;
 	bool advanceCompletionPasswords;
 	const Rebel2DifficultyOverride *difficultyOverrides;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index c8ab8c301ff..b0f7e3eb3aa 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -164,6 +164,7 @@ MODULE_OBJS += \
 	insane/rebel2/audio.o \
 	insane/rebel2/iact.o \
 	insane/rebel2/levels.o \
+	insane/rebel2/mac_archive.o \
 	insane/rebel2/menu.o \
 	insane/rebel2/render.o \
 	insane/rebel2/runlevels.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 2c40e90565f..bb445084e06 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Sun Sep 27 12:52:41 2026
+  This file was generated by the md5table tool on Sun Sep 27 13:12:00 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -716,6 +716,7 @@ static const MD5Table md5table[] = {
 	{ "d7ab7cd6105546016e6a0d46fb36b964", "pajama", "HE 100", "Demo", 18958, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "d7b247c26bf1f01f8f7daf142be84de3", "balloon", "HE 99", "Updated", -1, Common::EN_ANY, Common::kPlatformWindows },
 	{ "d8323015ecb8b10bf53474f6e6b0ae33", "dig", "", "", 16304, Common::UNK_LANG, Common::kPlatformUnknown },
+	{ "d89f31e209c0642b4d20ccf615840401", "rebel2", "Demo v1.0", "Demo v1.0", 128658792, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "d917f311a448e3cc7239c31bddb00dd2", "samnmax", "", "CD", 9080, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "d93cc8be628ed5d3b3a29188fc7105d3", "dig", "Steam", "Steam", 1061296, Common::UNK_LANG, Common::kPlatformMacintosh },
 	{ "d9d0dd93d16ab4dec55cabc2b86bbd17", "samnmax", "", "Demo", 6478, Common::EN_ANY, Common::kPlatformDOS },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index f4ec7c544a2..75040205bc9 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1188,6 +1188,9 @@ Common::Error ScummEngine::init() {
 			_filenamePattern.genMethod = kGenRoomNum;
 		} else if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2) {
 			_fileHandle = new ScummFile(this);
+			// RA2 must disable the SCUMM GUI before Mac screen initialization.
+			if (_game.id == GID_REBEL2)
+				_useOriginalGUI = false;
 		} else if (_game.platform == Common::kPlatformMacintosh) {
 			// The mac versions of Indy4, Sam&Max, DOTT, FT and The Dig used a
 			// special meta (container) file format to store the actual SCUMM data
@@ -1876,8 +1879,6 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 
 		setupScummVars();
 
-		_useOriginalGUI = false;
-
 		_sound = new Sound(this, _mixer, false);
 		// Rebel Assault 2 doesn't use iMUSE for audio.
 		_musicEngine = _imuseDigital = nullptr;




More information about the Scummvm-git-logs mailing list