[Scummvm-git-logs] scummvm master -> 243c6b1d36c6c32bbf0cbec3c11ef7aaf4a01340

neuromancer noreply at scummvm.org
Thu Jul 23 08:54:59 UTC 2026


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

Summary:
7be7156726 SCUMM: RA1: support for mac release
243c6b1d36 SCUMM: RA1: initial work to support psx release


Commit: 7be715672656766154b8e1bbce6dc1b2aeb20d06
    https://github.com/scummvm/scummvm/commit/7be715672656766154b8e1bbce6dc1b2aeb20d06
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-23T10:54:20+02:00

Commit Message:
SCUMM: RA1: support for mac release

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


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 3ade3d6c066..1454b15505d 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -457,6 +457,8 @@ rebel1	Star Wars: Rebel Assault
 	b978b92affdfe5744711ca0cb3548720	213115	de	DOS	-	v1.7	-	-
 	bb1d02798011e5906c4c726be4d63544	1908	en	3DO	-	-	-	-
 	d77be4d8a3e609092703508e09f1ed5d	132904	ja	SEGA	-	-	-	-
+	62543831f454b46ef8d0f50b9e1c063c	560453	en	Mac	-	-	68k application	-
+	8e1272e5c12ac35e29e3e4c50ec4a525	526296	en	Mac	-	-	PowerPC application	-
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index 81ca30b735a..77785688a06 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -485,6 +485,20 @@ static void composeFileHashMap(DescMap &fileMD5Map, const Common::FSList &fslist
 	}
 }
 
+static bool computeRebel1MacResourceForkMD5(const DetectorDesc &desc, const Common::String &baseFile,
+		Common::String &md5, int64 &size) {
+	Common::SearchSet directory;
+	directory.addDirectory(desc.node.getParent());
+	Common::MacResManager macResMan;
+
+	if (!macResMan.open(Common::Path(baseFile), directory) || !macResMan.hasResFork())
+		return false;
+
+	md5 = macResMan.computeResForkMD5AsString(kMD5FileSizeLimit);
+	size = macResMan.getResForkDataSize();
+	return !md5.empty();
+}
+
 static void detectGames(const Common::FSList &fslist, Common::List<DetectorResult> &results, const char *gameid) {
 	DescMap fileMD5Map;
 	DetectorResult dr;
@@ -504,11 +518,16 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 		// exist in the directory we are looking at, we can skip to the next
 		// one immediately.
 		Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod, gfp->platform));
+		const Common::String baseFile = file;
 		Common::Platform platform = gfp->platform;
+		const bool isRebel1Mac = !scumm_stricmp(gfp->gameid, "rebel1") && platform == Common::kPlatformMacintosh;
 		if (!fileMD5Map.contains(file)) {
 			if (fileMD5Map.contains(file + ".bin") && (platform == Common::Platform::kPlatformMacintosh || platform == Common::Platform::kPlatformUnknown)) {
 				file += ".bin";
 				platform = Common::Platform::kPlatformMacintosh;
+			} else if (isRebel1Mac && fileMD5Map.contains(file + ".rsrc")) {
+				file += ".rsrc";
+				platform = Common::Platform::kPlatformMacintosh;
 			} else
 				continue;
 		}
@@ -572,6 +591,20 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 						}
 						delete dataStream;
 					}
+
+					if (!d.md5Entry && isRebel1Mac) {
+						Common::String resourceMD5;
+						int64 resourceSize;
+						if (computeRebel1MacResourceForkMD5(d, baseFile, resourceMD5, resourceSize)) {
+							const MD5Table *resourceMD5Entry = findInMD5Table(resourceMD5.c_str());
+							if (resourceMD5Entry) {
+								d.md5 = resourceMD5;
+								d.md5Entry = resourceMD5Entry;
+								filesize = resourceSize;
+								platform = Common::Platform::kPlatformMacintosh;
+							}
+						}
+					}
 				}
 
 				dr.md5 = d.md5;
@@ -582,7 +615,7 @@ static void detectGames(const Common::FSList &fslist, Common::List<DetectorResul
 
 					// Print some debug info.
 					debugC(1, kDebugGlobalDetection, "SCUMM detector found matching file '%s' with MD5 %s, size %" PRId64 "\n",
-						file.c_str(), md5str.c_str(), filesize);
+						file.c_str(), d.md5.c_str(), filesize);
 
 					// Sanity check: We *should* have found a matching gameid/variant at this point.
 					// If not, we may have #ifdef'ed the entry out in our detection_tables.h, because we
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 789e13a3102..99b7746e997 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -50,6 +50,7 @@ static const char *const directoryGlobs[] = {
 	"MacOS",    // Mac Steam versions
 	"Resources", // Mac SE/Remastered versions
 	"OPEN",     // 3DO version of rebel1
+	"Rebel Assault PowerPC", // Mac version of rebel1
 	0
 };
 
@@ -503,6 +504,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", "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/resource.cpp b/engines/scumm/resource.cpp
index ccffb701fa5..df195fef9c6 100644
--- a/engines/scumm/resource.cpp
+++ b/engines/scumm/resource.cpp
@@ -190,6 +190,11 @@ bool ScummEngine::openFile(BaseScummFile &file, const Common::Path &filename, bo
 		result = file.open(filename);
 	}
 
+	if (!result && _game.id == GID_REBEL1 && _game.platform == Common::kPlatformMacintosh) {
+		file.close();
+		result = file.open(filename.append(";1"));
+	}
+
 	return result;
 }
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index d5b332ff223..3ba9d4bfe39 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:34:38 2026
+  This file was generated by the md5table tool on Wed Jul 22 20:44:15 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -334,6 +334,7 @@ static const MD5Table md5table[] = {
 	{ "61fa2dd4dc9e58347c4de5f4243a3ba6", "funpack", "", "", 5798, Common::JA_JPN, Common::kPlatformMacintosh },
 	{ "62050da376483d8edcbd98cd26b6cb57", "puttrace", "HE 99", "", -1, Common::RU_RUS, Common::kPlatformWindows },
 	{ "624cdb93654667c869d204a64af7e57f", "maniac", "V2", "V2", 1988, Common::EN_ANY, Common::kPlatformDOS },
+	{ "62543831f454b46ef8d0f50b9e1c063c", "rebel1", "", "", 560453, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "6269b8fbf51a353e5b501e4ad98cdc67", "arttime", "", "", -1, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "6271130f440066830eca9056c1d7926f", "water", "HE 80", "", 12699, Common::RU_RUS, Common::kPlatformWindows },
 	{ "62b8c16b6db226ba95aaa8be73f9885c", "indy3", "EGA", "EGA", -1, Common::ES_ESP, Common::kPlatformAmiga },
@@ -476,6 +477,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 },
+	{ "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 },
 	{ "8e9417564f33790815445b2136efa667", "atlantis", "", "CD", 11915, Common::JA_JPN, Common::kPlatformMacintosh },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 98ecc12e0b7..5fc86950157 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1054,6 +1054,9 @@ Common::Error ScummEngine::init() {
 		SearchMan.addSubDirectoryMatching(gameDataDir, "video");
 		SearchMan.addSubDirectoryMatching(gameDataDir, "data");
 	}
+
+	if (_game.id == GID_REBEL1 && _game.platform == Common::kPlatformMacintosh)
+		SearchMan.addSubDirectoryMatching(gameDataDir, "REBEL", 0, 2);
 #endif
 
 	// Extra directories needed for the Steam versions
@@ -1262,7 +1265,7 @@ Common::Error ScummEngine::init() {
 		Common::MacResManager resource;
 
 		// Indy3 and LOOM *must* use the _macScreen
-		if (isUsingOriginalGUI() || _game.version == 3) {
+		if (_game.id != GID_REBEL1 && (isUsingOriginalGUI() || _game.version == 3)) {
 			_macScreen = new Graphics::Surface();
 			_macScreen->create(640, _useMacScreenCorrectHeight ? 480 : 400, Graphics::PixelFormat::createFormatCLUT8());
 		}


Commit: 243c6b1d36c6c32bbf0cbec3c11ef7aaf4a01340
    https://github.com/scummvm/scummvm/commit/243c6b1d36c6c32bbf0cbec3c11ef7aaf4a01340
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-23T10:54:20+02:00

Commit Message:
SCUMM: RA1: initial work to support psx release

Changed paths:
  A engines/scumm/insane/rebel2/psx/level1.cpp
  A engines/scumm/insane/rebel2/psx/model.cpp
  A engines/scumm/insane/rebel2/psx/psx.cpp
  A engines/scumm/insane/rebel2/psx/psx.h
  A engines/scumm/insane/rebel2/psx/resource.cpp
  A engines/scumm/insane/rebel2/psx/video.cpp
  A engines/scumm/insane/rebel2/psx/video.h
    devtools/md5table.cpp
    devtools/scumm-md5.txt
    engines/scumm/configure.engine
    engines/scumm/detection_tables.h
    engines/scumm/metaengine.cpp
    engines/scumm/module.mk
    engines/scumm/scumm-md5.h
    engines/scumm/scumm.cpp
    engines/scumm/scumm_v7.h


diff --git a/devtools/md5table.cpp b/devtools/md5table.cpp
index cbff4585da3..5c95d95f440 100644
--- a/devtools/md5table.cpp
+++ b/devtools/md5table.cpp
@@ -84,6 +84,7 @@ static const StringMap platformMap[] = {
 	{ "Mac",		"kPlatformMacintosh" },
 	{ "NES",		"kPlatformNES" },
 	{ "PC-Engine",		"kPlatformPCEngine" },
+	{ "PSX",		"kPlatformPSX" },
 	{ "SEGA",		"kPlatformSegaCD" },
 	{ "Windows",		"kPlatformWindows" },
 	{ "Wii",		"kPlatformWii" },
diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 1454b15505d..ec53bda19d4 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -462,6 +462,8 @@ rebel1	Star Wars: Rebel Assault
 
 rebel2	Star Wars: Rebel Assault II: The Hidden Empire
 	6b73a08c535c0544785d73ff812908a0	9430	All	DOS	-	-	-	-
+	569b6160d1d56573ff54f2c908483897	720896	us	PSX	PlayStation	-	Disc 1 executable	-
+	ae607f6ed270311d7dbc033d3e943d3e	750723120	us	PSX	PlayStation	-	Disc 1 raw image	-
 
 	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
 
diff --git a/engines/scumm/configure.engine b/engines/scumm/configure.engine
index 9e36dfb23c0..0f0c80de3f8 100644
--- a/engines/scumm/configure.engine
+++ b/engines/scumm/configure.engine
@@ -1,5 +1,6 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
-add_engine scumm "SCUMM" yes "scumm_7_8 he" "v0-v6 games" "" "midi fmtowns_pc98_audio sid_audio imgui"
+add_engine scumm "SCUMM" yes "scumm_7_8 he rebel2_psx" "v0-v6 games" "" "midi fmtowns_pc98_audio sid_audio imgui"
 add_engine scumm_7_8 "v7 & v8 games" yes
 add_engine he "HE71+ games" yes "" "" "highres bink" "enet"
+add_engine rebel2_psx "Rebel Assault II PlayStation" no "" "" "16bit 3d tinygl"
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 99b7746e997..19b4cb535b3 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -234,6 +234,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, 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_TESTING | 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)},
 	{"dig",  "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -510,6 +511,7 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
+	{ "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/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
new file mode 100644
index 00000000000..ceae659a063
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -0,0 +1,421 @@
+/* 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/events.h"
+#include "common/random.h"
+#include "common/system.h"
+#include "common/util.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/surface.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/video.h"
+
+#include <math.h>
+
+namespace Scumm {
+
+#ifdef USE_TINYGL
+struct RA2PSXLevel1Enemy {
+	RA2PSXLevel1Enemy() : active(false), pattern(0), age(0), lifetime(0), fireFrame(0),
+			laserFrames(0), startX(0), startY(0), controlX(0), controlY(0), endX(0), endY(0),
+			x(0), y(0), size(0), pitch(0), yaw(0), roll(0) {}
+
+	bool active;
+	int pattern;
+	int age;
+	int lifetime;
+	int fireFrame;
+	int laserFrames;
+	float startX;
+	float startY;
+	float controlX;
+	float controlY;
+	float endX;
+	float endY;
+	float x;
+	float y;
+	float size;
+	float pitch;
+	float yaw;
+	float roll;
+};
+
+struct RA2PSXLevel1Explosion {
+	RA2PSXLevel1Explosion() : frames(0), x(0), y(0) {}
+
+	int frames;
+	int x;
+	int y;
+};
+
+static void updateLevel1Enemy(RA2PSXLevel1Enemy &enemy) {
+	const float t = MIN(1.0f, (float)enemy.age / enemy.lifetime);
+	const float inverse = 1.0f - t;
+	enemy.x = inverse * inverse * enemy.startX + 2.0f * inverse * t * enemy.controlX +
+			t * t * enemy.endX;
+	enemy.y = inverse * inverse * enemy.startY + 2.0f * inverse * t * enemy.controlY +
+			t * t * enemy.endY;
+	enemy.size = 5.0f + 72.0f * t * t;
+	enemy.yaw = (enemy.controlX - enemy.startX) * 0.18f + sinf(enemy.age * 0.09f) * 18.0f;
+	enemy.pitch = -8.0f + sinf(enemy.age * 0.06f) * 10.0f;
+	enemy.roll = (enemy.controlY - enemy.startY) * 0.12f + sinf(enemy.age * 0.12f) * 8.0f;
+}
+
+static void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &random) {
+	enemy = RA2PSXLevel1Enemy();
+	enemy.active = true;
+	enemy.pattern = random.getRandomBit();
+	enemy.lifetime = random.getRandomNumberRng(95, 150);
+	enemy.fireFrame = enemy.lifetime * random.getRandomNumberRng(45, 70) / 100;
+
+	const bool fromLeft = random.getRandomBit();
+	if (enemy.pattern == 0) {
+		enemy.startX = fromLeft ? -24.0f : 344.0f;
+		enemy.startY = (float)random.getRandomNumberRng(25, 175);
+		enemy.controlX = (float)random.getRandomNumberRng(95, 225);
+		enemy.controlY = (float)random.getRandomNumberRng(35, 170);
+		enemy.endX = fromLeft ? (float)random.getRandomNumberRng(210, 390) :
+				(float)random.getRandomNumberRngSigned(-70, 110);
+	} else {
+		enemy.startX = (float)random.getRandomNumberRng(35, 285);
+		enemy.startY = (float)random.getRandomNumberRng(20, 155);
+		enemy.controlX = fromLeft ? (float)random.getRandomNumberRng(180, 310) :
+				(float)random.getRandomNumberRng(10, 140);
+		enemy.controlY = (float)random.getRandomNumberRng(20, 190);
+		enemy.endX = fromLeft ? (float)random.getRandomNumberRngSigned(-80, 80) :
+				(float)random.getRandomNumberRng(240, 400);
+	}
+	enemy.endY = (float)random.getRandomNumberRngSigned(-25, 245);
+	updateLevel1Enemy(enemy);
+}
+
+static void drawLevel1Overlay(Graphics::Surface &surface, const RA2PSXLevel1Enemy *enemies,
+		const RA2PSXLevel1Explosion *explosions, int aimX, int aimY, int shield, int kills,
+		int misses, int frame, int fireFrames, int hitFrames, int damageFrames) {
+	const uint32 white = surface.format.RGBToColor(255, 255, 255);
+	const uint32 red = surface.format.RGBToColor(255, 48, 32);
+	const uint32 green = surface.format.RGBToColor(64, 255, 96);
+	const uint32 orange = surface.format.RGBToColor(255, 176, 32);
+	const uint32 dark = surface.format.RGBToColor(20, 32, 28);
+
+	for (int i = 0; i < 3; ++i) {
+		if (enemies[i].active && enemies[i].laserFrames > 0) {
+			surface.drawLine((int)enemies[i].x - 2, (int)enemies[i].y,
+					145, surface.h - 1, green);
+			surface.drawLine((int)enemies[i].x + 2, (int)enemies[i].y,
+					175, surface.h - 1, green);
+		}
+		if (explosions[i].frames > 0) {
+			const int radius = 2 + (8 - explosions[i].frames) * 2;
+			surface.drawLine(explosions[i].x - radius, explosions[i].y,
+					explosions[i].x + radius, explosions[i].y, orange);
+			surface.drawLine(explosions[i].x, explosions[i].y - radius,
+					explosions[i].x, explosions[i].y + radius, orange);
+			surface.drawLine(explosions[i].x - radius / 2, explosions[i].y - radius / 2,
+					explosions[i].x + radius / 2, explosions[i].y + radius / 2, red);
+			surface.drawLine(explosions[i].x + radius / 2, explosions[i].y - radius / 2,
+					explosions[i].x - radius / 2, explosions[i].y + radius / 2, red);
+		}
+	}
+
+	if (fireFrames > 0) {
+		surface.drawLine(38, surface.h - 1, aimX - 2, aimY, red);
+		surface.drawLine(surface.w - 39, surface.h - 1, aimX + 2, aimY, red);
+	}
+
+	const uint32 crosshair = hitFrames > 0 ? red : white;
+	surface.drawLine(aimX - 12, aimY, aimX - 4, aimY, crosshair);
+	surface.drawLine(aimX + 4, aimY, aimX + 12, aimY, crosshair);
+	surface.drawLine(aimX, aimY - 12, aimX, aimY - 4, crosshair);
+	surface.drawLine(aimX, aimY + 4, aimX, aimY + 12, crosshair);
+	surface.frameRect(Common::Rect(aimX - 8, aimY - 8, aimX + 9, aimY + 9), crosshair);
+
+	surface.fillRect(Common::Rect(8, surface.h - 14, 112, surface.h - 4), dark);
+	surface.frameRect(Common::Rect(8, surface.h - 14, 112, surface.h - 4), white);
+	const uint32 shieldColor = shield > 50 ? green : (shield > 20 ? orange : red);
+	if (shield > 0)
+		surface.fillRect(Common::Rect(10, surface.h - 12, 10 + shield, surface.h - 6), shieldColor);
+
+	for (int i = 0; i < MIN(kills, 20); ++i)
+		surface.fillRect(Common::Rect(8 + (i % 10) * 4, 6 + (i / 10) * 5,
+				10 + (i % 10) * 4, 9 + (i / 10) * 5), green);
+	for (int i = 0; i < MIN(misses, 20); ++i)
+		surface.fillRect(Common::Rect(surface.w - 10 - (i % 10) * 4, 6 + (i / 10) * 5,
+				surface.w - 8 - (i % 10) * 4, 9 + (i / 10) * 5), red);
+
+	const int progress = frame * (surface.w - 2) / 1699;
+	if (progress > 0)
+		surface.drawLine(1, surface.h - 2, progress, surface.h - 2, white);
+	if (damageFrames > 0) {
+		surface.frameRect(Common::Rect(1, 1, surface.w - 1, surface.h - 1), red);
+		surface.frameRect(Common::Rect(2, 2, surface.w - 2, surface.h - 2), red);
+	}
+}
+#endif
+
+Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
+#ifndef USE_TINYGL
+	(void)model;
+	return kLevel1Error;
+#else
+	Common::SeekableReadStream *stream = openRawFile("S1/L01_PLAY.STR", 1);
+	if (!stream)
+		return kLevel1Error;
+
+	RA2PSXStreamDecoder decoder(RA2PSXStreamDecoder::kVersion2);
+	if (!decoder.loadStream(stream) || !decoder.setOutputPixelFormat(g_system->getScreenFormat())) {
+		decoder.close();
+		return kLevel1Error;
+	}
+
+	RA2PSXTinyGLRenderer renderer;
+	if (!renderer.init(_vm->_screenWidth, _vm->_screenHeight)) {
+		decoder.close();
+		return kLevel1Error;
+	}
+
+	RA2PSXLevel1Enemy enemies[3];
+	RA2PSXLevel1Explosion explosions[3];
+	int aimX = _vm->_screenWidth / 2;
+	int aimY = _vm->_screenHeight / 2;
+	int shield = 100;
+	int kills = 0;
+	int misses = 0;
+	int spawnDelay = 0;
+	int spawnRange = 80;
+	int spawnBase = 60;
+	int nextSpawnAdjustment = 0;
+	int lastFrame = -1;
+	int lastShotFrame = -10;
+	int fireFrames = 0;
+	int hitFrames = 0;
+	int damageFrames = 0;
+	bool moveLeft = false;
+	bool moveRight = false;
+	bool moveUp = false;
+	bool moveDown = false;
+	bool mouseFire = false;
+	bool keyFire = false;
+	bool fireRequested = false;
+	Level1Result result = kLevel1Complete;
+
+	const bool cursorWasVisible = CursorMan.isVisible();
+	CursorMan.showMouse(false);
+	g_system->warpMouse(aimX, aimY);
+	decoder.start();
+
+	while (!_vm->shouldQuit() && !decoder.endOfVideo()) {
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_MOUSEMOVE:
+				aimX = CLIP<int>(event.mouse.x, 14, _vm->_screenWidth - 15);
+				aimY = CLIP<int>(event.mouse.y, 14, _vm->_screenHeight - 18);
+				break;
+			case Common::EVENT_LBUTTONDOWN:
+				aimX = CLIP<int>(event.mouse.x, 14, _vm->_screenWidth - 15);
+				aimY = CLIP<int>(event.mouse.y, 14, _vm->_screenHeight - 18);
+				mouseFire = true;
+				fireRequested = true;
+				break;
+			case Common::EVENT_LBUTTONUP:
+				mouseFire = false;
+				break;
+			case Common::EVENT_KEYDOWN:
+				if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+					result = kLevel1Quit;
+				} else if (event.kbd.keycode == Common::KEYCODE_LEFT ||
+						event.kbd.keycode == Common::KEYCODE_a) {
+					moveLeft = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_RIGHT ||
+						event.kbd.keycode == Common::KEYCODE_d) {
+					moveRight = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_UP ||
+						event.kbd.keycode == Common::KEYCODE_w) {
+					moveUp = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_DOWN ||
+						event.kbd.keycode == Common::KEYCODE_s) {
+					moveDown = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_SPACE ||
+						event.kbd.keycode == Common::KEYCODE_RETURN) {
+					keyFire = true;
+					fireRequested = true;
+				}
+				break;
+			case Common::EVENT_KEYUP:
+				if (event.kbd.keycode == Common::KEYCODE_LEFT || event.kbd.keycode == Common::KEYCODE_a)
+					moveLeft = false;
+				else if (event.kbd.keycode == Common::KEYCODE_RIGHT || event.kbd.keycode == Common::KEYCODE_d)
+					moveRight = false;
+				else if (event.kbd.keycode == Common::KEYCODE_UP || event.kbd.keycode == Common::KEYCODE_w)
+					moveUp = false;
+				else if (event.kbd.keycode == Common::KEYCODE_DOWN || event.kbd.keycode == Common::KEYCODE_s)
+					moveDown = false;
+				else if (event.kbd.keycode == Common::KEYCODE_SPACE ||
+						event.kbd.keycode == Common::KEYCODE_RETURN)
+					keyFire = false;
+				break;
+			case Common::EVENT_QUIT:
+			case Common::EVENT_RETURN_TO_LAUNCHER:
+				_vm->quitGame();
+				result = kLevel1Quit;
+				break;
+			default:
+				break;
+			}
+		}
+		if (result == kLevel1Quit)
+			break;
+
+		if (decoder.needsUpdate()) {
+			const Graphics::Surface *frame = decoder.decodeNextFrame();
+			if (!frame) {
+				result = kLevel1Error;
+				break;
+			}
+
+			const int currentFrame = decoder.getCurFrame();
+			while (lastFrame < currentFrame && shield > 0) {
+				++lastFrame;
+				aimX = CLIP<int>(aimX + ((int)moveRight - (int)moveLeft) * 6,
+						14, _vm->_screenWidth - 15);
+				aimY = CLIP<int>(aimY + ((int)moveDown - (int)moveUp) * 6,
+						14, _vm->_screenHeight - 18);
+
+				if (lastFrame >= nextSpawnAdjustment) {
+					nextSpawnAdjustment = lastFrame + 20;
+					spawnRange = MAX(40, spawnRange - 1);
+					spawnBase = MAX(20, spawnBase - 1);
+				}
+
+				int activeEnemies = 0;
+				for (int i = 0; i < 3; ++i)
+					activeEnemies += enemies[i].active ? 1 : 0;
+				--spawnDelay;
+				if (lastFrame < 1599 && activeEnemies < 3 && spawnDelay <= 0) {
+					for (int i = 0; i < 3; ++i) {
+						if (!enemies[i].active) {
+							spawnLevel1Enemy(enemies[i], _vm->_rnd);
+							break;
+						}
+					}
+					spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
+				}
+
+				if (fireFrames > 0)
+					--fireFrames;
+				if (hitFrames > 0)
+					--hitFrames;
+				if (damageFrames > 0)
+					--damageFrames;
+				for (int i = 0; i < 3; ++i) {
+					if (explosions[i].frames > 0)
+						--explosions[i].frames;
+					if (!enemies[i].active)
+						continue;
+
+					if (enemies[i].laserFrames > 0)
+						--enemies[i].laserFrames;
+					++enemies[i].age;
+					updateLevel1Enemy(enemies[i]);
+					if (enemies[i].age == enemies[i].fireFrame) {
+						enemies[i].laserFrames = 4;
+						if (_vm->_rnd.getRandomNumber(99) < 38) {
+							shield = MAX(0, shield - (int)_vm->_rnd.getRandomNumberRng(6, 10));
+							damageFrames = 4;
+						}
+					}
+					if (enemies[i].age >= enemies[i].lifetime) {
+						enemies[i].active = false;
+						++misses;
+						if (_vm->_rnd.getRandomNumber(99) < 18) {
+							shield = MAX(0, shield - 12);
+							damageFrames = 4;
+						}
+					}
+				}
+			}
+
+			if (shield <= 0) {
+				result = kLevel1Death;
+				break;
+			}
+
+			const bool heldFire = mouseFire || keyFire;
+			if ((fireRequested || (heldFire && currentFrame - lastShotFrame >= 5)) &&
+					currentFrame - lastShotFrame >= 3) {
+				lastShotFrame = currentFrame;
+				fireRequested = false;
+				fireFrames = 2;
+				int hitEnemy = -1;
+				float hitDistance = 1000000.0f;
+				for (int i = 0; i < 3; ++i) {
+					if (!enemies[i].active)
+						continue;
+					const float dx = enemies[i].x - aimX;
+					const float dy = enemies[i].y - aimY;
+					const float distance = dx * dx + dy * dy;
+					const float radius = MAX(10.0f, enemies[i].size * 0.72f);
+					if (distance <= radius * radius && distance < hitDistance) {
+						hitEnemy = i;
+						hitDistance = distance;
+					}
+				}
+				if (hitEnemy >= 0) {
+					enemies[hitEnemy].active = false;
+					++kills;
+					hitFrames = 3;
+					for (int i = 0; i < 3; ++i) {
+						if (explosions[i].frames == 0) {
+							explosions[i].frames = 8;
+							explosions[i].x = (int)enemies[hitEnemy].x;
+							explosions[i].y = (int)enemies[hitEnemy].y;
+							break;
+						}
+					}
+				}
+			}
+
+			renderer.beginFrame(*frame);
+			for (int i = 0; i < 3; ++i) {
+				if (enemies[i].active)
+					renderer.renderModel(model, enemies[i].x, enemies[i].y, enemies[i].size,
+							enemies[i].pitch, enemies[i].yaw, enemies[i].roll);
+			}
+			Graphics::Surface output;
+			renderer.finishFrame(output);
+			drawLevel1Overlay(output, enemies, explosions, aimX, aimY, shield, kills, misses,
+					currentFrame, fireFrames, hitFrames, damageFrames);
+			g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
+			g_system->updateScreen();
+		}
+		g_system->delayMillis(5);
+	}
+
+	decoder.close();
+	CursorMan.showMouse(cursorWasVisible);
+	return _vm->shouldQuit() ? kLevel1Quit : result;
+#endif
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
new file mode 100644
index 00000000000..f03027fab8b
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -0,0 +1,249 @@
+/* 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.
+ */
+
+#include "common/endian.h"
+#include "common/system.h"
+
+#include "scumm/insane/rebel2/psx/psx.h"
+
+#include <math.h>
+
+namespace Scumm {
+
+static bool readU16(const Common::Array<byte> &data, uint32 offset, uint16 &value) {
+	if (offset + 2 > data.size())
+		return false;
+	value = READ_LE_UINT16(data.data() + offset);
+	return true;
+}
+
+static bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &value) {
+	if (offset + 4 > data.size())
+		return false;
+	value = READ_LE_UINT32(data.data() + offset);
+	return true;
+}
+
+RA2PSXModel::RA2PSXModel() : _radius(1.0f) {
+}
+
+bool RA2PSXModel::load(const Common::Array<byte> &data) {
+	_vertices.clear();
+	_faces.clear();
+	_radius = 1.0f;
+	if (!parseModel(data, 0, 0) || _vertices.empty() || _faces.empty())
+		return false;
+
+	double radiusSquared = 1.0;
+	for (uint i = 0; i < _vertices.size(); ++i) {
+		const RA2PSXVertex &vertex = _vertices[i];
+		const double lengthSquared = (double)vertex.x * vertex.x +
+				(double)vertex.y * vertex.y + (double)vertex.z * vertex.z;
+		if (lengthSquared > radiusSquared)
+			radiusSquared = lengthSquared;
+	}
+	_radius = (float)sqrt(radiusSquared);
+	return true;
+}
+
+bool RA2PSXModel::parseModel(const Common::Array<byte> &data, uint32 modelOffset, int depth) {
+	if (depth > 8 || modelOffset + 8 > data.size())
+		return false;
+
+	uint32 nodeOffset;
+	uint32 childOffset;
+	if (!readU32(data, modelOffset, nodeOffset) || !readU32(data, modelOffset + 4, childOffset))
+		return false;
+
+	uint32 nodeCount = 0;
+	while (nodeOffset) {
+		if (++nodeCount > 1024 || nodeOffset + 8 > data.size())
+			return false;
+		uint32 nextOffset;
+		uint32 objectOffset;
+		if (!readU32(data, nodeOffset, nextOffset) || !readU32(data, nodeOffset + 4, objectOffset) ||
+				!parseObject(data, objectOffset))
+			return false;
+		nodeOffset = nextOffset;
+	}
+
+	return !childOffset || parseModel(data, childOffset, depth + 1);
+}
+
+bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffset) {
+	if (objectOffset + 48 > data.size())
+		return false;
+
+	uint16 vertexCount;
+	uint32 vertexOffset;
+	uint32 primitiveOffset;
+	if (!readU16(data, objectOffset + 2, vertexCount) || !readU32(data, objectOffset + 4, vertexOffset) ||
+			!readU32(data, objectOffset + 8, primitiveOffset) || !vertexCount ||
+			vertexOffset + (uint32)vertexCount * 8 > data.size())
+		return false;
+
+	const uint32 firstVertex = _vertices.size();
+	if (firstVertex + vertexCount > 0xffff)
+		return false;
+	for (uint32 index = 0; index < vertexCount; ++index) {
+		const byte *source = data.data() + vertexOffset + index * 8;
+		RA2PSXVertex vertex;
+		vertex.x = (int16)READ_LE_UINT16(source);
+		vertex.y = (int16)READ_LE_UINT16(source + 2);
+		vertex.z = (int16)READ_LE_UINT16(source + 4);
+		_vertices.push_back(vertex);
+	}
+
+	Common::HashMap<uint16, bool> seen;
+	for (uint32 listIndex = 0; listIndex < 8; ++listIndex) {
+		uint32 listOffset;
+		if (!readU32(data, objectOffset + 12 + listIndex * 4, listOffset) || listOffset >= data.size())
+			return false;
+
+		bool terminated = false;
+		for (uint32 count = 0; count < 65536; ++count) {
+			uint16 rawIndex;
+			if (!readU16(data, listOffset, rawIndex))
+				return false;
+			listOffset += 2;
+			if (rawIndex == 0xffff) {
+				terminated = true;
+				break;
+			}
+			if (seen.contains(rawIndex))
+				continue;
+			seen[rawIndex] = true;
+
+			const uint32 record = primitiveOffset + (uint32)rawIndex * 32;
+			if (record + 32 > data.size())
+				return false;
+			const uint16 mode = READ_LE_UINT16(data.data() + record + 12) & 0x1f;
+			RA2PSXFace face = {};
+			face.vertexCount = (mode & 0x10) ? 4 : 3;
+			const uint16 offsets[4] = {
+				READ_LE_UINT16(data.data() + record),
+				READ_LE_UINT16(data.data() + record + 2),
+				READ_LE_UINT16(data.data() + record + 4),
+				READ_LE_UINT16(data.data() + record + 24)
+			};
+			for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+				if ((offsets[vertexIndex] & 7) || offsets[vertexIndex] / 8 >= vertexCount)
+					return false;
+				face.vertex[vertexIndex] = firstVertex + offsets[vertexIndex] / 8;
+			}
+			face.r = data[record + 16];
+			face.g = data[record + 17];
+			face.b = data[record + 18];
+			if (!(face.r | face.g | face.b))
+				face.r = face.g = face.b = 0x7f;
+			_faces.push_back(face);
+		}
+		if (!terminated)
+			return false;
+	}
+	return true;
+}
+
+#ifdef USE_TINYGL
+
+RA2PSXTinyGLRenderer::RA2PSXTinyGLRenderer() : _context(nullptr), _width(0), _height(0) {
+}
+
+RA2PSXTinyGLRenderer::~RA2PSXTinyGLRenderer() {
+	if (_context)
+		TinyGL::destroyContext(_context);
+}
+
+bool RA2PSXTinyGLRenderer::init(int width, int height) {
+	if (_context)
+		return _width == width && _height == height;
+	_context = TinyGL::createContext(width, height, g_system->getScreenFormat(), 256, false, false);
+	if (!_context)
+		return false;
+	_width = width;
+	_height = height;
+	TinyGL::setContext(_context);
+	tglViewport(0, 0, width, height);
+	tglClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+	tglEnable(TGL_DEPTH_TEST);
+	tglDepthFunc(TGL_LESS);
+	tglDisable(TGL_LIGHTING);
+	tglDisable(TGL_TEXTURE_2D);
+	tglDisable(TGL_CULL_FACE);
+	return true;
+}
+
+void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
+	if (!_context)
+		return;
+	TinyGL::setContext(_context);
+
+	Graphics::Surface surface;
+	TinyGL::getSurfaceRef(surface);
+	surface.fillRect(Common::Rect(surface.w, surface.h), 0);
+	const int width = MIN<int>(background.w, surface.w);
+	const int height = MIN<int>(background.h, surface.h);
+	const int sourceX = (background.w - width) / 2;
+	const int sourceY = (background.h - height) / 2;
+	const int destX = (surface.w - width) / 2;
+	const int destY = (surface.h - height) / 2;
+	surface.copyRectToSurface(background, destX, destY,
+			Common::Rect(sourceX, sourceY, sourceX + width, sourceY + height));
+	tglClear(TGL_DEPTH_BUFFER_BIT);
+
+	tglMatrixMode(TGL_PROJECTION);
+	tglLoadIdentity();
+	tglOrthof(0.0f, (float)_width, (float)_height, 0.0f, -1024.0f, 1024.0f);
+	tglMatrixMode(TGL_MODELVIEW);
+	tglLoadIdentity();
+}
+
+void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float y, float size,
+		float pitch, float yaw, float roll) {
+	if (!_context || model.vertices().empty())
+		return;
+	TinyGL::setContext(_context);
+	tglMatrixMode(TGL_MODELVIEW);
+	tglPushMatrix();
+	tglTranslatef(x, y, 0.0f);
+	tglRotatef(roll, 0.0f, 0.0f, 1.0f);
+	tglRotatef(pitch, 1.0f, 0.0f, 0.0f);
+	tglRotatef(yaw, 0.0f, 1.0f, 0.0f);
+	const float scale = size / model.radius();
+	tglScalef(scale, -scale, scale);
+
+	const Common::Array<RA2PSXVertex> &vertices = model.vertices();
+	const Common::Array<RA2PSXFace> &faces = model.faces();
+	for (uint faceIndex = 0; faceIndex < faces.size(); ++faceIndex) {
+		const RA2PSXFace &face = faces[faceIndex];
+		tglColor3ub(face.r, face.g, face.b);
+		tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
+		for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+			const RA2PSXVertex &vertex = vertices[face.vertex[vertexIndex]];
+			tglVertex3f((float)vertex.x, (float)vertex.y, (float)vertex.z);
+		}
+		tglEnd();
+	}
+	tglPopMatrix();
+}
+
+void RA2PSXTinyGLRenderer::finishFrame(Graphics::Surface &surface) {
+	if (!_context)
+		return;
+	TinyGL::setContext(_context);
+	TinyGL::presentBuffer();
+	TinyGL::getSurfaceRef(surface);
+}
+
+#endif
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
new file mode 100644
index 00000000000..cd39c3552de
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -0,0 +1,179 @@
+/* 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/events.h"
+#include "common/file.h"
+#include "common/system.h"
+#include "common/translation.h"
+#include "common/util.h"
+
+#include "graphics/surface.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/video.h"
+
+namespace Scumm {
+
+Rebel2PSX::Rebel2PSX(ScummEngine_v7 *vm) : _vm(vm) {
+}
+
+Common::SeekableReadStream *Rebel2PSX::openResource(int number) {
+	const Common::Path path(Common::String::format("RESOURCE.%03d", number));
+	Common::File *file = new Common::File();
+	if (file->open(path))
+		return file;
+	delete file;
+	return nullptr;
+}
+
+Common::SeekableReadStream *Rebel2PSX::openRawFile(const Common::Path &path, int discNumber) {
+	Common::File *file = new Common::File();
+	Common::Path extractedPath = path;
+	if (discNumber == 2) {
+		const Common::Path disc2Path = Common::Path("DISC2").join(path);
+		if (file->open(disc2Path))
+			extractedPath = disc2Path;
+	}
+	if (!file->isOpen() && !file->open(extractedPath)) {
+		delete file;
+		return nullptr;
+	}
+	if (file->size() % 2352 != 0) {
+		delete file;
+		return nullptr;
+	}
+	return file;
+}
+
+bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version2) {
+	Common::SeekableReadStream *stream = openRawFile(path, discNumber);
+	if (!stream)
+		return false;
+
+	const RA2PSXStreamDecoder::FrameVersion frameVersion = version2 ?
+			RA2PSXStreamDecoder::kVersion2 : RA2PSXStreamDecoder::kVersion3;
+	RA2PSXStreamDecoder decoder(frameVersion);
+	if (!decoder.loadStream(stream) || !decoder.setOutputPixelFormat(g_system->getScreenFormat())) {
+		decoder.close();
+		return false;
+	}
+	decoder.start();
+	g_system->fillScreen(0);
+
+	bool skipped = false;
+	while (!_vm->shouldQuit() && !decoder.endOfVideo() && !skipped) {
+		if (decoder.needsUpdate()) {
+			const Graphics::Surface *frame = decoder.decodeNextFrame();
+			if (frame) {
+				const int width = MIN<int>(frame->w, _vm->_screenWidth);
+				const int height = MIN<int>(frame->h, _vm->_screenHeight);
+				const int sourceX = (frame->w - width) / 2;
+				const int sourceY = (frame->h - height) / 2;
+				const int destX = (_vm->_screenWidth - width) / 2;
+				const int destY = (_vm->_screenHeight - height) / 2;
+				g_system->copyRectToScreen(frame->getBasePtr(sourceX, sourceY), frame->pitch,
+						destX, destY, width, height);
+				g_system->updateScreen();
+			}
+		}
+
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			if ((event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START &&
+					(event.customType == kScummActionInsaneSkip ||
+					 event.customType == kScummActionInsaneBack)) ||
+					(event.type == Common::EVENT_KEYDOWN &&
+					 event.kbd.keycode == Common::KEYCODE_ESCAPE) ||
+					event.type == Common::EVENT_LBUTTONUP)
+				skipped = true;
+			else if (event.type == Common::EVENT_QUIT || event.type == Common::EVENT_RETURN_TO_LAUNCHER)
+				_vm->quitGame();
+		}
+		g_system->delayMillis(5);
+	}
+	decoder.close();
+	return !_vm->shouldQuit();
+}
+
+bool Rebel2PSX::loadLevel1Model(RA2PSXModel &model) {
+	Common::SeekableReadStream *stream = openResource(1);
+	if (!stream)
+		return false;
+	RA2PSXArchive archive;
+	const bool loaded = archive.load(*stream);
+	delete stream;
+	if (!loaded)
+		return false;
+
+	Common::Array<byte> modelData;
+	return archive.getMember("fOFS/TieFighter/main", modelData) && model.load(modelData);
+}
+
+Common::Error Rebel2PSX::runGame() {
+#ifdef USE_TINYGL
+	RA2PSXModel model;
+	if (!loadLevel1Model(model))
+		return Common::Error(Common::kReadingFailed,
+				_("Could not load the PlayStation Level 1 resources"));
+
+	if (!playVideo("S1/L01_INTR.STR", 1, false)) {
+		if (_vm->shouldQuit())
+			return Common::kNoError;
+		return Common::Error(Common::kReadingFailed,
+				_("Could not play the PlayStation Level 1 introduction"));
+	}
+
+	int lives = 3;
+	while (!_vm->shouldQuit()) {
+		const Level1Result result = playLevel1(model);
+		if (result == kLevel1Quit)
+			return Common::kNoError;
+		if (result == kLevel1Error)
+			return Common::Error(Common::kReadingFailed,
+					_("Could not run the PlayStation Level 1 gameplay"));
+		if (result == kLevel1Complete) {
+			if (!playVideo("S1/L01_EXTR.STR", 1, false) && !_vm->shouldQuit())
+				return Common::Error(Common::kReadingFailed,
+						_("Could not play the PlayStation Level 1 ending"));
+			return Common::kNoError;
+		}
+
+		if (!playVideo("S1/L01_DIE.STR", 1, false)) {
+			if (_vm->shouldQuit())
+				return Common::kNoError;
+			return Common::Error(Common::kReadingFailed,
+					_("Could not play the PlayStation Level 1 death video"));
+		}
+		if (--lives == 0) {
+			if (!playVideo("S1/L01_OVER.STR", 1, false) && !_vm->shouldQuit())
+				return Common::Error(Common::kReadingFailed,
+						_("Could not play the PlayStation game-over video"));
+			return Common::kNoError;
+		}
+	}
+	return Common::kNoError;
+#else
+	return Common::Error(Common::kUnsupportedGameidError,
+			_s("Rebel Assault II PlayStation support requires TinyGL"));
+#endif
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
new file mode 100644
index 00000000000..5667d84abd9
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -0,0 +1,136 @@
+/* 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.
+ */
+
+#if !defined(SCUMM_INSANE_REBEL2_PSX_H) && defined(ENABLE_SCUMM_7_8) && defined(ENABLE_REBEL2_PSX)
+#define SCUMM_INSANE_REBEL2_PSX_H
+
+#include "common/array.h"
+#include "common/error.h"
+#include "common/hashmap.h"
+#include "common/path.h"
+#include "common/str.h"
+#include "common/types.h"
+
+#ifdef USE_TINYGL
+#include "graphics/surface.h"
+#include "graphics/tinygl/tinygl.h"
+#endif
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Scumm {
+
+class ScummEngine_v7;
+
+class RA2PSXArchive {
+public:
+	bool load(Common::SeekableReadStream &stream);
+	bool getMember(const Common::String &path, Common::Array<byte> &data) const;
+
+private:
+	struct Entry {
+		Common::String name;
+		uint32 unpackedSize;
+		uint32 offset;
+		uint32 endOffset;
+	};
+
+	bool findTopLevelEntry(const Common::String &name, Entry &entry) const;
+	bool unpack(const Entry &entry, Common::Array<byte> &data) const;
+	bool findNestedMember(const Common::Array<byte> &container, const Common::String &path,
+			Common::Array<byte> &data) const;
+	static bool decompress(const byte *source, uint32 sourceSize, uint32 expectedSize,
+			Common::Array<byte> &data);
+
+	Common::Array<byte> _data;
+};
+
+struct RA2PSXVertex {
+	int16 x;
+	int16 y;
+	int16 z;
+};
+
+struct RA2PSXFace {
+	uint16 vertex[4];
+	byte vertexCount;
+	byte r;
+	byte g;
+	byte b;
+};
+
+class RA2PSXModel {
+public:
+	RA2PSXModel();
+
+	bool load(const Common::Array<byte> &data);
+	const Common::Array<RA2PSXVertex> &vertices() const { return _vertices; }
+	const Common::Array<RA2PSXFace> &faces() const { return _faces; }
+	float radius() const { return _radius; }
+
+private:
+	bool parseModel(const Common::Array<byte> &data, uint32 modelOffset, int depth);
+	bool parseObject(const Common::Array<byte> &data, uint32 objectOffset);
+
+	Common::Array<RA2PSXVertex> _vertices;
+	Common::Array<RA2PSXFace> _faces;
+	float _radius;
+};
+
+#ifdef USE_TINYGL
+class RA2PSXTinyGLRenderer {
+public:
+	RA2PSXTinyGLRenderer();
+	~RA2PSXTinyGLRenderer();
+
+	bool init(int width, int height);
+	void beginFrame(const Graphics::Surface &background);
+	void renderModel(const RA2PSXModel &model, float x, float y, float size,
+			float pitch, float yaw, float roll);
+	void finishFrame(Graphics::Surface &surface);
+
+private:
+	TinyGL::ContextHandle *_context;
+	int _width;
+	int _height;
+};
+#endif
+
+class Rebel2PSX {
+public:
+	explicit Rebel2PSX(ScummEngine_v7 *vm);
+	~Rebel2PSX() = default;
+
+	Common::Error runGame();
+
+private:
+	enum Level1Result {
+		kLevel1Quit,
+		kLevel1Complete,
+		kLevel1Death,
+		kLevel1Error
+	};
+
+	Common::SeekableReadStream *openResource(int number);
+	Common::SeekableReadStream *openRawFile(const Common::Path &path, int discNumber);
+	bool playVideo(const Common::Path &path, int discNumber, bool version2);
+	bool loadLevel1Model(RA2PSXModel &model);
+	Level1Result playLevel1(const RA2PSXModel &model);
+
+	ScummEngine_v7 *_vm;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel2/psx/resource.cpp b/engines/scumm/insane/rebel2/psx/resource.cpp
new file mode 100644
index 00000000000..afa1f1bf443
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/resource.cpp
@@ -0,0 +1,188 @@
+/* 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.
+ */
+
+#include "common/endian.h"
+#include "common/stream.h"
+
+#include "scumm/insane/rebel2/psx/psx.h"
+
+namespace Scumm {
+
+bool RA2PSXArchive::load(Common::SeekableReadStream &stream) {
+	_data.clear();
+	if (stream.size() <= 0 || stream.size() > 64 * 1024 * 1024)
+		return false;
+	_data.resize((uint32)stream.size());
+	stream.seek(0);
+	return stream.read(_data.data(), _data.size()) == _data.size();
+}
+
+bool RA2PSXArchive::findTopLevelEntry(const Common::String &name, Entry &entry) const {
+	for (uint32 index = 0; (index + 1) * 16 <= _data.size(); ++index) {
+		const uint32 record = index * 16;
+		if (_data[record] == 0)
+			break;
+
+		Common::String entryName((const char *)_data.data() + record, 8);
+		while (!entryName.empty() && entryName.lastChar() == 0)
+			entryName.deleteLastChar();
+		if (!entryName.equalsIgnoreCase(name))
+			continue;
+
+		const uint32 nextRecord = record + 16;
+		if (nextRecord + 16 > _data.size())
+			return false;
+		entry.name = entryName;
+		entry.unpackedSize = READ_LE_UINT32(_data.data() + record + 8);
+		entry.offset = (READ_LE_UINT32(_data.data() + record + 12) & 0x7fffffffU) + record;
+		entry.endOffset = (READ_LE_UINT32(_data.data() + nextRecord + 12) & 0x7fffffffU) + nextRecord;
+		return entry.offset <= entry.endOffset && entry.endOffset <= _data.size();
+	}
+	return false;
+}
+
+bool RA2PSXArchive::decompress(const byte *source, uint32 sourceSize, uint32 expectedSize,
+		Common::Array<byte> &data) {
+	if (sourceSize < 4)
+		return false;
+
+	const byte mode = source[0];
+	const uint32 declaredSize = ((uint32)source[1] << 16) | ((uint32)source[2] << 8) | source[3];
+	if (declaredSize != expectedSize || declaredSize > 32 * 1024 * 1024)
+		return false;
+
+	data.clear();
+	data.reserve(declaredSize);
+	uint32 sourcePos = 4;
+	while (data.size() < declaredSize) {
+		if (sourcePos >= sourceSize)
+			return false;
+		const byte control = source[sourcePos++];
+		if (!(control & 0x80)) {
+			const uint32 count = (control & 0x7f) + 1;
+			if (sourcePos + count > sourceSize || data.size() + count > declaredSize)
+				return false;
+			for (uint32 i = 0; i < count; ++i)
+				data.push_back(source[sourcePos++]);
+			continue;
+		}
+
+		uint32 distance;
+		uint32 count;
+		if (mode == 1) {
+			if (sourcePos >= sourceSize)
+				return false;
+			distance = (((uint32)control & 7) << 8) | source[sourcePos++];
+			distance++;
+			count = ((control & 0x7f) >> 3) + 3;
+		} else {
+			if (sourcePos + 2 > sourceSize)
+				return false;
+			distance = READ_BE_UINT16(source + sourcePos) + 1;
+			sourcePos += 2;
+			count = (control & 0x7f) + 4;
+		}
+		if (!distance || distance > data.size() || data.size() + count > declaredSize)
+			return false;
+		for (uint32 i = 0; i < count; ++i)
+			data.push_back(data[data.size() - distance]);
+	}
+	return true;
+}
+
+bool RA2PSXArchive::unpack(const Entry &entry, Common::Array<byte> &data) const {
+	if (!entry.unpackedSize) {
+		data.clear();
+		return true;
+	}
+	return decompress(_data.data() + entry.offset, entry.endOffset - entry.offset,
+			entry.unpackedSize, data);
+}
+
+bool RA2PSXArchive::findNestedMember(const Common::Array<byte> &container,
+		const Common::String &path, Common::Array<byte> &data) const {
+	uint32 directory = 0;
+	uint32 boundary = container.size();
+	uint32 componentStart = 0;
+
+	while (componentStart < path.size()) {
+		uint32 slash = path.findFirstOf('/', componentStart);
+		if (slash == Common::String::npos)
+			slash = path.size();
+		const Common::String component = path.substr(componentStart, slash - componentStart);
+		if (component.empty())
+			return false;
+
+		bool found = false;
+		uint32 selectedOffset = 0;
+		uint32 selectedEnd = boundary;
+		for (uint32 record = directory; record + 16 <= boundary; record += 16) {
+			if (container[record] == 0)
+				break;
+			Common::String entryName((const char *)container.data() + record, 12);
+			while (!entryName.empty() && entryName.lastChar() == 0)
+				entryName.deleteLastChar();
+			const uint32 relativeOffset = READ_LE_UINT32(container.data() + record + 12);
+			const uint32 absoluteOffset = directory + relativeOffset;
+			if (absoluteOffset < directory || absoluteOffset > boundary)
+				return false;
+			if (entryName.equalsIgnoreCase(component)) {
+				found = true;
+				selectedOffset = absoluteOffset;
+			}
+		}
+		if (!found)
+			return false;
+
+		for (uint32 record = directory; record + 16 <= boundary; record += 16) {
+			if (container[record] == 0)
+				break;
+			const uint32 absoluteOffset = directory + READ_LE_UINT32(container.data() + record + 12);
+			if (absoluteOffset > selectedOffset && absoluteOffset < selectedEnd)
+				selectedEnd = absoluteOffset;
+		}
+
+		if (slash == path.size()) {
+			// Named offsets share the rest of their parent block.
+			if (boundary < selectedOffset)
+				return false;
+			data.resize(boundary - selectedOffset);
+			if (!data.empty())
+				memcpy(data.data(), container.data() + selectedOffset, data.size());
+			return true;
+		}
+
+		directory = selectedOffset;
+		boundary = selectedEnd;
+		componentStart = slash + 1;
+	}
+	return false;
+}
+
+bool RA2PSXArchive::getMember(const Common::String &path, Common::Array<byte> &data) const {
+	uint32 slash = path.findFirstOf('/');
+	const Common::String topName = slash == Common::String::npos ? path : path.substr(0, slash);
+	Entry entry;
+	if (!findTopLevelEntry(topName, entry))
+		return false;
+
+	Common::Array<byte> container;
+	if (!unpack(entry, container))
+		return false;
+	if (slash == Common::String::npos) {
+		data = container;
+		return true;
+	}
+	return findNestedMember(container, path.substr(slash + 1), data);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/video.cpp b/engines/scumm/insane/rebel2/psx/video.cpp
new file mode 100644
index 00000000000..f714b6c2f5a
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/video.cpp
@@ -0,0 +1,157 @@
+/* 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/endian.h"
+#include "common/stream.h"
+#include "common/util.h"
+
+#include "scumm/insane/rebel2/psx/video.h"
+
+namespace Scumm {
+
+static const uint32 kRawSectorSize = 2352;
+static const uint32 kFrameDataOffset = 56;
+static const byte kSectorTypeMask = 0x0e;
+static const byte kDataSectorType = 0x08;
+static const byte kVideoSectorType = 0x02;
+
+class RA2PSXVideoReadStream final : public Common::SeekableReadStream {
+public:
+	RA2PSXVideoReadStream(Common::SeekableReadStream *stream,
+			RA2PSXStreamDecoder::FrameVersion version) :
+			_stream(stream), _version(version), _position(0), _cachedSector(-1),
+			_cachedSize(0), _eos(false), _err(false) {
+	}
+
+	~RA2PSXVideoReadStream() override {
+		delete _stream;
+	}
+
+	uint32 read(void *dataPtr, uint32 dataSize) override {
+		byte *destination = static_cast<byte *>(dataPtr);
+		uint32 total = 0;
+
+		while (total < dataSize && _position < size()) {
+			const int64 sector = _position / kRawSectorSize;
+			if (!loadSector(sector))
+				break;
+
+			const uint32 sectorOffset = _position % kRawSectorSize;
+			if (sectorOffset >= _cachedSize) {
+				_err = true;
+				break;
+			}
+
+			const uint32 amount = MIN<uint32>(dataSize - total, _cachedSize - sectorOffset);
+			memcpy(destination + total, _sector + sectorOffset, amount);
+			total += amount;
+			_position += amount;
+		}
+
+		if (total < dataSize && _position >= size())
+			_eos = true;
+		return total;
+	}
+
+	bool eos() const override { return _eos; }
+	bool err() const override { return _err || _stream->err(); }
+
+	void clearErr() override {
+		_eos = false;
+		_err = false;
+		_stream->clearErr();
+	}
+
+	int64 pos() const override { return _position; }
+	int64 size() const override { return _stream->size(); }
+
+	bool seek(int64 offset, int whence = SEEK_SET) override {
+		int64 position;
+		switch (whence) {
+		case SEEK_SET:
+			position = offset;
+			break;
+		case SEEK_CUR:
+			position = _position + offset;
+			break;
+		case SEEK_END:
+			position = size() + offset;
+			break;
+		default:
+			return false;
+		}
+
+		if (position < 0 || position > size())
+			return false;
+		_position = position;
+		_eos = false;
+		return true;
+	}
+
+private:
+	bool loadSector(int64 sector) {
+		if (_cachedSector == sector)
+			return true;
+
+		if (!_stream->seek(sector * kRawSectorSize)) {
+			_err = true;
+			return false;
+		}
+
+		_cachedSize = _stream->read(_sector, kRawSectorSize);
+		if (!_cachedSize) {
+			_err = true;
+			return false;
+		}
+		_cachedSector = sector;
+
+		if (_cachedSize >= kFrameDataOffset + 12) {
+			const byte sectorType = _sector[0x12] & kSectorTypeMask;
+			if ((sectorType == kDataSectorType || sectorType == kVideoSectorType) &&
+					READ_LE_UINT16(_sector + 28) == 0) {
+				byte *frame = _sector + kFrameDataOffset;
+				frame[6] = _version;
+				frame[7] = 0;
+				SWAP(frame[8], frame[10]);
+				SWAP(frame[9], frame[11]);
+			}
+		}
+		return true;
+	}
+
+	Common::SeekableReadStream *_stream;
+	RA2PSXStreamDecoder::FrameVersion _version;
+	int64 _position;
+	int64 _cachedSector;
+	uint32 _cachedSize;
+	bool _eos;
+	bool _err;
+	byte _sector[kRawSectorSize];
+};
+
+RA2PSXStreamDecoder::RA2PSXStreamDecoder(FrameVersion version, uint32 frameCount) :
+		Video::PSXStreamDecoder(Video::PSXStreamDecoder::kCD2x, frameCount), _version(version) {
+}
+
+bool RA2PSXStreamDecoder::loadStream(Common::SeekableReadStream *stream) {
+	return Video::PSXStreamDecoder::loadStream(new RA2PSXVideoReadStream(stream, _version));
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/video.h b/engines/scumm/insane/rebel2/psx/video.h
new file mode 100644
index 00000000000..1379548c03f
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/video.h
@@ -0,0 +1,44 @@
+/* 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_PSX_VIDEO_H
+#define SCUMM_INSANE_REBEL2_PSX_VIDEO_H
+
+#include "video/psx_decoder.h"
+
+namespace Scumm {
+
+class RA2PSXStreamDecoder : public Video::PSXStreamDecoder {
+public:
+	enum FrameVersion {
+		kVersion2 = 2,
+		kVersion3 = 3
+	};
+
+	explicit RA2PSXStreamDecoder(FrameVersion version, uint32 frameCount = 0);
+	bool loadStream(Common::SeekableReadStream *stream) override;
+
+private:
+	FrameVersion _version;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index b84c9233dc1..a5ac6b9c203 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -48,6 +48,10 @@
 #include "scumm/file.h"
 #include "scumm/file_nes.h"
 
+#if defined(ENABLE_REBEL2_PSX) && !defined(ENABLE_SCUMM_7_8)
+#error "SCUMM v7 & v8 games must be enabled for Rebel Assault II PlayStation. Specify --enable-engine=scumm-7-8,rebel2-psx"
+#endif
+
 namespace Scumm {
 
 Common::Path ScummEngine::generateFilename(const int room) const {
@@ -450,6 +454,12 @@ Common::Error ScummMetaEngine::createInstance(OSystem *syst, Engine **engine,
 			res.game.midi = MDT_MACINTOSH;
 	}
 
+#ifndef ENABLE_REBEL2_PSX
+	if (res.game.id == GID_REBEL2 && res.game.platform == Common::kPlatformPSX)
+		return Common::Error(Common::kUnsupportedGameidError,
+				_s("Rebel Assault II PlayStation support is not compiled in"));
+#endif
+
 	// Finally, we have massaged the GameDescriptor to our satisfaction, and can
 	// instantiate the appropriate game engine. Hooray!
 	switch (res.game.version) {
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 2cee05abc31..66b16c4f780 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -177,6 +177,15 @@ MODULE_OBJS += \
 	smush/rebel/smush_player_ra1.o \
 	smush/rebel/smush_player_ra2.o
 
+ifdef ENABLE_REBEL2_PSX
+MODULE_OBJS += \
+	insane/rebel2/psx/level1.o \
+	insane/rebel2/psx/model.o \
+	insane/rebel2/psx/psx.o \
+	insane/rebel2/psx/resource.o \
+	insane/rebel2/psx/video.o
+endif
+
 ifdef USE_ARM_SMUSH_ASM
 MODULE_OBJS += \
 	smush/codec47ARM.o
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 3ba9d4bfe39..7b499df44fe 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 20:44:15 2026
+  This file was generated by the md5table tool on Thu Jul 23 05:28:13 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -297,6 +297,7 @@ static const MD5Table md5table[] = {
 	{ "55e4cc866ff9046824e1c638ba2b8c7f", "ft", "", "Akella", -1, Common::RU_RUS, Common::kPlatformUnknown },
 	{ "55f4e9402bec2bded383843123f37c5c", "pajama2", "HE 98.5", "", -1, Common::DE_DEU, Common::kPlatformWindows },
 	{ "566165a7338fa11029e7c14d94fa70d0", "freddi", "HE 73", "Demo", 9800, Common::EN_ANY, Common::kPlatformWindows },
+	{ "569b6160d1d56573ff54f2c908483897", "rebel2", "PlayStation", "", 720896, Common::EN_USA, Common::kPlatformPSX },
 	{ "56b5922751be7ffd771b38dda56b028b", "freddi", "HE 100", "", 34837, Common::NL_NLD, Common::kPlatformWii },
 	{ "56e8c37a0a08c3a7076f82417461a877", "indy3", "EGA", "EGA", -1, Common::EN_ANY, Common::kPlatformDOS },
 	{ "5719fc8a13b4638b78d9d8d12f091f94", "puttrace", "HE 99", "", -1, Common::FR_FRA, Common::kPlatformWindows },
@@ -581,6 +582,7 @@ static const MD5Table md5table[] = {
 	{ "ac62d50e39492ee3738b4e83a5ac780f", "freddi2", "HE 80", "", 65305, Common::NL_NLD, Common::kPlatformWindows },
 	{ "acad97ab1c6fc2a5b2d98abf6db4a190", "tentacle", "Floppy", "Floppy", -1, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "add05fa3f50d9264f47638516c9496dd", "atlantis", "", "CD", 12035, Common::RU_RUS, Common::kPlatformDOS },
+	{ "ae607f6ed270311d7dbc033d3e943d3e", "rebel2", "PlayStation", "", 750723120, Common::EN_USA, Common::kPlatformPSX },
 	{ "ae94f110a14ce71fc515d5b648827a8f", "tentacle", "Floppy", "Floppy", 7932, Common::ES_ESP, Common::kPlatformDOS },
 	{ "aeec382acef62e122bf0d5b14581cfa4", "zak", "V1", "", 1914, Common::IT_ITA, Common::kPlatformC64 },
 	{ "aef415cc5dc063e3668359c2657169f3", "PuttTime", "HE 99", "Mini Game", 18458, Common::DE_DEU, Common::kPlatformWindows },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 5fc86950157..cb0f5bc1b1a 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -55,6 +55,9 @@
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
 #include "scumm/insane/rebel2/rebel.h"
+#ifdef ENABLE_REBEL2_PSX
+#include "scumm/insane/rebel2/psx/psx.h"
+#endif
 #include "scumm/insane/rebel1/rebel.h"
 #include "scumm/he/animation_he.h"
 #include "scumm/he/font_he.h"
@@ -145,6 +148,10 @@ ScummEngine::ScummEngine(OSystem *syst, const DetectorResult &dr)
 			_gdi = new GdiPCEngine(this);
 		else if (_game.heversion > 0)
 			_gdi = new GdiHE16bit(this);
+#ifdef ENABLE_REBEL2_PSX
+		else if (_game.id == GID_REBEL2 && _game.platform == Common::kPlatformPSX)
+			_gdi = new Gdi(this);
+#endif
 	} else
 #endif
 	if (_game.heversion > 0) {
@@ -401,6 +408,10 @@ ScummEngine::ScummEngine(OSystem *syst, const DetectorResult &dr)
 		// #15666, #11290, and <https://forums.scummvm.org/viewtopic.php?p=97395#p97395>).
 		if (_game.id == GID_LOOM || !ConfMan.getBool("trim_fmtowns_to_200_pixels"))
 			_screenHeight = 240;
+#ifdef ENABLE_REBEL2_PSX
+	} else if (_game.id == GID_REBEL2 && _game.platform == Common::kPlatformPSX) {
+		_screenHeight = 240;
+#endif
 	} else if (_game.id == GID_REBEL2 && ConfMan.getBool("rebel2_hires")) {
 		_screenWidth = 640;
 		_screenHeight = 400;
@@ -962,6 +973,9 @@ ScummEngine_v7::~ScummEngine_v7() {
 	}
 
 	delete _insane;
+#ifdef ENABLE_REBEL2_PSX
+	delete _rebel2PSX;
+#endif
 	delete _textV7;
 	delete[] _guiStringTransBuff;
 
@@ -1834,6 +1848,15 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 	}
 
 	if (_game.id == GID_REBEL2) {
+#ifdef ENABLE_REBEL2_PSX
+		if (_game.platform == Common::kPlatformPSX) {
+			_useOriginalGUI = false;
+			_musicEngine = _imuseDigital = nullptr;
+			_rebel2PSX = new Rebel2PSX(this);
+			return;
+		}
+#endif
+
 		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
 		initScreens(0, _screenHeight);
 
@@ -1854,7 +1877,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 		_useOriginalGUI = false;
 
 		_sound = new Sound(this, _mixer, false);
-		// Rebel Assault 2 doesn't use iMUSE for audio - audio is handled directly by INSANE
+		// Rebel Assault 2 doesn't use iMUSE for audio.
 		_musicEngine = _imuseDigital = nullptr;
 		_insane = new InsaneRebel2(this);
 		_splayer = new SmushPlayerRebel2(this, nullptr, _insane);
@@ -2738,6 +2761,10 @@ Common::Error ScummEngine::go() {
 
 	if (_game.id == GID_REBEL2) {
 		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
+#ifdef ENABLE_REBEL2_PSX
+		if (_game.platform == Common::kPlatformPSX)
+			return vm7->getRebel2PSX()->runGame();
+#endif
 		InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
 		rebel->runGame();
 		return Common::kNoError;
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index 5e96c695c8c..0e5aa90007b 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -31,6 +31,9 @@
 namespace Scumm {
 
 class Insane;
+#ifdef ENABLE_REBEL2_PSX
+class Rebel2PSX;
+#endif
 class SmushMixer;
 class SmushPlayer;
 class TextRenderer_v7;
@@ -40,6 +43,9 @@ class ScummEngine_v7 : public ScummEngine_v6 {
 	friend class Insane;
 	friend class InsaneRebel1;
 	friend class InsaneRebel2;
+#ifdef ENABLE_REBEL2_PSX
+	friend class Rebel2PSX;
+#endif
 public:
 	ScummEngine_v7(OSystem *syst, const DetectorResult &dr);
 	~ScummEngine_v7() override;
@@ -57,6 +63,9 @@ protected:
 	bool _smushActive = false;
 
 	Insane *_insane = nullptr;
+#ifdef ENABLE_REBEL2_PSX
+	Rebel2PSX *_rebel2PSX = nullptr;
+#endif
 
 public:
 	void syncSoundSettings() override;
@@ -107,6 +116,9 @@ public:
 	bool isSmushActive() override { return _smushActive; }
 	bool isInsaneActive() override { return _insane ? _insane->isInsaneActive() : false; }
 	Insane *getInsane() { return _insane; }
+#ifdef ENABLE_REBEL2_PSX
+	Rebel2PSX *getRebel2PSX() { return _rebel2PSX; }
+#endif
 	void removeBlastTexts() override;
 	void restoreBlastTextsRects();
 




More information about the Scummvm-git-logs mailing list