[Scummvm-git-logs] scummvm master -> 572e7ace90e5157e9540623c63dda7760dd8899a

neuromancer noreply at scummvm.org
Wed Jul 22 20:28:00 UTC 2026


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

Summary:
bc003485a1 SCUMM: RA2: added language-specific detection code
e6927b61f3 SCUMM: RA2: use high resolution mode by default
f50c2afe71 SCUMM: RA1: added language-specific detection code
a675548a73 SCUMM: RA2: added spanish release detection code
f522260a58 SCUMM: RA1: added support for 3DO release
4b7d5e6348 SCUMM: RA1: initial support for the SegaCD release
583f57338b SCUMM: RA1: skip L7 in SegaCD as there are is such mission
572e7ace90 SCUMM: RA1: magenta entries inherit the previous Sega palette


Commit: bc003485a1869ff4d80ae443a910884dba3b067a
    https://github.com/scummvm/scummvm/commit/bc003485a1869ff4d80ae443a910884dba3b067a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA2: added language-specific detection code

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection_internal.h
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 72b3a55490b..e7625327a42 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -455,7 +455,7 @@ rebel1	Star Wars: Rebel Assault
 	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-	
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
-	6b73a08c535c0544785d73ff812908a0	9430	en	DOS	-	-	-	-
+	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
 
 	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
 
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index 4b1258592e8..60d0c2f64f2 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -239,6 +239,30 @@ static Common::Language detectLanguage(const Common::FSList &fslist, byte id, co
 			return Common::KO_KOR;
 		}
 
+		if (id == GID_REBEL2) {
+			Common::FSNode systmDir;
+			Common::FSList systmList;
+			Common::File trs;
+			if (searchFSNode(fslist, "SYSTM", systmDir)
+				&& systmDir.isDirectory()
+				&& systmDir.getChildren(systmList, Common::FSNode::kListFilesOnly)
+				&& searchFSNode(systmList, "GAME.TRS", langFile)
+				&& trs.open(langFile)) {
+				switch (trs.size()) {
+				case 46746:	// d9aced0c3fcb8f6a0045dcd4cbf12590
+					return Common::EN_ANY;
+				case 48097:	// 66353d7250f680b28992459c355caa17
+					return Common::IT_ITA;
+				case 50094:	// 004fb2fd15f84a1f81cc362d73811c9c
+					return Common::DE_DEU;
+				case 60976:	// c53823d48beca122c45a83d35027a0e7
+					return Common::FR_FRA;
+				default:
+					break;
+				}
+			}
+		}
+
 		return originalLanguage;
 	}
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index ec658bb719f..24c4c65b281 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 Wed Jun 24 21:59:49 2026
+  This file was generated by the md5table tool on Wed Jul 22 12:10:11 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -373,7 +373,7 @@ static const MD5Table md5table[] = {
 	{ "6b27dbcd8d5697d5c918eeca0f68ef6a", "puttrace", "HE CUP", "Preview", 3901484, Common::UNK_LANG, Common::kPlatformUnknown },
 	{ "6b3ec67da214f558dc5ceaa2acd47453", "indy3", "EGA", "EGA", 5361, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6b5a3fef241e90d4b2e77f1e222773ee", "maniac", "NES", "", 2082, Common::SV_SWE, Common::kPlatformNES },
-	{ "6b73a08c535c0544785d73ff812908a0", "rebel2", "", "", 9430, Common::EN_ANY, Common::kPlatformDOS },
+	{ "6b73a08c535c0544785d73ff812908a0", "rebel2", "", "", 9430, Common::UNK_LANG, Common::kPlatformDOS },
 	{ "6bb0fe2d7816c5528c773f2898e4e0ff", "rebel1", "", "v1.7", 212915, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6bca7a1a96d16e52b8f3c42b50dbdca3", "fbear", "HE 62", "", -1, Common::JA_JPN, Common::kPlatform3DO },
 	{ "6bf70eee5de3d24d2403e0dd3d267e8a", "spyfox", "", "", 49221, Common::EN_USA, Common::kPlatformWindows },


Commit: e6927b61f3ac1c9eec47ee56130ed51bfeb927fe
    https://github.com/scummvm/scummvm/commit/e6927b61f3ac1c9eec47ee56130ed51bfeb927fe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA2: use high resolution mode by default

Changed paths:
    engines/scumm/metaengine.cpp


diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 3ed790a2d3c..b84c9233dc1 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -882,7 +882,7 @@ static const ExtraGuiOption enableRebel2HiRes = {
 	_s("High resolution mode"),
 	_s("Run the game in 640x400 high resolution mode instead of 320x200"),
 	"rebel2_hires",
-	false,
+	true,
 	0,
 	0
 };


Commit: f50c2afe7193809aa47274a33e970692adf136da
    https://github.com/scummvm/scummvm/commit/f50c2afe7193809aa47274a33e970692adf136da
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA1: added language-specific detection code

Changed paths:
    devtools/scumm-md5.txt
    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/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index e7625327a42..885e47dabdb 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -452,7 +452,9 @@ ft	Full Throttle
 rebel1	Star Wars: Rebel Assault
 	b9ac90e4411da1b6f7dd075cc4b03c2a	214435	en	DOS	-	v1.0	-	-
 	e210a57b756473be705d24b9378a8233	212383	en	DOS	-	v1.3	-	-
-	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-	
+	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-
+	02236dcfb0cac59457153fc296233825	213171	fr	DOS	-	v1.7	-	-
+	b978b92affdfe5744711ca0cb3548720	213115	de	DOS	-	v1.7	-	-
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 6947fb4f0a9..0f55b7da6ec 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -1009,8 +1009,9 @@ void InsaneRebel1::drawMenuTitleText(byte *dst, int pitch, int width, int height
 
 void InsaneRebel1::renderHighScoresOverlay(byte *dst, int pitch, int width, int height) {
 	// entries fade in one per frame. We show all immediately.
-	const int titleW = getMenuTalkTextWidth("TOP PILOTS");
-	drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
+	const char *title = uiStr(kR1StrTopPilots);
+	const int titleW = getMenuTalkTextWidth(title);
+	drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 10, title);
 
 	for (int i = 0; i < kHighScoreCount; i++) {
 		const int y = 25 + i * 14;
@@ -1029,26 +1030,24 @@ void InsaneRebel1::renderOptionsOverlay(byte *dst, int pitch, int width, int hei
 	_optTextEnabled = ConfMan.getBool("subtitles");
 	_optVolume = CLIP<int>(ConfMan.getInt("music_volume") / 2, 0, 127);
 
-	const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
-
-	const int titleW = getFontBankStringWidth("GAME OPTIONS");
-	drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
+	const char *title = uiStr(kR1StrOptionsTitle);
+	const int titleW = getFontBankStringWidth(title);
+	drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, title);
 
 	// Build dynamic option strings for each row
-	char diffLine[64], volLine[64];
-	Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
-	Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
+	char volLine[64];
+	Common::sprintf_s(volLine, uiStr(kR1StrOptVolumeFmt), (_optVolume * 100) / 127);
 
 	const char *optItems[kOptionsItemCount] = {
-		"EXIT MENU",
-		_optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
-		_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
-		_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
-		_optTextEnabled   ? "DIALOGUE TEXT IS ON"     : "DIALOGUE TEXT IS OFF",
-		_optControlsYFlip ? "Y AXIS IS INVERTED"      : "Y AXIS IS NORMAL",
+		uiStr(kR1StrOptExitMenu),
+		uiStr(_optRookieOneFemale ? kR1StrOptRookieFemale : kR1StrOptRookieMale),
+		uiStr(_optMusicEnabled  ? kR1StrOptMusicOn : kR1StrOptMusicOff),
+		uiStr(_optSfxEnabled    ? kR1StrOptSfxOn   : kR1StrOptSfxOff),
+		uiStr(_optTextEnabled   ? kR1StrOptTextOn  : kR1StrOptTextOff),
+		uiStr(_optControlsYFlip ? kR1StrOptYFlipped : kR1StrOptYNormal),
 		_optRapidFire     ? "RAPID FIRE IS ON"        : "RAPID FIRE IS OFF",
 		volLine,
-		diffLine
+		uiStr(kR1StrOptDiffEasy + CLIP(_difficulty, 0, 2))
 	};
 
 	for (int i = 0; i < kOptionsItemCount; i++) {
@@ -1114,27 +1113,28 @@ void InsaneRebel1::renderLevelSelectOverlay(byte *dst, int pitch, int width, int
 }
 
 void InsaneRebel1::renderMainMenuItems(byte *dst, int pitch, int width, int height) {
-	const char *const kMenuItems[kRA1MainMenuItemCount] = {
-		"START NEW GAME",
-		"GAME OPTIONS",
-		"ENTER PASSCODE",
+	const char *kMenuItems[kRA1MainMenuItemCount] = {
+		uiStr(kR1StrMenuNewGame),
+		uiStr(kR1StrMenuGameOptions),
+		uiStr(kR1StrMenuEnterPasscode),
 		"LEVEL SELECT",
-		"CONTINUE DEMO",
-		"EXIT TO DOS"
+		uiStr(kR1StrMenuContinueDemo),
+		uiStr(kR1StrMenuExitToDos)
 	};
-	const char *const kMenuItemsLocked[kRA1MainMenuItemCount - 1] = {
-		"START NEW GAME",
-		"GAME OPTIONS",
-		"ENTER PASSCODE",
-		"CONTINUE DEMO",
-		"EXIT TO DOS"
+	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 int titleW = getFontBankStringWidth("MAIN MENU");
+	const char *menuTitle = uiStr(kR1StrMainMenuTitle);
+	const int titleW = getFontBankStringWidth(menuTitle);
 	const int titleX = getRebel1MenuCenteredX(titleW);
-	drawMenuTitleText(dst, pitch, width, height, titleX, 30, "MAIN MENU");
+	drawMenuTitleText(dst, pitch, width, height, titleX, 30, menuTitle);
 
 	for (int i = 0; i < mainMenuItemCount; i++) {
 		const int textW = getMenuTalkTextWidth(menuItems[i]);
@@ -1194,13 +1194,13 @@ void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int h
 	};
 
 	if (_textEntryPasscodeMode) {
-		drawCenteredTalkText(0x4b, "ENTER PASSCODE");
+		drawCenteredTalkText(0x4b, uiStr(kR1StrEnterPasscodeTitle));
 		drawCenteredTalkText(0x5f, _textEntryBuffer);
 	} else {
 		char scoreText[40];
 		Common::sprintf_s(scoreText, "SCORE: %ld", (long)_score);
 		drawCenteredTalkText(0x37, scoreText);
-		drawCenteredTalkText(0x4b, "NEW HIGH SCORE");
+		drawCenteredTalkText(0x4b, uiStr(kR1StrNewHighScore));
 		drawCenteredTalkText(0x5f, _textEntryBuffer);
 	}
 
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 5ec62f2f5cb..d3ab2cde2d0 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/config-manager.h"
+#include "common/file.h"
 #include "common/system.h"
 #include "common/events.h"
 #include "common/endian.h"
@@ -208,6 +209,257 @@ void InsaneRebel1::resetGameplayFlagsFromTuning() {
 	_gameplayFlags75ff = tuningFlags >> 8;
 }
 
+// English fallbacks for the UI strings normally extracted from ASSAULT.EXE.
+// Order must match Rebel1UiStringId in rebel.h.
+static const char *const kRebel1UiFallback[kR1StrUiCount] = {
+	"MAIN MENU",
+	"START NEW GAME",
+	"GAME OPTIONS",
+	"ENTER PASSCODE",
+	"CONTINUE DEMO",
+	"EXIT TO DOS",
+	"TOP PILOTS",
+	"ENTER PASSCODE",
+	"NEW HIGH SCORE",
+	"GAME OPTIONS",
+	"EXIT MENU",
+	"ROOKIE1 IS FEMALE",
+	"ROOKIE1 IS MALE",
+	"MUSIC IS ON",
+	"MUSIC IS OFF",
+	"SFX AND VOICE ARE ON",
+	"SFX AND VOICE ARE OFF",
+	"DIALOGUE TEXT IS ON",
+	"DIALOGUE TEXT IS OFF",
+	"Y AXIS IS INVERTED",
+	"Y AXIS IS NORMAL",
+	"VOLUME AT %hd PERCENT",
+	"DIFFICULTY IS EASY",
+	"DIFFICULTY IS NORMAL",
+	"DIFFICULTY IS HARD",
+	"Chapter Complete",
+	"Completion bonus: %d",
+	"Bonus: %d",
+	"Password: %s",
+	"Path Taken: Hard",
+	"Path Taken: Easy",
+	"Target Accuracy: Perfect",
+	"Target Accuracy: %d percent",
+	"Part II",
+	"Part I",
+	"Torpedo Hit",
+	"Torpedo Missed",
+	"Torpedo on mark",
+	"<<SHOOT TARGETS FOR BONUS",
+	"<<WALKER %d%%",
+	"<<TIME %d"
+};
+
+// The printf conversions used by a format string ("%%" doesn't count), e.g.
+// "hd" for "<VOLUME AT %hd PERCENT". Used to reject extracted strings whose
+// conversions don't match the fallback's before they reach sprintf.
+static Common::String rebel1FormatSpecs(const char *s) {
+	Common::String out;
+	for (const char *p = s; *p; p++) {
+		if (*p != '%')
+			continue;
+		p++;
+		if (*p == '%' || *p == '\0')
+			continue;
+		while (*p == 'h' || *p == 'l')
+			out += *p++;
+		if (*p)
+			out += *p;
+	}
+	return out;
+}
+
+const char *InsaneRebel1::uiStr(int id) const {
+	if (id < 0 || id >= kR1StrUiCount)
+		return "";
+	if (!_uiStrings[id].empty())
+		return _uiStrings[id].c_str();
+	return kRebel1UiFallback[id];
+}
+
+// All localized DOS releases keep their UI text in ASSAULT.EXE 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.
+void InsaneRebel1::loadLocalizedUiStrings() {
+	Common::File f;
+	if (!f.open("ASSAULT.EXE"))
+		return;
+
+	int32 size = (int32)f.size();
+	if (size <= 0 || size > 2 * 1024 * 1024)
+		return;
+
+	byte *data = new byte[size];
+	if (f.read(data, size) != (uint32)size) {
+		delete[] data;
+		return;
+	}
+
+	// Collect NUL-terminated printable strings (DOS codepage bytes allowed).
+	Common::Array<Common::String> strs;
+	int32 runStart = -1;
+	for (int32 i = 0; i < size; i++) {
+		const byte b = data[i];
+		const bool printable = (b >= 0x20 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE);
+		if (printable) {
+			if (runStart < 0)
+				runStart = i;
+		} else {
+			if (b == 0 && runStart >= 0 && i - runStart >= 2)
+				strs.push_back(Common::String((const char *)data + runStart, i - runStart));
+			runStart = -1;
+		}
+	}
+	delete[] data;
+
+	auto find = [&strs](const Common::String &needle, int from = 0) -> int {
+		for (uint i = (uint)MAX(from, 0); i < strs.size(); i++)
+			if (strs[i] == needle)
+				return (int)i;
+		return -1;
+	};
+
+	auto assign = [&](int id, int idx) -> bool {
+		if (id < 0 || id >= kR1StrUiCount || idx < 0 || idx >= (int)strs.size())
+			return false;
+		Common::String s = strs[idx];
+		const char *fb = kRebel1UiFallback[id];
+		if (fb[0] == '<') {
+			// HUD strings keep their '<' layer markup.
+			if (s[0] != '<')
+				return false;
+		} else {
+			while (!s.empty() && s[0] == '<')
+				s.deleteChar(0);
+		}
+		if (s.size() < 2 || s.size() > 60)
+			return false;
+		if (s.contains('\\'))
+			return false;
+		if (rebel1FormatSpecs(s.c_str()) != rebel1FormatSpecs(fb))
+			return false;
+		_uiStrings[id] = s;
+		return true;
+	};
+
+	// Main menu: title and the five original items follow OPEN\O1OPTION.ANM.
+	int idx = find("OPEN\\O1OPTION.ANM");
+	if (idx >= 0) {
+		assign(kR1StrMainMenuTitle, idx + 1);
+		for (int i = 0; i < 5; i++)
+			assign(kR1StrMenuNewGame + i, idx + 2 + i);
+	}
+
+	// High score table title follows OPEN\O1SCORE.ANM.
+	idx = find("OPEN\\O1SCORE.ANM");
+	if (idx >= 0)
+		assign(kR1StrTopPilots, idx + 1);
+
+	// The joystick calibration coordinate format "(%d,%d)" sits between the
+	// passcode/calibration strings and the options menu block.
+	const int calib = find("OPEN\\O1CALIB.ANM");
+	int paren = (calib >= 0) ? find("(%d,%d)", calib) : -1;
+	if (paren >= 0 && paren > calib + 40)
+		paren = -1;
+	if (paren >= 0) {
+		// 4 calibration instructions and the screen title precede it.
+		assign(kR1StrEnterPasscodeTitle, paren - 6);
+
+		int j = paren + 1;
+		while (j < (int)strs.size() && strs[j].hasPrefix("<<"))
+			j++;
+		if (assign(kR1StrOptionsTitle, j)) {
+			for (int i = 0; i < 15 && j + 1 + i < (int)strs.size(); i++) {
+				if (!strs[j + 1 + i].hasPrefix("<"))
+					break;
+				assign(kR1StrOptExitMenu + i, j + 1 + i);
+			}
+		}
+	}
+
+	// Chapter summary block: the only "...: %s" string is the password line,
+	// preceded by the title and the two bonus formats.
+	int pw = -1;
+	for (uint i = 0; i < strs.size(); i++) {
+		const Common::String &s = strs[i];
+		if (s.size() >= 7 && s.hasSuffix(": %s") && !s.contains('\\') && !s.hasPrefix("<") &&
+				rebel1FormatSpecs(s.c_str()) == "s") {
+			if (pw >= 0) {
+				pw = -1;  // ambiguous
+				break;
+			}
+			pw = (int)i;
+		}
+	}
+	if (pw >= 0) {
+		assign(kR1StrPasswordFmt, pw);
+		assign(kR1StrChapterComplete, pw - 1);
+		assign(kR1StrBonusFmt, pw - 2);
+		assign(kR1StrCompletionBonusFmt, pw - 3);
+		// v1.7 keeps NEW HIGH SCORE with this block; v1.0 keeps it with the
+		// calibration block.
+		if (!assign(kR1StrNewHighScore, pw - 4) && paren >= 0)
+			assign(kR1StrNewHighScore, paren - 7);
+	}
+
+	// Level result strings, each anchored on the video played before them.
+	idx = find("LVL1\\L1END.ANM");
+	if (idx >= 0) {
+		assign(kR1StrPathHard, idx + 1);
+		assign(kR1StrPathEasy, idx + 2);
+		assign(kR1StrAccuracyPerfect, idx + 3);
+		assign(kR1StrAccuracyFmt, idx + 4);
+		assign(kR1StrPartII, idx + 5);
+		assign(kR1StrPartI, idx + 6);
+	}
+	idx = find("LVL4\\L4END2.ANM");
+	if (idx >= 0) {
+		assign(kR1StrTorpedoHit, idx + 1);
+		assign(kR1StrTorpedoMissed, idx + 2);
+	}
+	idx = find("LVL5\\L5PLAY2.ANM");
+	if (idx >= 0)
+		assign(kR1StrShootTargets, idx + 1);
+	idx = find("LVL8\\L8PLAY.ANM");
+	if (idx >= 0) {
+		assign(kR1StrWalkerFmt, idx + 1);
+		assign(kR1StrTimeFmt, idx + 2);
+	}
+	idx = find("LVL15\\L15END1.ANM");
+	if (idx >= 0)
+		assign(kR1StrTorpedoOnMark, idx + 1);
+
+	// Chapter title cards: title and "Chapter N" label follow the chapter's
+	// intro video name.
+	for (int n = 1; n <= 15; n++) {
+		Common::String anchor = (n == 1) ? Common::String("LVL1\\L1HANGAR.ANM")
+			: Common::String::format("LVL%d\\L%dINTRO.ANM", n, n);
+		idx = find(anchor);
+		if (idx < 0 || idx + 2 >= (int)strs.size())
+			continue;
+		const Common::String &title = strs[idx + 1];
+		const Common::String &label = strs[idx + 2];
+		if (title.size() >= 2 && title.size() <= 40 && !title.contains('\\') &&
+				!title.contains('%') && title[0] != '<')
+			_chapterTitles[n - 1] = title;
+		if (label.size() >= 2 && label.size() <= 40 && !label.contains('\\') &&
+				!label.contains('%') && label[0] != '<')
+			_chapterLabels[n - 1] = label;
+	}
+
+	int loaded = 0;
+	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);
+}
+
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	Insane::_vm = scumm;
 
@@ -435,6 +687,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		warning("InsaneRebel1::InsaneRebel1(): failed to load targeting font bank (TECHFONT/TALKFONT)");
 	}
 
+	loadLocalizedUiStrings();
+
 	initAudio(11025);
 	memset(_sfxData, 0, sizeof(_sfxData));
 	memset(_sfxSize, 0, sizeof(_sfxSize));
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index bf7f6969681..3dd4d01e138 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -51,6 +51,53 @@ enum RA1MenuCommand {
 	kRA1MenuCommandSelect6
 };
 
+// UI strings extracted from ASSAULT.EXE so localized releases show their own
+// text. Order must match kRebel1UiFallback in rebel.cpp.
+enum Rebel1UiStringId {
+	kR1StrMainMenuTitle = 0,
+	kR1StrMenuNewGame,
+	kR1StrMenuGameOptions,
+	kR1StrMenuEnterPasscode,
+	kR1StrMenuContinueDemo,
+	kR1StrMenuExitToDos,
+	kR1StrTopPilots,
+	kR1StrEnterPasscodeTitle,
+	kR1StrNewHighScore,
+	kR1StrOptionsTitle,
+	kR1StrOptExitMenu,
+	kR1StrOptRookieFemale,
+	kR1StrOptRookieMale,
+	kR1StrOptMusicOn,
+	kR1StrOptMusicOff,
+	kR1StrOptSfxOn,
+	kR1StrOptSfxOff,
+	kR1StrOptTextOn,
+	kR1StrOptTextOff,
+	kR1StrOptYFlipped,
+	kR1StrOptYNormal,
+	kR1StrOptVolumeFmt,
+	kR1StrOptDiffEasy,
+	kR1StrOptDiffNormal,
+	kR1StrOptDiffHard,
+	kR1StrChapterComplete,
+	kR1StrCompletionBonusFmt,
+	kR1StrBonusFmt,
+	kR1StrPasswordFmt,
+	kR1StrPathHard,
+	kR1StrPathEasy,
+	kR1StrAccuracyPerfect,
+	kR1StrAccuracyFmt,
+	kR1StrPartII,
+	kR1StrPartI,
+	kR1StrTorpedoHit,
+	kR1StrTorpedoMissed,
+	kR1StrTorpedoOnMark,
+	kR1StrShootTargets,
+	kR1StrWalkerFmt,
+	kR1StrTimeFmt,
+	kR1StrUiCount
+};
+
 // Sprite bank for RA1 NUT files.
 struct RA1Sprite {
 	int16 xoffs;
@@ -385,6 +432,14 @@ private:
 	void beginLevelTitleOverlay(int level);
 	void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
 
+	// Localized UI text, loaded from ASSAULT.EXE (falls back to English).
+	Common::String _uiStrings[kR1StrUiCount];
+	Common::String _chapterLabels[15];
+	Common::String _chapterTitles[15];
+	void loadLocalizedUiStrings();
+	const char *uiStr(int id) const;
+	void formatTargetAccuracy(char *dst, size_t dstSize, int kills, int targetCount, bool perfectText) const;
+
 	static const int kChapterSummaryTextSize = 80;
 	struct ChapterSummaryState {
 		bool active;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 9a34d3975dd..4ee2a284189 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -997,10 +997,11 @@ void InsaneRebel1::renderLevel5Part2Overlay(byte *dst, int pitch, int width, int
 	overlayY = (int16)(0x8C - ((overlayY - 0x8C) >> 2));
 
 	if (curFrame < 0x32) {
-		const char instructionText[] = "<<SHOOT TARGETS FOR BONUS";
-		const int revealChars = CLIP<int>((int)curFrame, 0, (int)sizeof(instructionText) - 1);
-		char revealedText[sizeof(instructionText)];
-		Common::strlcpy(revealedText, instructionText, revealChars + 1);
+		const char *instructionText = uiStr(kR1StrShootTargets);
+		const int instructionLen = (int)strlen(instructionText);
+		const int revealChars = CLIP<int>((int)curFrame, 0, instructionLen);
+		char revealedText[64];
+		Common::strlcpy(revealedText, instructionText, MIN<int>(revealChars + 1, sizeof(revealedText)));
 		drawFontBankString(dst, pitch, width, height,
 			viewportX + overlayX - 0x27, viewportY + overlayY, revealedText);
 	} else {
@@ -1365,8 +1366,10 @@ void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int he
 		return;
 	}
 
-	const char *title = kLevelTitles[_introTextLevel].titleText;
-	const char *subtitle = kLevelTitles[_introTextLevel].subtitleText;
+	const char *title = _chapterLabels[_introTextLevel].empty() ?
+		kLevelTitles[_introTextLevel].titleText : _chapterLabels[_introTextLevel].c_str();
+	const char *subtitle = _chapterTitles[_introTextLevel].empty() ?
+		kLevelTitles[_introTextLevel].subtitleText : _chapterTitles[_introTextLevel].c_str();
 
 	int titleW = getFontBankStringWidth(title);
 	int subtitleW = getFontBankStringWidth(subtitle);
@@ -1427,18 +1430,18 @@ void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, in
 	const int centerX = width / 2;
 	const int titleChars = MAX<int>(0, (int)(curFrame - revealBaseFrame));
 	drawCenteredRebel1String(this, dst, pitch, width, height,
-		centerX, 5, "Chapter Complete", titleChars);
+		centerX, 5, uiStr(kR1StrChapterComplete), titleChars);
 
 	if (revealBaseFrame + 0x0F < curFrame) {
-		char completionText[40];
-		Common::sprintf_s(completionText, "Completion bonus: %d", (int)_tuning.levelPts);
+		char completionText[64];
+		Common::sprintf_s(completionText, uiStr(kR1StrCompletionBonusFmt), (int)_tuning.levelPts);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x19, completionText);
 	}
 
 	if (_chapterSummary.hasBonus1 &&
 			revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
-		char bonusText[32];
-		Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue1);
+		char bonusText[64];
+		Common::sprintf_s(bonusText, uiStr(kR1StrBonusFmt), _chapterSummary.bonusValue1);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel1);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText1);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
@@ -1446,8 +1449,8 @@ void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, in
 
 	if (_chapterSummary.hasBonus2 &&
 			revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
-		char bonusText[32];
-		Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue2);
+		char bonusText[64];
+		Common::sprintf_s(bonusText, uiStr(kR1StrBonusFmt), _chapterSummary.bonusValue2);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel2);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText2);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
@@ -1457,8 +1460,8 @@ void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, in
 			revealBaseFrame + 10 < curFrame) {
 		const char *password = getChapterCompletePassword(_chapterSummary.passwordIndex);
 		if (password) {
-			char passwordText[40];
-			Common::sprintf_s(passwordText, "Password: %s", password);
+			char passwordText[64];
+			Common::sprintf_s(passwordText, uiStr(kR1StrPasswordFmt), password);
 			drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
 		}
 	}
@@ -1871,8 +1874,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 		projX = (int16)(0x61 - ((projX - 0x61) >> 2));
 		projY = (int16)(0x8D - ((projY - 0x8D) >> 2));
 
-		char walkerStr[24];
-		Common::sprintf_s(walkerStr, "<<WALKER %d%%", (int)_walkerHealth);
+		char walkerStr[64];
+		Common::sprintf_s(walkerStr, uiStr(kR1StrWalkerFmt), (int)_walkerHealth);
 		drawFontBankString(dst, pitch, width, height,
 			viewportX + projX, viewportY + projY, walkerStr);
 	}
@@ -1907,8 +1910,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 		projX = (int16)(0x62 - ((projX - 0x62) >> 2));
 		projY = (int16)(0x9C - ((projY - 0x9C) >> 2));
 
-		char timerStr[16];
-		Common::sprintf_s(timerStr, "<<TIME %d", (int)_walkerTimer);
+		char timerStr[64];
+		Common::sprintf_s(timerStr, uiStr(kR1StrTimeFmt), (int)_walkerTimer);
 		drawFontBankString(dst, pitch, width, height,
 			viewportX + projX, viewportY + projY, timerStr);
 	}
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 347d092a792..29507d3518b 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -125,11 +125,11 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
 	return result;
 }
 
-static void formatTargetAccuracy(char *dst, size_t dstSize, int kills, int targetCount, bool perfectText) {
+void InsaneRebel1::formatTargetAccuracy(char *dst, size_t dstSize, int kills, int targetCount, bool perfectText) const {
 	if (perfectText && kills >= targetCount)
-		Common::sprintf_s(dst, dstSize, "Target Accuracy: Perfect");
+		Common::sprintf_s(dst, dstSize, "%s", uiStr(kR1StrAccuracyPerfect));
 	else
-		Common::sprintf_s(dst, dstSize, "Target Accuracy: %d percent", (kills * 100) / targetCount);
+		Common::sprintf_s(dst, dstSize, uiStr(kR1StrAccuracyFmt), (kills * 100) / targetCount);
 }
 
 static int calculateThresholdBonus(int kills, int perfectThreshold, int perKillThreshold, int perKillBonus) {
@@ -376,14 +376,14 @@ bool InsaneRebel1::runLevel1() {
 					break;
 
 				if (_killCount > 4 || _interactiveVideoCheatSkipped) {
-					const char *pathText = _rightPathSelected ? "Path Taken: Hard" : "Path Taken: Easy";
+					const char *pathText = uiStr(_rightPathSelected ? kR1StrPathHard : kR1StrPathEasy);
 					const int pathBonus = _rightPathSelected ? _tuning.bonus * 3 : 0;
 					char accuracyText[80];
 					formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x0E, true);
 					const int targetBonus = calculateThresholdBonus(_killCount, 0x0D, 5, _tuning.bonus);
 					playChapterCompleteCinematic("LVL1/L1END.ANM", 1, 0x78, 5,
-						"Part I", pathText, pathBonus,
-						"Part II", accuracyText, targetBonus);
+						uiStr(kR1StrPartI), pathText, pathBonus,
+						uiStr(kR1StrPartII), accuracyText, targetBonus);
 					return !shouldAbortGameFlow();
 				}
 
@@ -552,7 +552,7 @@ bool InsaneRebel1::runLevel4() {
 		if (_health >= 0 && shieldGeneratorsDestroyed) {
 			const bool torpedoHit = (_killCount != 0);
 			playChapterCompleteCinematic(torpedoHit ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM",
-				4, 0x69, 5, " ", torpedoHit ? "Torpedo Hit" : "Torpedo Missed",
+				4, 0x69, 5, " ", uiStr(torpedoHit ? kR1StrTorpedoHit : kR1StrTorpedoMissed),
 				torpedoHit ? _tuning.bonus : 0);
 			return !shouldAbortGameFlow();
 		}
@@ -1379,7 +1379,7 @@ bool InsaneRebel1::runLevel15() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, false);
 			playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
 				"Part I", accuracyText, targetBonus,
-				"Part II", "Torpedo on mark", 10000, _difficulty + 13);
+				uiStr(kR1StrPartII), uiStr(kR1StrTorpedoOnMark), 10000, _difficulty + 13);
 			return !shouldAbortGameFlow();
 		}
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 24c4c65b281..b2dc16ffbd2 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 Wed Jul 22 12:10:11 2026
+  This file was generated by the md5table tool on Wed Jul 22 12:44:10 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -22,6 +22,7 @@ struct MD5Table {
 static const MD5Table md5table[] = {
 	{ "008e76ec3ae58d0add637ea7aa299a2c", "freddi3", "", "", -1, Common::FR_FRA, Common::kPlatformMacintosh },
 	{ "01e51615b26e8ddf6ae88dd86e435d4a", "freddi", "HE 71", "", -1, Common::EN_ANY, Common::kPlatformWindows },
+	{ "02236dcfb0cac59457153fc296233825", "rebel1", "", "v1.7", 213171, Common::FR_FRA, Common::kPlatformDOS },
 	{ "02cae0e7ff8504f73618391873d5781a", "freddi3", "HE 98.5", "", -1, Common::DE_DEU, Common::kPlatformWindows },
 	{ "0305e850382b812fec6e5998ef88a966", "pajama", "", "Demo", -1, Common::NL_NLD, Common::kPlatformUnknown },
 	{ "03173f0df12062d84721062666155efa", "farm", "HE 71", "Demo", 34312, Common::EN_ANY, Common::kPlatformWindows },
@@ -604,6 +605,7 @@ static const MD5Table md5table[] = {
 	{ "b886b0a5d909c7158a914e1d7c1c6c65", "loom", "EGA", "EGA", -1, Common::FR_FRA, Common::kPlatformDOS },
 	{ "b8955d7d23b4972229060d1592489fef", "freddicove", "HE 100", "", 41182, Common::NL_NLD, Common::kPlatformUnknown },
 	{ "b95347da0052a623f7a4bb15f425d897", "pajama3", "", "Demo", 13911, Common::IT_ITA, Common::kPlatformWindows },
+	{ "b978b92affdfe5744711ca0cb3548720", "rebel1", "", "v1.7", 213115, Common::DE_DEU, Common::kPlatformDOS },
 	{ "b9ac90e4411da1b6f7dd075cc4b03c2a", "rebel1", "", "v1.0", 214435, Common::EN_ANY, Common::kPlatformDOS },
 	{ "b9ba19ce376efc69be78ef3baef8d2b9", "monkey", "Mac", "", 8955, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "b9bb68c5d2c9b6e2d9c513a29a754a57", "puttmoon", "", "", 7828, Common::EN_ANY, Common::kPlatformDOS },


Commit: a675548a73eb09cad04dce31cbaa534aee058726
    https://github.com/scummvm/scummvm/commit/a675548a73eb09cad04dce31cbaa534aee058726
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA2: added spanish release detection code

Changed paths:
    engines/scumm/detection_internal.h


diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index 60d0c2f64f2..81ca30b735a 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -253,6 +253,8 @@ static Common::Language detectLanguage(const Common::FSList &fslist, byte id, co
 					return Common::EN_ANY;
 				case 48097:	// 66353d7250f680b28992459c355caa17
 					return Common::IT_ITA;
+				case 49750:	// a4d2d985548cdd29523db5b117ca1b3d
+					return Common::ES_ESP;
 				case 50094:	// 004fb2fd15f84a1f81cc362d73811c9c
 					return Common::DE_DEU;
 				case 60976:	// c53823d48beca122c45a83d35027a0e7


Commit: f522260a5886099825d0b158cd54af4458174b55
    https://github.com/scummvm/scummvm/commit/f522260a5886099825d0b158cd54af4458174b55
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA1: added support for 3DO release

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection_tables.h
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 885e47dabdb..80ba3075efc 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -455,6 +455,7 @@ rebel1	Star Wars: Rebel Assault
 	6bb0fe2d7816c5528c773f2898e4e0ff	212915	en	DOS	-	v1.7	-	-
 	02236dcfb0cac59457153fc296233825	213171	fr	DOS	-	v1.7	-	-
 	b978b92affdfe5744711ca0cb3548720	213115	de	DOS	-	v1.7	-	-
+	bb1d02798011e5906c4c726be4d63544	1908	en	3DO	-	-	-	-
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 29c5118c8b2..2859350ad90 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -49,6 +49,7 @@ static const char *const directoryGlobs[] = {
 	"Contents", // Mac Steam versions
 	"MacOS",    // Mac Steam versions
 	"Resources", // Mac SE/Remastered versions
+	"OPEN",     // 3DO version of rebel1
 	0
 };
 
@@ -502,6 +503,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", "O1LOGO.AAA", kGenUnchanged, UNK_LANG, Common::kPlatform3DO, "" },
 
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index b2dc16ffbd2..5d58a3c5625 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 Wed Jul 22 12:44:10 2026
+  This file was generated by the md5table tool on Wed Jul 22 13:13:22 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -613,6 +613,7 @@ static const MD5Table md5table[] = {
 	{ "ba888e6831517597859e91aa173f945c", "spyfox", "", "Demo", 20103, Common::FR_FRA, Common::kPlatformUnknown },
 	{ "ba90a1290f45f116407841862b8539f6", "spyfox2", "", "Demo", 14689, Common::IT_ITA, Common::kPlatformWindows },
 	{ "bab0fb81dcb12b8930c5d850b8f2a7de", "balloon", "HE 80", "", 12800, Common::DE_DEU, Common::kPlatformWindows },
+	{ "bb1d02798011e5906c4c726be4d63544", "rebel1", "", "", 1908, Common::EN_ANY, Common::kPlatform3DO },
 	{ "bbadf7309c4a2c2763e4bbba3c3be634", "freddi3", "", "Demo", 22718, Common::FR_FRA, Common::kPlatformUnknown },
 	{ "bc4700bc0e12879f6d25d14d6be6cfdd", "spyfox2", "", "", 51286, Common::DE_DEU, Common::kPlatformUnknown },
 	{ "bd126753de619a495f9f22adc951c8d5", "monkey2", "", "", 11135, Common::IT_ITA, Common::kPlatformDOS },


Commit: 4b7d5e6348fb945c0a861b4be1fd8cb6a69e33c0
    https://github.com/scummvm/scummvm/commit/4b7d5e6348fb945c0a861b4be1fd8cb6a69e33c0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA1: initial support for the SegaCD release

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/charset.cpp
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel1/levels.cpp
    engines/scumm/scumm-md5.h
    engines/scumm/smush/rebel/codec_ra1.cpp
    engines/scumm/smush/rebel/codec_ra1.h
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h
    engines/scumm/smush/smush_player.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 80ba3075efc..3ade3d6c066 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -456,6 +456,7 @@ rebel1	Star Wars: Rebel Assault
 	02236dcfb0cac59457153fc296233825	213171	fr	DOS	-	v1.7	-	-
 	b978b92affdfe5744711ca0cb3548720	213115	de	DOS	-	v1.7	-	-
 	bb1d02798011e5906c4c726be4d63544	1908	en	3DO	-	-	-	-
+	d77be4d8a3e609092703508e09f1ed5d	132904	ja	SEGA	-	-	-	-
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
diff --git a/engines/scumm/charset.cpp b/engines/scumm/charset.cpp
index 81c14ae2972..42910601596 100644
--- a/engines/scumm/charset.cpp
+++ b/engines/scumm/charset.cpp
@@ -59,6 +59,10 @@ void ScummEngine::loadCJKFont() {
 
 	_useMultiFont = false;	// Korean Multi-Font
 
+	// Sega CD Rebel Assault uses its SMUSH subtitle font.
+	if (_game.id == GID_REBEL1 && _game.platform == Common::kPlatformSegaCD)
+		return;
+
 	// Special case for Korean
 	if (isScummvmKorTarget()) {
 		loadKorFont();
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 2859350ad90..789e13a3102 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -504,6 +504,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 
 	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel1", "O1LOGO.AAA", kGenUnchanged, UNK_LANG, Common::kPlatform3DO, "" },
+	{ "rebel1", "SUB_AP.BIN", kGenUnchanged, UNK_LANG, Common::kPlatformSegaCD, "" },
 
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index 77a4fbdb136..c947b5c2cec 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -117,7 +117,8 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank, bool wa
 			continue;
 		}
 
-		int codec = READ_LE_UINT16(data + fobjOffset + 8);
+		// FOBJ stores the codec and its parameter in separate bytes.
+		const int codec = data[fobjOffset + 8];
 		bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
 		bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
 		bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
@@ -140,6 +141,13 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank, bool wa
 			smushDecodeRA1Transparent(decPtr, fobjData, 0, 0,
 				bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width,
 				fobjDataSize);
+		} else if (codec == SMUSH_CODEC_SEGACD_RLE || codec == SMUSH_CODEC_SEGACD_OPAQUE) {
+			// Codecs 31/32 use palette-banked 4bpp pixels.
+			const byte paletteBase = data[fobjOffset + 9];
+			bank.sprites[i].data = decPtr;
+			smushDecodeRA1SegaCDRLE(decPtr, fobjData, 0, 0,
+				bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width,
+				bank.sprites[i].height, fobjDataSize, codec == SMUSH_CODEC_SEGACD_RLE, paletteBase);
 		} else {
 			bank.sprites[i].width = 0;
 			bank.sprites[i].height = 0;
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 5d58a3c5625..d5b332ff223 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 Wed Jul 22 13:13:22 2026
+  This file was generated by the md5table tool on Wed Jul 22 13:34:38 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -703,6 +703,7 @@ static const MD5Table md5table[] = {
 	{ "d6dd0646404768a63e963891a96daadd", "atlantis", "Mac Floppy", "Floppy", 12035, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "d73c851b942af44deb9b6d5f416a0972", "freddi3", "HE 99", "Demo", 22779, Common::HE_ISR, Common::kPlatformWindows },
 	{ "d74122362a77ec24525fdd50297dfd82", "freddi4", "", "", -1, Common::FR_FRA, Common::kPlatformMacintosh },
+	{ "d77be4d8a3e609092703508e09f1ed5d", "rebel1", "", "", 132904, Common::JA_JPN, Common::kPlatformSegaCD },
 	{ "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 },
diff --git a/engines/scumm/smush/rebel/codec_ra1.cpp b/engines/scumm/smush/rebel/codec_ra1.cpp
index 1b2ab04b528..d7b87963532 100644
--- a/engines/scumm/smush/rebel/codec_ra1.cpp
+++ b/engines/scumm/smush/rebel/codec_ra1.cpp
@@ -27,6 +27,61 @@
 
 namespace Scumm {
 
+// Codecs 31/32 use the codec 1/3 line RLE over low-nibble-first packed 4bpp
+// pixels. Codec 31 leaves color zero transparent.
+void smushDecodeRA1SegaCDRLE(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufHeight, int dataSize, bool transparent, byte paletteBase) {
+	if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+		return;
+
+	const byte *srcEnd = src + dataSize;
+	for (int row = 0; row < height && srcEnd - src >= 2; row++) {
+		const int lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+		const int dstY = top + row;
+		byte *rowDst = (dstY >= 0 && dstY < bufHeight) ? dst + dstY * pitch : nullptr;
+		int x = 0;
+
+		while (x < width && lineData < lineEnd) {
+			const byte code = *lineData++;
+			const int num = (code >> 1) + 1;
+			const bool isRun = (code & 1) != 0;
+			byte runByte = 0;
+			if (isRun) {
+				if (lineData >= lineEnd)
+					break;
+				runByte = *lineData++;
+			}
+
+			for (int k = 0; k < num && x < width; k++) {
+				byte packed;
+				if (isRun) {
+					packed = runByte;
+				} else {
+					if (lineData >= lineEnd)
+						break;
+					packed = *lineData++;
+				}
+				const byte nibbles[2] = { (byte)(packed & 0x0F), (byte)(packed >> 4) };
+				for (int d = 0; d < 2 && x < width; d++, x++) {
+					const byte nibble = nibbles[d];
+					if (rowDst && !(transparent && nibble == 0)) {
+						const int dstX = left + x;
+						if (dstX >= 0 && dstX < pitch)
+							rowDst[dstX] = paletteBase + nibble;
+					}
+				}
+			}
+		}
+
+		const int rowSize = 2 + lineSize;
+		if (rowSize > srcEnd - src)
+			break;
+		src += rowSize;
+	}
+}
+
 // RLE with transparency on pixel 0.
 void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height,
 		int pitch, int dataSize, int sourceSkipX) {
@@ -212,7 +267,7 @@ void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bu
 	}
 }
 
-// Codecs 4/5 use 4x4 tile lookup tables.
+// Codecs 4/5/33/34 use 4x4 glyph tables.
 static uint8 s_ra1C4Tbl[2][256][16];
 static uint16 s_ra1C4Param = 0xFFFF;
 
@@ -254,6 +309,49 @@ static void ra1Codec4GenTiles(uint16 param1) {
 	}
 }
 
+// Sega codecs use four 64-glyph families over an 8x8 color-pair grid.
+static void ra1CodecSegaGenTiles(uint16 param1) {
+	uint8 *dst = &s_ra1C4Tbl[0][0][0];
+	for (int family = 0; family < 4; family++) {
+		for (int i = 0; i < 8; i++) {
+			for (int k = 0; k < 8; k++) {
+				const int cy = i + param1;
+				const int cx = k + param1;
+				const int c0 = param1 + ((i + k) >> 1);
+				const int c1 = (c0 + cy) >> 1;
+				const int c2 = (c0 + cx) >> 1;
+
+				switch (family) {
+				case 0:
+					*dst++ = c0; *dst++ = c0; *dst++ = c1; *dst++ = cy;
+					*dst++ = c0; *dst++ = c0; *dst++ = c1; *dst++ = cy;
+					*dst++ = c2; *dst++ = c2; *dst++ = c0; *dst++ = c1;
+					*dst++ = cx; *dst++ = cx; *dst++ = c2; *dst++ = c0;
+					break;
+				case 1:
+					*dst++ = cy; *dst++ = cy; *dst++ = cy; *dst++ = cy;
+					*dst++ = c0; *dst++ = c0; *dst++ = c0; *dst++ = c0;
+					*dst++ = c2; *dst++ = c2; *dst++ = c2; *dst++ = c2;
+					*dst++ = cx; *dst++ = cx; *dst++ = cx; *dst++ = cx;
+					break;
+				case 2:
+					*dst++ = cy; *dst++ = cy; *dst++ = c1; *dst++ = c0;
+					*dst++ = cy; *dst++ = cy; *dst++ = c1; *dst++ = c0;
+					*dst++ = c1; *dst++ = c1; *dst++ = c0; *dst++ = c2;
+					*dst++ = c0; *dst++ = c0; *dst++ = c2; *dst++ = cx;
+					break;
+				case 3:
+					*dst++ = cy; *dst++ = c0; *dst++ = c2; *dst++ = cx;
+					*dst++ = cy; *dst++ = c0; *dst++ = c2; *dst++ = cx;
+					*dst++ = cy; *dst++ = c0; *dst++ = c2; *dst++ = cx;
+					*dst++ = cy; *dst++ = c0; *dst++ = c2; *dst++ = cx;
+					break;
+				}
+			}
+		}
+	}
+}
+
 static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
 	uint8 *dst = &s_ra1C4Tbl[1][0][0];
 	int loop = param2 * 8;
@@ -275,9 +373,15 @@ void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int widt
 
 	const int mx = pitch;
 	const int my = bufHeight;
-	if (s_ra1C4Param != param) {
-		ra1Codec4GenTiles(param);
-		s_ra1C4Param = param;
+	const bool segaGlyphs = codec == 33 || codec == 34;
+	const bool hasSkipGlyph = codec == 4 || codec == 33;
+	const uint16 tableParam = param | (segaGlyphs ? 0x100 : 0);
+	if (s_ra1C4Param != tableParam) {
+		if (segaGlyphs)
+			ra1CodecSegaGenTiles(param);
+		else
+			ra1Codec4GenTiles(param);
+		s_ra1C4Param = tableParam;
 	}
 	int remaining = dataSize;
 	const byte *data = src;
@@ -309,7 +413,7 @@ void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int widt
 				return;
 			byte idx = *data++;
 			remaining--;
-			if (bit == 0 && idx == 0x80 && codec != 5)
+			if (bit == 0 && idx == 0x80 && hasSkipGlyph)
 				continue;
 			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
 				continue;
diff --git a/engines/scumm/smush/rebel/codec_ra1.h b/engines/scumm/smush/rebel/codec_ra1.h
index 4aeb572b18d..02de34a0cff 100644
--- a/engines/scumm/smush/rebel/codec_ra1.h
+++ b/engines/scumm/smush/rebel/codec_ra1.h
@@ -35,6 +35,8 @@ void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int
 void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
 void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
 		int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec);
+void smushDecodeRA1SegaCDRLE(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufHeight, int dataSize, bool transparent, byte paletteBase);
 
 } // End of namespace Scumm
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 33b0f2967de..bf01626994e 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -155,9 +155,14 @@ void SmushPlayerRebel1::initGamePlayerFields() {
 	_ra1FadeFrameHeight = 0;
 	_ra1FadeFrameValid = false;
 	_ra1UseFadeFrame = false;
+	_segaCDFont = nullptr;
+	_segaCDFontSize = 0;
 }
 
 void SmushPlayerRebel1::destroyGamePlayerFields() {
+	free(_segaCDFont);
+	_segaCDFont = nullptr;
+	_segaCDFontSize = 0;
 	free(_ra1ObjOverlayData);
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
@@ -545,7 +550,15 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 		return false;
 
 	// Block codecs consume column-major tiles while clipping destination pixels.
-	if (codec == SMUSH_CODEC_RA1_DELTA || codec == SMUSH_CODEC_RA1_BLOCK) {
+	if (codec == SMUSH_CODEC_RA1_DELTA || codec == SMUSH_CODEC_RA1_BLOCK ||
+			codec == SMUSH_CODEC_SEGACD_BLOCK33 || codec == SMUSH_CODEC_SEGACD_BLOCK34) {
+		left += _fobjOffsetX;
+		top += _fobjOffsetY;
+		return false;
+	}
+
+	// Packed Sega RLE clips internally.
+	if (codec == SMUSH_CODEC_SEGACD_RLE || codec == SMUSH_CODEC_SEGACD_OPAQUE) {
 		left += _fobjOffsetX;
 		top += _fobjOffsetY;
 		return false;
@@ -587,6 +600,8 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		return true;
 	case SMUSH_CODEC_RA1_DELTA:
 	case SMUSH_CODEC_RA1_BLOCK:
+	case SMUSH_CODEC_SEGACD_BLOCK33:
+	case SMUSH_CODEC_SEGACD_BLOCK34:
 		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch,
 			(_dst == _specialBuffer) ? _height : _vm->_screenHeight,
 			dataSize, param, parm2, codec);
@@ -595,6 +610,12 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch,
 			pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight, dataSize);
 		return true;
+	case SMUSH_CODEC_SEGACD_RLE:
+	case SMUSH_CODEC_SEGACD_OPAQUE:
+		smushDecodeRA1SegaCDRLE(_dst, src, left, top, width, height, pitch,
+			(_dst == _specialBuffer) ? _height : _vm->_screenHeight,
+			dataSize, codec == SMUSH_CODEC_SEGACD_RLE, param);
+		return true;
 	case SMUSH_CODEC_SKIP_RLE: {
 		const int bufWidth = pitch;
 		const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
@@ -1164,10 +1185,126 @@ SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
 	return _sf[font];
 }
 
+// MAI_AP.BIN stores 16x16 1bpp glyphs at 0x5f04, indexed as
+// (highByte - 0x80) * 128 + lowByte - 0x80.
+static const int32 kSegaCDFontOffset = 0x5f04;
+static const int kSegaCDGlyphBytes = 32;   // 16x16, 2 bytes per row
+static const int kSegaCDGlyphSize = 16;
+
+void SmushPlayerRebel1::ra1LoadSegaCDFont() {
+	if (_segaCDFont)
+		return;
+	ScummFile *f = _vm->instantiateScummFile();
+	_vm->openFile(*f, "MAI_AP.BIN");
+	if (f->isOpen()) {
+		_segaCDFontSize = (int32)f->size();
+		if (_segaCDFontSize > 0 && _segaCDFontSize <= 1024 * 1024) {
+			_segaCDFont = (byte *)malloc(_segaCDFontSize);
+			if (_segaCDFont && f->read(_segaCDFont, _segaCDFontSize) != (uint32)_segaCDFontSize) {
+				free(_segaCDFont);
+				_segaCDFont = nullptr;
+				_segaCDFontSize = 0;
+			}
+		}
+	}
+	delete f;
+}
+
+void SmushPlayerRebel1::ra1HandleTextSegaCD(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
+		return;
+
+	ra1LoadSegaCDFont();
+	if (!_segaCDFont)
+		return;
+
+	const int anchorX = b.readSint32BE();
+	const int startY = b.readSint32BE();
+	const int32 bodyLen = subSize - 8;
+	if (bodyLen <= 0)
+		return;
+	byte *body = (byte *)malloc(bodyLen);
+	if (!body)
+		return;
+	if (b.read(body, bodyLen) != (uint32)bodyLen) {
+		free(body);
+		return;
+	}
+
+	// Choose contrasting colors from the current palette.
+	int fg = 0, sh = 0, bestLum = -1, worstLum = 0x10000;
+	for (int i = 1; i < 256; i++) {
+		const int lum = _pal[i * 3] + _pal[i * 3 + 1] + _pal[i * 3 + 2];
+		if (lum > bestLum) {
+			bestLum = lum;
+			fg = i;
+		}
+		if (lum < worstLum) {
+			worstLum = lum;
+			sh = i;
+		}
+	}
+
+	// Lines are marked by 20 00 3C and contain two-byte glyph codes.
+	int cursorY = startY;
+	int32 p = 0;
+	while (p < bodyLen) {
+		Common::Array<uint16> glyphs;
+		while (p < bodyLen) {
+			byte c = body[p];
+			if (c >= 0x80 && p + 1 < bodyLen) {
+				glyphs.push_back((uint16)((c << 8) | body[p + 1]));
+				p += 2;
+			} else if (c == 0x3c && !glyphs.empty()) {
+				break;  // start of the next line
+			} else {
+				p++;    // skip control bytes (0x20, 0x00, '<')
+			}
+		}
+		if (glyphs.empty())
+			continue;
+
+		const int lineWidth = (int)glyphs.size() * kSegaCDGlyphSize;
+		int drawX = anchorX - lineWidth / 2;
+		for (uint g = 0; g < glyphs.size(); g++) {
+			const int hi = (glyphs[g] >> 8) - 0x80;
+			const int lo = (glyphs[g] & 0xFF) - 0x80;
+			const int32 idx = hi * 128 + lo;
+			const int32 off = kSegaCDFontOffset + idx * kSegaCDGlyphBytes;
+			if (idx < 0 || off < 0 || off + kSegaCDGlyphBytes > _segaCDFontSize) {
+				drawX += kSegaCDGlyphSize;
+				continue;
+			}
+			for (int row = 0; row < kSegaCDGlyphSize; row++) {
+				const uint16 bits = (_segaCDFont[off + row * 2] << 8) | _segaCDFont[off + row * 2 + 1];
+				for (int col = 0; col < kSegaCDGlyphSize; col++) {
+					if (!((bits >> (15 - col)) & 1))
+						continue;
+					const int px = drawX + col;
+					const int py = cursorY + row;
+					if (px + 1 >= 0 && px + 1 < _width && py + 1 >= 0 && py + 1 < _height)
+						_dst[(py + 1) * _width + (px + 1)] = (byte)sh;
+					if (px >= 0 && px < _width && py >= 0 && py < _height)
+						_dst[py * _width + px] = (byte)fg;
+				}
+			}
+			drawX += kSegaCDGlyphSize;
+		}
+		cursorY += kSegaCDGlyphSize + 2;
+	}
+
+	free(body);
+}
+
 void SmushPlayerRebel1::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
 		return;
 
+	if (_vm->_game.platform == Common::kPlatformSegaCD) {
+		ra1HandleTextSegaCD(subSize, b);
+		return;
+	}
+
 	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 	if (!rebel1)
 		return;
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index e34acd792eb..d77a9c989d3 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -65,6 +65,8 @@ private:
 	void ra1HandleFade(int32 subSize, Common::SeekableReadStream &b);
 	SmushFont *ra1GetFont(int font);
 	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+	void ra1HandleTextSegaCD(int32 subSize, Common::SeekableReadStream &b);
+	void ra1LoadSegaCDFont();
 	void ra1HandleFrameAudioChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b);
 	void ra1FeedAudio(uint32 subType, uint8 *srcBuf, int groupId, int volume, int pan, int16 flags);
 	void ra1HandleGameFrameChunk(int32 subSize, Common::SeekableReadStream &b, bool fastForwarding);
@@ -74,6 +76,9 @@ private:
 		Common::SeekableReadStream &b, bool fastForwarding);
 	void ra1InitAudioTrackSizes();
 
+	byte *_segaCDFont;
+	int32 _segaCDFontSize;
+
 	// Clean frame buffer used as the next delta source.
 	byte *_ra1CleanFrame;
 	int32 _ra1CleanFrameSize;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index fe55b5e7ec1..e4ee345163b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -86,6 +86,11 @@ namespace Scumm {
 #define SMUSH_CODEC_SKIP_RLE     23   // RA1: additive line-update overlay; RA2: skip/copy with embedded RLE
 #define SMUSH_CODEC_DELTA_BLOCKS 37
 #define SMUSH_CODEC_LINE_UPDATE2 44   // RA2: Variant of codec 21
+
+#define SMUSH_CODEC_SEGACD_RLE     31   // RA1 Sega CD: transparent 4bpp line RLE
+#define SMUSH_CODEC_SEGACD_OPAQUE  32   // RA1 Sega CD: opaque 4bpp line RLE
+#define SMUSH_CODEC_SEGACD_BLOCK33 33   // RA1 Sega CD: codec 4 with Sega glyph table
+#define SMUSH_CODEC_SEGACD_BLOCK34 34   // RA1 Sega CD: codec 5 with Sega glyph table and no skip glyph
 #define SMUSH_CODEC_RA2_BOMP     45   // RA2: BOMP RLE with variable header
 #define SMUSH_CODEC_DELTA_GLYPHS 47
 


Commit: 583f57338b5ad2f471f6a7f0a22edd5b92114249
    https://github.com/scummvm/scummvm/commit/583f57338b5ad2f471f6a7f0a22edd5b92114249
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA1: skip L7 in SegaCD as there are is such mission

Changed paths:
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 29507d3518b..e863db47cd6 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1436,6 +1436,10 @@ void InsaneRebel1::runGame() {
 				if (shouldAbortGameFlow())
 					break;
 
+				// The Sega CD release omits level 7 but retains its chapter transition.
+				if (_vm->_game.platform == Common::kPlatformSegaCD && level == 7)
+					continue;
+
 				completed = (this->*kLevelRunners[level - 1])();
 				if (completed) {
 					lastCompletedLevel = level;


Commit: 572e7ace90e5157e9540623c63dda7760dd8899a
    https://github.com/scummvm/scummvm/commit/572e7ace90e5157e9540623c63dda7760dd8899a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-22T22:27:46+02:00

Commit Message:
SCUMM: RA1: magenta entries inherit the previous Sega palette

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index bf01626994e..3c6e27f1ee1 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -367,6 +367,18 @@ SmushFont *SmushPlayerRebel1::getGameFont(int font) {
 }
 
 void SmushPlayerRebel1::adjustGamePalette() {
+	if (_vm->_game.platform == Common::kPlatformSegaCD) {
+		// Magenta entries inherit the previous Sega palette.
+		for (int i = 0; i < 256; ++i) {
+			byte *color = _pal + i * 3;
+			if (color[0] == 0xff && color[1] == 0 && color[2] == 0xff) {
+				color[0] = _shiftedDeltaPal[i * 3] >> 7;
+				color[1] = _shiftedDeltaPal[i * 3 + 1] >> 7;
+				color[2] = _shiftedDeltaPal[i * 3 + 2] >> 7;
+			}
+		}
+	}
+
 	for (int i = 0; i < ARRAYSIZE(_pal); ++i)
 		_shiftedDeltaPal[i] = _pal[i] << 7;
 	memset(_deltaPal, 0, sizeof(_deltaPal));




More information about the Scummvm-git-logs mailing list