[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