[Scummvm-git-logs] scummvm master -> 1fb9617cfa5a5df05f416d1fee213612ba194710
neuromancer
noreply at scummvm.org
Fri Jul 24 11:32:55 UTC 2026
This automated email contains information about 11 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
2652f526db SCUMM: RA2: load cockpit for level 1 in psx
fc0f6f32cc SCUMM: RA2: basic gamepad controls and laser rendering in psx
0d7833eaaa SCUMM: RA2: third person ship view in psx
ce8263d11f SCUMM: RA2: load textures in psx
f1ee431e61 SCUMM: RA2: render all the triangles for the psx release
8875e80135 SCUMM: RA2: improved sound support for psx release
4b8f11a32a SCUMM: RA2: added initial support for main meny in the psx release
a21f477759 PLAYGROUND3D: Added a quad strip test
c81277189f TINYGL: Fix deferred quad strip rendering
91bd24ea24 SCUMM: RA2: font rendering support for psx release
1fb9617cfa SCUMM: RA2: re-use common L1 code for psx release
Commit: 2652f526dbe17c63c7ca851b533d2d0b74094c4c
https://github.com/scummvm/scummvm/commit/2652f526dbe17c63c7ca851b533d2d0b74094c4c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: load cockpit for level 1 in psx
Changed paths:
A engines/scumm/insane/rebel2/psx/ui.cpp
A engines/scumm/insane/rebel2/psx/ui.h
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/resource.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index ceae659a063..035dba8a892 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -28,6 +28,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/ui.h"
#include "scumm/insane/rebel2/psx/video.h"
#include <math.h>
@@ -109,14 +110,11 @@ static void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &ran
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);
+static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
+ const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions,
+ int aimX, int aimY, int fireFrames) {
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) {
@@ -125,57 +123,26 @@ static void drawLevel1Overlay(Graphics::Surface &surface, const RA2PSXLevel1Enem
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 (explosions[i].frames > 0)
+ ui.drawExplosion(surface, explosions[i].x, explosions[i].y,
+ 10 - explosions[i].frames);
}
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) {
+Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
+ const RA2PSXModel &crosshair, const RA2PSXLevel1UI &ui, int lives, int &score) {
#ifndef USE_TINYGL
(void)model;
+ (void)crosshair;
+ (void)ui;
+ (void)lives;
+ (void)score;
return kLevel1Error;
#else
Common::SeekableReadStream *stream = openRawFile("S1/L01_PLAY.STR", 1);
@@ -196,11 +163,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
RA2PSXLevel1Enemy enemies[3];
RA2PSXLevel1Explosion explosions[3];
- int aimX = _vm->_screenWidth / 2;
- int aimY = _vm->_screenHeight / 2;
+ int aimX = 160;
+ int aimY = 113;
int shield = 100;
- int kills = 0;
- int misses = 0;
int spawnDelay = 0;
int spawnRange = 80;
int spawnBase = 60;
@@ -208,8 +173,6 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
int lastFrame = -1;
int lastShotFrame = -10;
int fireFrames = 0;
- int hitFrames = 0;
- int damageFrames = 0;
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
@@ -229,12 +192,12 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
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);
+ aimX = CLIP<int>(event.mouse.x, 30, 290);
+ aimY = CLIP<int>(event.mouse.y, 48, 178);
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);
+ aimX = CLIP<int>(event.mouse.x, 30, 290);
+ aimY = CLIP<int>(event.mouse.y, 48, 178);
mouseFire = true;
fireRequested = true;
break;
@@ -298,9 +261,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
while (lastFrame < currentFrame && shield > 0) {
++lastFrame;
aimX = CLIP<int>(aimX + ((int)moveRight - (int)moveLeft) * 6,
- 14, _vm->_screenWidth - 15);
+ 30, 290);
aimY = CLIP<int>(aimY + ((int)moveDown - (int)moveUp) * 6,
- 14, _vm->_screenHeight - 18);
+ 48, 178);
if (lastFrame >= nextSpawnAdjustment) {
nextSpawnAdjustment = lastFrame + 20;
@@ -324,10 +287,6 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
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;
@@ -340,18 +299,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
updateLevel1Enemy(enemies[i]);
if (enemies[i].age == enemies[i].fireFrame) {
enemies[i].laserFrames = 4;
- if (_vm->_rnd.getRandomNumber(99) < 38) {
+ 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) {
+ if (_vm->_rnd.getRandomNumber(99) < 18)
shield = MAX(0, shield - 12);
- damageFrames = 4;
- }
}
}
}
@@ -383,11 +337,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
}
if (hitEnemy >= 0) {
enemies[hitEnemy].active = false;
- ++kills;
- hitFrames = 3;
+ score = MIN(9999999, score + 100);
for (int i = 0; i < 3; ++i) {
if (explosions[i].frames == 0) {
- explosions[i].frames = 8;
+ explosions[i].frames = 10;
explosions[i].x = (int)enemies[hitEnemy].x;
explosions[i].y = (int)enemies[hitEnemy].y;
break;
@@ -402,10 +355,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model) {
renderer.renderModel(model, enemies[i].x, enemies[i].y, enemies[i].size,
enemies[i].pitch, enemies[i].yaw, enemies[i].roll);
}
+ renderer.renderModel(crosshair, aimX, aimY, 31.0f,
+ (aimY - 113) * 0.12f, -(aimX - 160) * 0.10f, 0.0f, false);
Graphics::Surface output;
renderer.finishFrame(output);
- drawLevel1Overlay(output, enemies, explosions, aimX, aimY, shield, kills, misses,
- currentFrame, fireFrames, hitFrames, damageFrames);
+ drawLevel1Effects(output, ui, enemies, explosions, aimX, aimY, fireFrames);
+ ui.drawCockpit(output);
+ ui.drawHUD(output, score, lives, shield, currentFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
g_system->updateScreen();
}
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index f03027fab8b..aadde528877 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -208,10 +208,12 @@ void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
}
void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float y, float size,
- float pitch, float yaw, float roll) {
+ float pitch, float yaw, float roll, bool depthTest) {
if (!_context || model.vertices().empty())
return;
TinyGL::setContext(_context);
+ if (!depthTest)
+ tglDisable(TGL_DEPTH_TEST);
tglMatrixMode(TGL_MODELVIEW);
tglPushMatrix();
tglTranslatef(x, y, 0.0f);
@@ -234,6 +236,8 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
tglEnd();
}
tglPopMatrix();
+ if (!depthTest)
+ tglEnable(TGL_DEPTH_TEST);
}
void RA2PSXTinyGLRenderer::finishFrame(Graphics::Surface &surface) {
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index cd39c3552de..d78be61c0df 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -28,6 +28,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/ui.h"
#include "scumm/insane/rebel2/psx/video.h"
namespace Scumm {
@@ -113,7 +114,8 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
return !_vm->shouldQuit();
}
-bool Rebel2PSX::loadLevel1Model(RA2PSXModel &model) {
+bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
+ RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -124,13 +126,18 @@ bool Rebel2PSX::loadLevel1Model(RA2PSXModel &model) {
return false;
Common::Array<byte> modelData;
- return archive.getMember("fOFS/TieFighter/main", modelData) && model.load(modelData);
+ Common::Array<byte> crosshairData;
+ return archive.getMember("fOFS/TieFighter/main", modelData) && model.load(modelData) &&
+ archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
+ ui.load(archive);
}
Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
RA2PSXModel model;
- if (!loadLevel1Model(model))
+ RA2PSXModel crosshair;
+ RA2PSXLevel1UI ui;
+ if (!loadLevel1Assets(model, crosshair, ui))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
@@ -142,8 +149,9 @@ Common::Error Rebel2PSX::runGame() {
}
int lives = 3;
+ int score = 0;
while (!_vm->shouldQuit()) {
- const Level1Result result = playLevel1(model);
+ const Level1Result result = playLevel1(model, crosshair, ui, lives, score);
if (result == kLevel1Quit)
return Common::kNoError;
if (result == kLevel1Error)
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 5667d84abd9..38aca961b99 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -32,6 +32,7 @@ class SeekableReadStream;
namespace Scumm {
class ScummEngine_v7;
+class RA2PSXLevel1UI;
class RA2PSXArchive {
public:
@@ -97,7 +98,7 @@ public:
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);
+ float pitch, float yaw, float roll, bool depthTest = true);
void finishFrame(Graphics::Surface &surface);
private:
@@ -125,8 +126,9 @@ private:
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);
+ bool loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair, RA2PSXLevel1UI &ui);
+ Level1Result playLevel1(const RA2PSXModel &model, const RA2PSXModel &crosshair,
+ const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
};
diff --git a/engines/scumm/insane/rebel2/psx/resource.cpp b/engines/scumm/insane/rebel2/psx/resource.cpp
index afa1f1bf443..4c397da44ba 100644
--- a/engines/scumm/insane/rebel2/psx/resource.cpp
+++ b/engines/scumm/insane/rebel2/psx/resource.cpp
@@ -132,8 +132,8 @@ bool RA2PSXArchive::findNestedMember(const Common::Array<byte> &container,
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)
+ const uint32 absoluteOffset = record + relativeOffset;
+ if (absoluteOffset < record || absoluteOffset > boundary)
return false;
if (entryName.equalsIgnoreCase(component)) {
found = true;
@@ -146,7 +146,7 @@ bool RA2PSXArchive::findNestedMember(const Common::Array<byte> &container,
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);
+ const uint32 absoluteOffset = record + READ_LE_UINT32(container.data() + record + 12);
if (absoluteOffset > selectedOffset && absoluteOffset < selectedEnd)
selectedEnd = absoluteOffset;
}
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
new file mode 100644
index 00000000000..a34df27bfc9
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -0,0 +1,283 @@
+/* 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/util.h"
+
+#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/ui.h"
+
+namespace Scumm {
+
+struct RA2PSXUIColor {
+ int r;
+ int g;
+ int b;
+};
+
+struct RA2PSXUIGradientStop {
+ int index;
+ RA2PSXUIColor color;
+};
+
+static RA2PSXUIColor interpolateColor(const RA2PSXUIColor &from,
+ const RA2PSXUIColor &to, int value, int maximum) {
+ if (maximum <= 0)
+ return from;
+ RA2PSXUIColor color;
+ color.r = from.r + (to.r - from.r) * value / maximum;
+ color.g = from.g + (to.g - from.g) * value / maximum;
+ color.b = from.b + (to.b - from.b) * value / maximum;
+ return color;
+}
+
+static RA2PSXUIColor shieldColor(const RA2PSXUIGradientStop *stops, uint count, int index) {
+ for (uint i = 1; i < count; ++i) {
+ if (index <= stops[i].index)
+ return interpolateColor(stops[i - 1].color, stops[i].color,
+ index - stops[i - 1].index, stops[i].index - stops[i - 1].index);
+ }
+ return stops[count - 1].color;
+}
+
+bool RA2PSXLevel1UI::loadTextures(const Common::Array<byte> &data) {
+ const uint initialCount = _textures.size();
+ uint32 offset = 0;
+ while (offset + 20 <= data.size()) {
+ Common::String name;
+ for (uint i = 0; i < 8 && data[offset + i]; ++i) {
+ if (data[offset + i] < 0x20 || data[offset + i] >= 0x7f)
+ return _textures.size() > initialCount;
+ name += (char)data[offset + i];
+ }
+ if (name.empty())
+ break;
+
+ const uint16 recordSize = READ_LE_UINT16(data.data() + offset + 12);
+ const uint16 widthField = READ_LE_UINT16(data.data() + offset + 16);
+ const uint16 heightField = READ_LE_UINT16(data.data() + offset + 18);
+ const uint16 width = (widthField & 0xff) ? widthField & 0xff : 256;
+ const uint16 height = (heightField & 0xff) ? heightField & 0xff : 256;
+ const bool eightBit = (widthField & 0x100) != 0;
+ const uint32 paletteColors = eightBit ? 256 : 16;
+ const uint32 pixelCount = (uint32)width * height;
+ const uint32 pixelBytes = eightBit ? pixelCount : (pixelCount + 1) / 2;
+ const uint32 paletteOffset = offset + 20;
+ const uint32 pixelsOffset = paletteOffset + paletteColors * 2;
+ if (recordSize < 20 || offset + recordSize > data.size() ||
+ pixelsOffset + pixelBytes > offset + recordSize)
+ break;
+
+ Texture texture;
+ texture.name = name;
+ texture.width = width;
+ texture.height = height;
+ texture.pixels.resize(pixelCount);
+ for (uint32 i = 0; i < pixelCount; ++i) {
+ const byte packed = data[pixelsOffset + (eightBit ? i : i / 2)];
+ const byte paletteIndex = eightBit ? packed : ((i & 1) ? packed >> 4 : packed & 0xf);
+ const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
+ if (!value) {
+ texture.pixels[i] = 0;
+ continue;
+ }
+
+ const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
+ const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
+ const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
+ texture.pixels[i] = 0x01000000 | (r << 16) | (g << 8) | b;
+ }
+ _textures.push_back(texture);
+ offset += recordSize;
+ }
+ return _textures.size() > initialCount;
+}
+
+const RA2PSXLevel1UI::Texture *RA2PSXLevel1UI::findTexture(const char *name) const {
+ for (uint i = 0; i < _textures.size(); ++i) {
+ if (_textures[i].name.equalsIgnoreCase(name))
+ return &_textures[i];
+ }
+ return nullptr;
+}
+
+bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
+ Common::Array<byte> data;
+ _textures.clear();
+ if (!archive.getMember("tex/Common", data) || !loadTextures(data))
+ return false;
+ if (!archive.getMember("tex/BWingCockp", data) || !loadTextures(data))
+ return false;
+
+ static const char *const required[] = {
+ "COCKPITL", "COCKPITR", "PANEL1", "PANEL2", "PANEL3", "STATTEXT",
+ "FONT8X9", "REBLSIGN", "ENRGYMSK", "SMALLEX"
+ };
+ for (uint i = 0; i < ARRAYSIZE(required); ++i) {
+ if (!findTexture(required[i]))
+ return false;
+ }
+ return true;
+}
+
+void RA2PSXLevel1UI::drawTexture(Graphics::Surface &surface, const char *name,
+ int x, int y, const Common::Rect &source, int brightness, BlendMode blend) const {
+ const Texture *texture = findTexture(name);
+ if (!texture)
+ return;
+
+ int sourceLeft = MAX<int>(0, source.left);
+ int sourceTop = MAX<int>(0, source.top);
+ int sourceRight = MIN<int>(texture->width, source.right);
+ int sourceBottom = MIN<int>(texture->height, source.bottom);
+ x += sourceLeft - source.left;
+ y += sourceTop - source.top;
+ if (x < 0) {
+ sourceLeft -= x;
+ x = 0;
+ }
+ if (y < 0) {
+ sourceTop -= y;
+ y = 0;
+ }
+ sourceRight = MIN<int>(sourceRight, sourceLeft + surface.w - x);
+ sourceBottom = MIN<int>(sourceBottom, sourceTop + surface.h - y);
+ if (sourceLeft >= sourceRight || sourceTop >= sourceBottom)
+ return;
+
+ brightness = CLIP<int>(brightness, 0, 0xff);
+ for (int sourceY = sourceTop; sourceY < sourceBottom; ++sourceY) {
+ const int destY = y + sourceY - sourceTop;
+ for (int sourceX = sourceLeft; sourceX < sourceRight; ++sourceX) {
+ const uint32 pixel = texture->pixels[sourceY * texture->width + sourceX];
+ if (!(pixel & 0x01000000))
+ continue;
+
+ int r = ((pixel >> 16) & 0xff) * brightness / 0x80;
+ int g = ((pixel >> 8) & 0xff) * brightness / 0x80;
+ int b = (pixel & 0xff) * brightness / 0x80;
+ r = MIN(r, 0xff);
+ g = MIN(g, 0xff);
+ b = MIN(b, 0xff);
+ const int destX = x + sourceX - sourceLeft;
+ if (blend == kBlendAdditive) {
+ byte destR, destG, destB;
+ surface.format.colorToRGB(surface.getPixel(destX, destY), destR, destG, destB);
+ r = MIN<int>(0xff, r + destR);
+ g = MIN<int>(0xff, g + destG);
+ b = MIN<int>(0xff, b + destB);
+ }
+ surface.setPixel(destX, destY, surface.format.RGBToColor(r, g, b));
+ }
+ }
+}
+
+void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2 + 120;
+ drawTexture(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
+ drawTexture(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
+}
+
+void RA2PSXLevel1UI::drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const {
+ const int stage = CLIP<int>(frame / 2, 0, 4);
+ drawTexture(surface, "SMALLEX", x - 8, y - 8,
+ Common::Rect(stage * 16, 0, stage * 16 + 16, 16), 0x80, kBlendAdditive);
+}
+
+void RA2PSXLevel1UI::drawShield(Graphics::Surface &surface, int shield,
+ int xOffset, int yOffset) const {
+ static const RA2PSXUIGradientStop topLeft[] = {
+ { 0, { 0, 83, 0 } }, { 14, { 171, 147, 0 } }, { 31, { 63, 0, 0 } }
+ };
+ static const RA2PSXUIGradientStop topRight[] = {
+ { 0, { 51, 151, 0 } }, { 6, { 207, 183, 0 } },
+ { 16, { 202, 0, 0 } }, { 31, { 155, 0, 0 } }
+ };
+ static const RA2PSXUIGradientStop bottomLeft[] = {
+ { 0, { 0, 159, 0 } }, { 13, { 246, 214, 0 } }, { 31, { 183, 0, 0 } }
+ };
+ static const RA2PSXUIGradientStop bottomRight[] = {
+ { 0, { 226, 207, 0 } }, { 15, { 219, 24, 0 } }, { 31, { 139, 3, 0 } }
+ };
+
+ shield = CLIP<int>(shield, 0, 100);
+ const int width = shield * 68 / 100;
+ if (!width)
+ return;
+ const int colorIndex = CLIP<int>(32 - shield * 32 / 100, 0, 31);
+ const RA2PSXUIColor tl = shieldColor(topLeft, ARRAYSIZE(topLeft), colorIndex);
+ const RA2PSXUIColor tr = shieldColor(topRight, ARRAYSIZE(topRight), colorIndex);
+ const RA2PSXUIColor bl = shieldColor(bottomLeft, ARRAYSIZE(bottomLeft), colorIndex);
+ const RA2PSXUIColor br = shieldColor(bottomRight, ARRAYSIZE(bottomRight), colorIndex);
+
+ for (int row = 0; row < 9; ++row) {
+ const int left = xOffset + 297 - width - row;
+ const int right = xOffset + 288;
+ const int y = yOffset + 213 + row;
+ if (left >= right || y < 0 || y >= surface.h)
+ continue;
+ const RA2PSXUIColor leftColor = interpolateColor(tl, bl, row, 8);
+ const RA2PSXUIColor rightColor = interpolateColor(tr, br, row, 8);
+ for (int x = MAX(0, left); x < MIN<int>(right, surface.w); ++x) {
+ const RA2PSXUIColor color = interpolateColor(leftColor, rightColor,
+ x - left, right - left - 1);
+ surface.setPixel(x, y, surface.format.RGBToColor(color.r, color.g, color.b));
+ }
+
+ const int sourceLeft = 77 - width - row;
+ if (sourceLeft < 68)
+ drawTexture(surface, "ENRGYMSK", left, y,
+ Common::Rect(MAX(0, sourceLeft), row, 68, row + 1), 0x50, kBlendAdditive);
+ }
+}
+
+void RA2PSXLevel1UI::drawHUD(Graphics::Surface &surface, int score, int lives,
+ int shield, int frame) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+
+ drawTexture(surface, "PANEL3", xOffset + 2, yOffset + 17, Common::Rect(0, 0, 126, 21));
+ drawTexture(surface, "PANEL2", xOffset + 262, yOffset + 17, Common::Rect(0, 0, 56, 29));
+ drawTexture(surface, "PANEL1", xOffset + 210, yOffset + 207, Common::Rect(0, 0, 108, 21));
+ drawShield(surface, shield, xOffset, yOffset);
+
+ drawTexture(surface, "STATTEXT", xOffset + 14, yOffset + 21,
+ Common::Rect(42, 0, 62, 13), 0x5a);
+ score = CLIP<int>(score, 0, 9999999);
+ int divisor = 1000000;
+ for (int digit = 0; digit < 7; ++digit) {
+ const int value = score / divisor % 10;
+ drawTexture(surface, "FONT8X9", xOffset + 39 + digit * 10, yOffset + 23,
+ Common::Rect(value * 8, 0, value * 8 + 8, 9));
+ divisor /= 10;
+ }
+
+ const int rebelFrame = (MAX(frame, 0) / 2) % 12;
+ drawTexture(surface, "REBLSIGN", xOffset + 292, yOffset + 22,
+ Common::Rect((rebelFrame % 6) * 20, (rebelFrame / 6) * 19,
+ (rebelFrame % 6 + 1) * 20, (rebelFrame / 6 + 1) * 19));
+ const int reserveLives = CLIP<int>(lives - 1, 0, 9);
+ drawTexture(surface, "FONT8X9", xOffset + 274, yOffset + 28,
+ Common::Rect(reserveLives * 8, 0, reserveLives * 8 + 8, 9));
+ drawTexture(surface, "FONT8X9", xOffset + 283, yOffset + 29, Common::Rect(80, 0, 88, 9));
+
+ int shieldLabelBrightness = 0x5a;
+ if (shield <= 31) {
+ const int phase = (MAX(frame, 0) / 2) % 14;
+ shieldLabelBrightness += phase < 7 ? -50 + phase * 10 : 20 - (phase - 7) * 10;
+ }
+ drawTexture(surface, "STATTEXT", xOffset + 291, yOffset + 211,
+ Common::Rect(0, 0, 20, 13), shieldLabelBrightness);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
new file mode 100644
index 00000000000..1adbe99831f
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -0,0 +1,60 @@
+/* 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.
+ */
+
+#ifndef SCUMM_INSANE_REBEL2_PSX_UI_H
+#define SCUMM_INSANE_REBEL2_PSX_UI_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+#include "common/types.h"
+
+#include "graphics/surface.h"
+
+namespace Scumm {
+
+class RA2PSXArchive;
+
+class RA2PSXLevel1UI {
+public:
+ bool load(const RA2PSXArchive &archive);
+
+ void drawCockpit(Graphics::Surface &surface) const;
+ void drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const;
+ void drawHUD(Graphics::Surface &surface, int score, int lives, int shield, int frame) const;
+
+private:
+ enum BlendMode {
+ kBlendOpaque,
+ kBlendAdditive
+ };
+
+ struct Texture {
+ Common::String name;
+ uint16 width;
+ uint16 height;
+ Common::Array<uint32> pixels;
+ };
+
+ bool loadTextures(const Common::Array<byte> &data);
+ const Texture *findTexture(const char *name) const;
+ void drawTexture(Graphics::Surface &surface, const char *name,
+ int x, int y, const Common::Rect &source, int brightness = 0x80,
+ BlendMode blend = kBlendOpaque) const;
+ void drawShield(Graphics::Surface &surface, int shield, int xOffset, int yOffset) const;
+
+ Common::Array<Texture> _textures;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 66b16c4f780..c91e53f3756 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -183,6 +183,7 @@ MODULE_OBJS += \
insane/rebel2/psx/model.o \
insane/rebel2/psx/psx.o \
insane/rebel2/psx/resource.o \
+ insane/rebel2/psx/ui.o \
insane/rebel2/psx/video.o
endif
Commit: fc0f6f32ccac888d02a85c778f94e3df52099863
https://github.com/scummvm/scummvm/commit/fc0f6f32ccac888d02a85c778f94e3df52099863
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: basic gamepad controls and laser rendering in psx
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 035dba8a892..b559ef6315a 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/random.h"
#include "common/system.h"
@@ -69,6 +70,23 @@ struct RA2PSXLevel1Explosion {
int y;
};
+struct RA2PSXLevel1Shot {
+ RA2PSXLevel1Shot() : active(false), progress(0), targetX(0), targetY(0) {}
+
+ bool active;
+ int progress;
+ float targetX;
+ float targetY;
+};
+
+static const float kLevel1LaserStart[3][3] = {
+ { -350.0f, 200.0f, 400.0f },
+ { 350.0f, 200.0f, 400.0f },
+ { 0.0f, 500.0f, 400.0f }
+};
+
+static const float kLevel1LaserRoll[3] = { -45.0f, 45.0f, 0.0f };
+
static void updateLevel1Enemy(RA2PSXLevel1Enemy &enemy) {
const float t = MIN(1.0f, (float)enemy.age / enemy.lifetime);
const float inverse = 1.0f - t;
@@ -110,10 +128,106 @@ static void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &ran
updateLevel1Enemy(enemy);
}
+static void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
+ int &directionX, int &directionY, bool left, bool right, bool up, bool down) {
+ if (left && right)
+ left = right = false;
+ if (up && down)
+ up = down = false;
+
+ if (!left && !right) {
+ directionX = 0;
+ velocityX /= 2;
+ } else {
+ if (left) {
+ if (velocityX > 0)
+ velocityX = -velocityX / 4;
+ velocityX = MAX(-4096, velocityX - 448);
+ directionX = -1;
+ }
+ if (right) {
+ if (velocityX < 0)
+ velocityX = -velocityX / 4;
+ velocityX = MIN(4096, velocityX + 448);
+ directionX = 1;
+ }
+ if ((up || down) && ABS(velocityX) < ABS(velocityY) / 2)
+ velocityX = ABS(velocityY) * directionX / 2;
+ }
+ x = CLIP<int>(x + velocityX / 512, 30, 290);
+
+ if (!up && !down) {
+ directionY = 0;
+ velocityY /= 2;
+ } else {
+ if (up) {
+ if (velocityY > 0)
+ velocityY = -velocityY / 4;
+ velocityY = MAX(-4096, velocityY - 448);
+ directionY = -1;
+ }
+ if (down) {
+ if (velocityY < 0)
+ velocityY = -velocityY / 4;
+ velocityY = MIN(4096, velocityY + 448);
+ directionY = 1;
+ }
+ if ((left || right) && ABS(velocityX) / 2 > ABS(velocityY))
+ velocityY = ABS(velocityX) * directionY / 2;
+ }
+ y = CLIP<int>(y + velocityY / 512, 48, 178);
+}
+
+static void spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY) {
+ int slot = -1;
+ for (int i = 0; i < 8; ++i) {
+ if (!shots[i].active) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0)
+ return;
+
+ shots[slot].active = true;
+ shots[slot].progress = 400;
+ shots[slot].targetX = (aimX - 160) * 18000.0f / 640.0f;
+ shots[slot].targetY = (aimY - 120) * 18000.0f / 640.0f;
+}
+
+static void updateLevel1Shots(RA2PSXLevel1Shot *shots) {
+ for (int i = 0; i < 8; ++i) {
+ if (!shots[i].active)
+ continue;
+ shots[i].progress += 200;
+ if (shots[i].progress > 4399)
+ shots[i].active = false;
+ }
+}
+
+static void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &laser,
+ const RA2PSXLevel1Shot *shots) {
+ for (int shotIndex = 0; shotIndex < 8; ++shotIndex) {
+ const RA2PSXLevel1Shot &shot = shots[shotIndex];
+ if (!shot.active || shot.progress >= 4000)
+ continue;
+ const float progress = shot.progress / 4096.0f;
+ for (int laserIndex = 0; laserIndex < 3; ++laserIndex) {
+ const float *start = kLevel1LaserStart[laserIndex];
+ const float directionX = shot.targetX - start[0];
+ const float directionY = shot.targetY - start[1];
+ const float directionZ = 18000.0f - start[2];
+ renderer.renderPerspectiveModel(laser,
+ start[0] + directionX * progress,
+ start[1] + directionY * progress,
+ start[2] + directionZ * progress,
+ directionX, directionY, directionZ, kLevel1LaserRoll[laserIndex], false);
+ }
+ }
+}
+
static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
- const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions,
- int aimX, int aimY, int fireFrames) {
- const uint32 red = surface.format.RGBToColor(255, 48, 32);
+ const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions) {
const uint32 green = surface.format.RGBToColor(64, 255, 96);
for (int i = 0; i < 3; ++i) {
@@ -127,19 +241,16 @@ static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &
ui.drawExplosion(surface, explosions[i].x, explosions[i].y,
10 - explosions[i].frames);
}
-
- 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);
- }
}
#endif
Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
- const RA2PSXModel &crosshair, const RA2PSXLevel1UI &ui, int lives, int &score) {
+ const RA2PSXModel &crosshair, const RA2PSXModel &laser,
+ const RA2PSXLevel1UI &ui, int lives, int &score) {
#ifndef USE_TINYGL
(void)model;
(void)crosshair;
+ (void)laser;
(void)ui;
(void)lives;
(void)score;
@@ -163,24 +274,37 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
RA2PSXLevel1Enemy enemies[3];
RA2PSXLevel1Explosion explosions[3];
+ RA2PSXLevel1Shot shots[8];
int aimX = 160;
int aimY = 113;
+ int aimVelocityX = 0;
+ int aimVelocityY = 0;
+ int aimDirectionX = 0;
+ int aimDirectionY = 0;
int shield = 100;
int spawnDelay = 0;
int spawnRange = 80;
int spawnBase = 60;
int nextSpawnAdjustment = 0;
int lastFrame = -1;
- int lastShotFrame = -10;
- int fireFrames = 0;
+ int lastShotFrame = -4;
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
bool moveDown = false;
+ bool actionLeft = false;
+ bool actionRight = false;
+ bool actionUp = false;
+ bool actionDown = false;
+ int joystickAxisX = 0;
+ int joystickAxisY = 0;
bool mouseFire = false;
bool keyFire = false;
+ bool actionFire = false;
bool fireRequested = false;
Level1Result result = kLevel1Complete;
+ const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
+ MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
const bool cursorWasVisible = CursorMan.isVisible();
CursorMan.showMouse(false);
@@ -194,6 +318,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
case Common::EVENT_MOUSEMOVE:
aimX = CLIP<int>(event.mouse.x, 30, 290);
aimY = CLIP<int>(event.mouse.y, 48, 178);
+ aimVelocityX = aimVelocityY = 0;
+ aimDirectionX = aimDirectionY = 0;
break;
case Common::EVENT_LBUTTONDOWN:
aimX = CLIP<int>(event.mouse.x, 30, 290);
@@ -238,6 +364,61 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
event.kbd.keycode == Common::KEYCODE_RETURN)
keyFire = false;
break;
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_END: {
+ const bool pressed = event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START;
+ switch (event.customType) {
+ case kScummActionInsaneLeft:
+ actionLeft = pressed;
+ break;
+ case kScummActionInsaneRight:
+ actionRight = pressed;
+ break;
+ case kScummActionInsaneUp:
+ actionUp = pressed;
+ break;
+ case kScummActionInsaneDown:
+ actionDown = pressed;
+ break;
+ case kScummActionInsaneAttack:
+ actionFire = pressed;
+ if (pressed)
+ fireRequested = true;
+ break;
+ case kScummActionInsaneBack:
+ if (pressed)
+ result = kLevel1Quit;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS: {
+ const int axisPosition = event.joystick.position == Common::JOYAXIS_MIN ?
+ Common::JOYAXIS_MAX : event.joystick.position;
+ switch (event.customType) {
+ case kScummBackendActionRebel2AxisUp:
+ if (event.joystick.position != 0 || joystickAxisY <= 0)
+ joystickAxisY = -axisPosition;
+ break;
+ case kScummBackendActionRebel2AxisDown:
+ if (event.joystick.position != 0 || joystickAxisY >= 0)
+ joystickAxisY = axisPosition;
+ break;
+ case kScummBackendActionRebel2AxisLeft:
+ if (event.joystick.position != 0 || joystickAxisX <= 0)
+ joystickAxisX = -axisPosition;
+ break;
+ case kScummBackendActionRebel2AxisRight:
+ if (event.joystick.position != 0 || joystickAxisX >= 0)
+ joystickAxisX = axisPosition;
+ break;
+ default:
+ break;
+ }
+ break;
+ }
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
_vm->quitGame();
@@ -260,10 +441,12 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
const int currentFrame = decoder.getCurFrame();
while (lastFrame < currentFrame && shield > 0) {
++lastFrame;
- aimX = CLIP<int>(aimX + ((int)moveRight - (int)moveLeft) * 6,
- 30, 290);
- aimY = CLIP<int>(aimY + ((int)moveDown - (int)moveUp) * 6,
- 48, 178);
+ updateLevel1Aim(aimX, aimY, aimVelocityX, aimVelocityY,
+ aimDirectionX, aimDirectionY,
+ moveLeft || actionLeft || joystickAxisX < -joystickDeadzone,
+ moveRight || actionRight || joystickAxisX > joystickDeadzone,
+ moveUp || actionUp || joystickAxisY < -joystickDeadzone,
+ moveDown || actionDown || joystickAxisY > joystickDeadzone);
if (lastFrame >= nextSpawnAdjustment) {
nextSpawnAdjustment = lastFrame + 20;
@@ -285,8 +468,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
}
- if (fireFrames > 0)
- --fireFrames;
+ updateLevel1Shots(shots);
for (int i = 0; i < 3; ++i) {
if (explosions[i].frames > 0)
--explosions[i].frames;
@@ -315,12 +497,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
break;
}
- const bool heldFire = mouseFire || keyFire;
- if ((fireRequested || (heldFire && currentFrame - lastShotFrame >= 5)) &&
- currentFrame - lastShotFrame >= 3) {
+ const bool heldFire = mouseFire || keyFire || actionFire;
+ const bool shootRequested = fireRequested ||
+ (heldFire && currentFrame - lastShotFrame >= 12);
+ fireRequested = false;
+ if (shootRequested && currentFrame - lastShotFrame >= 4) {
lastShotFrame = currentFrame;
- fireRequested = false;
- fireFrames = 2;
+ spawnLevel1Shot(shots, aimX, aimY);
int hitEnemy = -1;
float hitDistance = 1000000.0f;
for (int i = 0; i < 3; ++i) {
@@ -355,11 +538,15 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
renderer.renderModel(model, enemies[i].x, enemies[i].y, enemies[i].size,
enemies[i].pitch, enemies[i].yaw, enemies[i].roll);
}
+ renderLevel1Shots(renderer, laser, shots);
+ const float crosshairX = (aimX - 160) * 620.0f / 640.0f;
+ const float crosshairY = (aimY - 120) * 620.0f / 640.0f;
renderer.renderModel(crosshair, aimX, aimY, 31.0f,
- (aimY - 113) * 0.12f, -(aimX - 160) * 0.10f, 0.0f, false);
+ crosshairY * 720.0f / 4096.0f,
+ -crosshairX * 720.0f / 4096.0f, 0.0f, false);
Graphics::Surface output;
renderer.finishFrame(output);
- drawLevel1Effects(output, ui, enemies, explosions, aimX, aimY, fireFrames);
+ drawLevel1Effects(output, ui, enemies, explosions);
ui.drawCockpit(output);
ui.drawHUD(output, score, lives, shield, currentFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index aadde528877..2fa82f17e0b 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -240,6 +240,90 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
tglEnable(TGL_DEPTH_TEST);
}
+void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
+ float x, float y, float z, float directionX, float directionY, float directionZ,
+ float roll, bool depthTest) {
+ if (!_context || model.vertices().empty())
+ return;
+
+ const float directionLength = sqrtf(directionX * directionX + directionY * directionY +
+ directionZ * directionZ);
+ if (directionLength < 0.001f)
+ return;
+ const float forwardX = directionX / directionLength;
+ const float forwardY = directionY / directionLength;
+ const float forwardZ = directionZ / directionLength;
+
+ float rightX = forwardZ;
+ float rightY = 0.0f;
+ float rightZ = -forwardX;
+ const float rightLength = sqrtf(rightX * rightX + rightZ * rightZ);
+ if (rightLength < 0.001f) {
+ rightX = 1.0f;
+ rightZ = 0.0f;
+ } else {
+ rightX /= rightLength;
+ rightZ /= rightLength;
+ }
+ const float downX = forwardY * rightZ - forwardZ * rightY;
+ const float downY = forwardZ * rightX - forwardX * rightZ;
+ const float downZ = forwardX * rightY - forwardY * rightX;
+ const float angle = roll * 0.017453292519943295f;
+ const float cosine = cosf(angle);
+ const float sine = sinf(angle);
+ const float modelXx = rightX * cosine + downX * sine;
+ const float modelXy = rightY * cosine + downY * sine;
+ const float modelXz = rightZ * cosine + downZ * sine;
+ const float modelYx = downX * cosine - rightX * sine;
+ const float modelYy = downY * cosine - rightY * sine;
+ const float modelYz = downZ * cosine - rightZ * sine;
+
+ struct ProjectedVertex {
+ float x;
+ float y;
+ bool visible;
+ };
+ Common::Array<ProjectedVertex> projected;
+ projected.resize(model.vertices().size());
+ const float centerX = _width * 0.5f;
+ const float centerY = _height * 0.5f;
+ const float focalLength = _width * 2.0f;
+ for (uint i = 0; i < model.vertices().size(); ++i) {
+ const RA2PSXVertex &vertex = model.vertices()[i];
+ const float worldX = x + modelXx * vertex.x + modelYx * vertex.y + forwardX * vertex.z;
+ const float worldY = y + modelXy * vertex.x + modelYy * vertex.y + forwardY * vertex.z;
+ const float worldZ = z + modelXz * vertex.x + modelYz * vertex.y + forwardZ * vertex.z;
+ projected[i].visible = worldZ > 1.0f;
+ if (projected[i].visible) {
+ projected[i].x = centerX + worldX * focalLength / worldZ;
+ projected[i].y = centerY + worldY * focalLength / worldZ;
+ }
+ }
+
+ TinyGL::setContext(_context);
+ if (!depthTest)
+ tglDisable(TGL_DEPTH_TEST);
+ const Common::Array<RA2PSXFace> &faces = model.faces();
+ for (uint faceIndex = 0; faceIndex < faces.size(); ++faceIndex) {
+ const RA2PSXFace &face = faces[faceIndex];
+ bool visible = true;
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex)
+ visible &= projected[face.vertex[vertexIndex]].visible;
+ if (!visible)
+ continue;
+
+ tglColor3ub(face.r, face.g, face.b);
+ tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ const ProjectedVertex &vertex = projected[face.vertex[vertexIndex]];
+ tglVertex3f(vertex.x, vertex.y, 0.0f);
+ }
+ tglEnd();
+ }
+ if (!depthTest)
+ tglEnable(TGL_DEPTH_TEST);
+}
+
void RA2PSXTinyGLRenderer::finishFrame(Graphics::Surface &surface) {
if (!_context)
return;
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index d78be61c0df..4e31bb946db 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -115,7 +115,7 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
}
bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
- RA2PSXLevel1UI &ui) {
+ RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -127,8 +127,10 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
Common::Array<byte> modelData;
Common::Array<byte> crosshairData;
+ Common::Array<byte> laserData;
return archive.getMember("fOFS/TieFighter/main", modelData) && model.load(modelData) &&
archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
+ archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
ui.load(archive);
}
@@ -136,8 +138,9 @@ Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
RA2PSXModel model;
RA2PSXModel crosshair;
+ RA2PSXModel laser;
RA2PSXLevel1UI ui;
- if (!loadLevel1Assets(model, crosshair, ui))
+ if (!loadLevel1Assets(model, crosshair, laser, ui))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
@@ -151,7 +154,7 @@ Common::Error Rebel2PSX::runGame() {
int lives = 3;
int score = 0;
while (!_vm->shouldQuit()) {
- const Level1Result result = playLevel1(model, crosshair, ui, lives, score);
+ const Level1Result result = playLevel1(model, crosshair, laser, ui, lives, score);
if (result == kLevel1Quit)
return Common::kNoError;
if (result == kLevel1Error)
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 38aca961b99..075128b281d 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -99,6 +99,9 @@ public:
void beginFrame(const Graphics::Surface &background);
void renderModel(const RA2PSXModel &model, float x, float y, float size,
float pitch, float yaw, float roll, bool depthTest = true);
+ void renderPerspectiveModel(const RA2PSXModel &model, float x, float y, float z,
+ float directionX, float directionY, float directionZ, float roll,
+ bool depthTest = true);
void finishFrame(Graphics::Surface &surface);
private:
@@ -126,9 +129,10 @@ private:
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 loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair, RA2PSXLevel1UI &ui);
+ bool loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
+ RA2PSXModel &laser, RA2PSXLevel1UI &ui);
Level1Result playLevel1(const RA2PSXModel &model, const RA2PSXModel &crosshair,
- const RA2PSXLevel1UI &ui, int lives, int &score);
+ const RA2PSXModel &laser, const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
};
Commit: 0d7833eaaaf8ae3b275d5aacbaec29806bd5eeb9
https://github.com/scummvm/scummvm/commit/0d7833eaaaf8ae3b275d5aacbaec29806bd5eeb9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: third person ship view in psx
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index b559ef6315a..4451015fbe0 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -37,6 +37,8 @@
namespace Scumm {
#ifdef USE_TINYGL
+static const int kLevel1FrameRate = 30;
+
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),
@@ -71,12 +73,25 @@ struct RA2PSXLevel1Explosion {
};
struct RA2PSXLevel1Shot {
- RA2PSXLevel1Shot() : active(false), progress(0), targetX(0), targetY(0) {}
+ RA2PSXLevel1Shot() : active(false), progress(0), targetX(0), targetY(0), targetZ(0) {}
bool active;
int progress;
+ float start[3][3];
+ float roll[3];
float targetX;
float targetY;
+ float targetZ;
+};
+
+struct RA2PSXLevel1Ship {
+ RA2PSXLevel1Ship() : x(0), y(0), z(1000), velocityX(0), velocityY(0) {}
+
+ int x;
+ int y;
+ int z;
+ int velocityX;
+ int velocityY;
};
static const float kLevel1LaserStart[3][3] = {
@@ -87,6 +102,12 @@ static const float kLevel1LaserStart[3][3] = {
static const float kLevel1LaserRoll[3] = { -45.0f, 45.0f, 0.0f };
+static const float kLevel1ShipLaserStart[3][3] = {
+ { -93.0f, 11.0f, -139.0f },
+ { 93.0f, 11.0f, -139.0f },
+ { 4.0f, 210.0f, -111.0f }
+};
+
static void updateLevel1Enemy(RA2PSXLevel1Enemy &enemy) {
const float t = MIN(1.0f, (float)enemy.age / enemy.lifetime);
const float inverse = 1.0f - t;
@@ -178,7 +199,72 @@ static void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
y = CLIP<int>(y + velocityY / 512, 48, 178);
}
-static void spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY) {
+static void updateLevel1Ship(RA2PSXLevel1Ship &ship,
+ bool left, bool right, bool up, bool down) {
+ if (left == right) {
+ ship.velocityX = ship.velocityX * 3 / 4;
+ } else if (left) {
+ ship.velocityX = MAX(-4096, ship.velocityX - 400);
+ } else {
+ ship.velocityX = MIN(4096, ship.velocityX + 400);
+ }
+
+ if (up == down) {
+ ship.velocityY = ship.velocityY * 3 / 4;
+ } else if (up) {
+ ship.velocityY = MAX(-4096, ship.velocityY - 400);
+ } else {
+ ship.velocityY = MIN(4096, ship.velocityY + 400);
+ }
+
+ ship.x = CLIP<int>(ship.x + ship.velocityX / 16 / 25, -282, 282);
+ ship.y = CLIP<int>(ship.y + ship.velocityY * 10 / 4096, -142, 157);
+}
+
+static void getLevel1ShipOrientation(const RA2PSXLevel1Ship &ship,
+ float &forwardX, float &forwardY, float &forwardZ, float &roll) {
+ const float bank = (ship.velocityX / 16) * 360.0f / 4096.0f;
+ const float yaw = -bank * 0.5f * 0.017453292519943295f;
+ forwardX = sinf(yaw);
+ forwardY = 0.0f;
+ forwardZ = -cosf(yaw);
+ roll = bank;
+}
+
+static void transformLevel1ShipPoint(const RA2PSXLevel1Ship &ship,
+ float localX, float localY, float localZ,
+ float &worldX, float &worldY, float &worldZ) {
+ float forwardX;
+ float forwardY;
+ float forwardZ;
+ float roll;
+ getLevel1ShipOrientation(ship, forwardX, forwardY, forwardZ, roll);
+
+ float rightX = forwardZ;
+ float rightZ = -forwardX;
+ const float rightLength = sqrtf(rightX * rightX + rightZ * rightZ);
+ rightX /= rightLength;
+ rightZ /= rightLength;
+ const float downX = forwardY * rightZ;
+ const float downY = forwardZ * rightX - forwardX * rightZ;
+ const float downZ = -forwardY * rightX;
+ const float angle = roll * 0.017453292519943295f;
+ const float cosine = cosf(angle);
+ const float sine = sinf(angle);
+ const float modelXx = rightX * cosine + downX * sine;
+ const float modelXy = downY * sine;
+ const float modelXz = rightZ * cosine + downZ * sine;
+ const float modelYx = downX * cosine - rightX * sine;
+ const float modelYy = downY * cosine;
+ const float modelYz = downZ * cosine - rightZ * sine;
+
+ worldX = ship.x + modelXx * localX + modelYx * localY + forwardX * localZ;
+ worldY = ship.y + modelXy * localX + modelYy * localY + forwardY * localZ;
+ worldZ = ship.z + modelXz * localX + modelYz * localY + forwardZ * localZ;
+}
+
+static bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
+ const RA2PSXLevel1Ship *ship, int &targetScreenX, int &targetScreenY) {
int slot = -1;
for (int i = 0; i < 8; ++i) {
if (!shots[i].active) {
@@ -187,12 +273,42 @@ static void spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY) {
}
}
if (slot < 0)
- return;
+ return false;
+
+ RA2PSXLevel1Shot &shot = shots[slot];
+ shot.active = true;
+ shot.progress = 400;
+ if (!ship) {
+ for (int i = 0; i < 3; ++i) {
+ for (int axis = 0; axis < 3; ++axis)
+ shot.start[i][axis] = kLevel1LaserStart[i][axis];
+ shot.roll[i] = kLevel1LaserRoll[i];
+ }
+ shot.targetX = (aimX - 160) * 18000.0f / 640.0f;
+ shot.targetY = (aimY - 120) * 18000.0f / 640.0f;
+ shot.targetZ = 18000.0f;
+ } else {
+ float forwardX;
+ float forwardY;
+ float forwardZ;
+ float shipRoll;
+ getLevel1ShipOrientation(*ship, forwardX, forwardY, forwardZ, shipRoll);
+ for (int i = 0; i < 3; ++i) {
+ transformLevel1ShipPoint(*ship, kLevel1ShipLaserStart[i][0],
+ kLevel1ShipLaserStart[i][1], kLevel1ShipLaserStart[i][2],
+ shot.start[i][0], shot.start[i][1], shot.start[i][2]);
+ shot.roll[i] = kLevel1LaserRoll[i] + shipRoll;
+ }
+ transformLevel1ShipPoint(*ship, 0.0f, -70.0f, -100.0f,
+ shot.targetX, shot.targetY, shot.targetZ);
+ shot.targetX -= forwardX * 18000.0f;
+ shot.targetY -= forwardY * 18000.0f;
+ shot.targetZ -= forwardZ * 18000.0f;
+ }
- shots[slot].active = true;
- shots[slot].progress = 400;
- shots[slot].targetX = (aimX - 160) * 18000.0f / 640.0f;
- shots[slot].targetY = (aimY - 120) * 18000.0f / 640.0f;
+ targetScreenX = 160 + (int)(shot.targetX * 640.0f / shot.targetZ);
+ targetScreenY = 120 + (int)(shot.targetY * 640.0f / shot.targetZ);
+ return true;
}
static void updateLevel1Shots(RA2PSXLevel1Shot *shots) {
@@ -213,29 +329,30 @@ static void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel
continue;
const float progress = shot.progress / 4096.0f;
for (int laserIndex = 0; laserIndex < 3; ++laserIndex) {
- const float *start = kLevel1LaserStart[laserIndex];
+ const float *start = shot.start[laserIndex];
const float directionX = shot.targetX - start[0];
const float directionY = shot.targetY - start[1];
- const float directionZ = 18000.0f - start[2];
+ const float directionZ = shot.targetZ - start[2];
renderer.renderPerspectiveModel(laser,
start[0] + directionX * progress,
start[1] + directionY * progress,
start[2] + directionZ * progress,
- directionX, directionY, directionZ, kLevel1LaserRoll[laserIndex], false);
+ directionX, directionY, directionZ, shot.roll[laserIndex], false);
}
}
}
static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
- const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions) {
+ const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions,
+ int laserTargetX, int laserTargetY) {
const uint32 green = surface.format.RGBToColor(64, 255, 96);
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);
+ laserTargetX - 15, laserTargetY, green);
surface.drawLine((int)enemies[i].x + 2, (int)enemies[i].y,
- 175, surface.h - 1, green);
+ laserTargetX + 15, laserTargetY, green);
}
if (explosions[i].frames > 0)
ui.drawExplosion(surface, explosions[i].x, explosions[i].y,
@@ -244,11 +361,12 @@ static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &
}
#endif
-Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
- const RA2PSXModel &crosshair, const RA2PSXModel &laser,
- const RA2PSXLevel1UI &ui, int lives, int &score) {
+Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
+ const RA2PSXModel &shipModel, const RA2PSXModel &crosshair,
+ const RA2PSXModel &laser, const RA2PSXLevel1UI &ui, int lives, int &score) {
#ifndef USE_TINYGL
- (void)model;
+ (void)enemyModel;
+ (void)shipModel;
(void)crosshair;
(void)laser;
(void)ui;
@@ -275,6 +393,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
RA2PSXLevel1Enemy enemies[3];
RA2PSXLevel1Explosion explosions[3];
RA2PSXLevel1Shot shots[8];
+ RA2PSXLevel1Ship ship;
int aimX = 160;
int aimY = 113;
int aimVelocityX = 0;
@@ -286,7 +405,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
int spawnRange = 80;
int spawnBase = 60;
int nextSpawnAdjustment = 0;
- int lastFrame = -1;
+ int logicFrame = -1;
+ int videoFrame = -1;
int lastShotFrame = -4;
bool moveLeft = false;
bool moveRight = false;
@@ -302,28 +422,41 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
bool keyFire = false;
bool actionFire = false;
bool fireRequested = false;
+ bool thirdPersonView = true;
Level1Result result = kLevel1Complete;
const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
const bool cursorWasVisible = CursorMan.isVisible();
CursorMan.showMouse(false);
- g_system->warpMouse(aimX, aimY);
+ g_system->warpMouse(160, 120);
decoder.start();
+ const uint32 gameplayStartTime = g_system->getMillis();
+ const Graphics::Surface *background = nullptr;
while (!_vm->shouldQuit() && !decoder.endOfVideo()) {
+ bool redraw = false;
+ bool toggleViewRequested = false;
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
- aimX = CLIP<int>(event.mouse.x, 30, 290);
- aimY = CLIP<int>(event.mouse.y, 48, 178);
- aimVelocityX = aimVelocityY = 0;
- aimDirectionX = aimDirectionY = 0;
+ if (thirdPersonView) {
+ ship.x = CLIP<int>((event.mouse.x - 160) * ship.z / 640, -282, 282);
+ ship.y = CLIP<int>((event.mouse.y - 120) * ship.z / 640, -142, 157);
+ ship.velocityX = ship.velocityY = 0;
+ } else {
+ aimX = CLIP<int>(event.mouse.x, 30, 290);
+ aimY = CLIP<int>(event.mouse.y, 48, 178);
+ aimVelocityX = aimVelocityY = 0;
+ aimDirectionX = aimDirectionY = 0;
+ }
break;
case Common::EVENT_LBUTTONDOWN:
- aimX = CLIP<int>(event.mouse.x, 30, 290);
- aimY = CLIP<int>(event.mouse.y, 48, 178);
+ if (!thirdPersonView) {
+ aimX = CLIP<int>(event.mouse.x, 30, 290);
+ aimY = CLIP<int>(event.mouse.y, 48, 178);
+ }
mouseFire = true;
fireRequested = true;
break;
@@ -333,6 +466,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
case Common::EVENT_KEYDOWN:
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
result = kLevel1Quit;
+ } else if (event.kbd.keycode == Common::KEYCODE_TAB && !event.kbdRepeat) {
+ toggleViewRequested = true;
} else if (event.kbd.keycode == Common::KEYCODE_LEFT ||
event.kbd.keycode == Common::KEYCODE_a) {
moveLeft = true;
@@ -385,6 +520,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
if (pressed)
fireRequested = true;
break;
+ case kScummActionInsaneSwitch:
+ if (pressed)
+ toggleViewRequested = true;
+ break;
case kScummActionInsaneBack:
if (pressed)
result = kLevel1Quit;
@@ -428,89 +567,108 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
break;
}
}
+ if (toggleViewRequested) {
+ thirdPersonView = !thirdPersonView;
+ ship.velocityX = ship.velocityY = 0;
+ aimX = 160;
+ aimY = 113;
+ aimVelocityX = aimVelocityY = 0;
+ aimDirectionX = aimDirectionY = 0;
+ g_system->warpMouse(160, thirdPersonView ? 120 : aimY);
+ redraw = true;
+ }
if (result == kLevel1Quit)
break;
- if (decoder.needsUpdate()) {
- const Graphics::Surface *frame = decoder.decodeNextFrame();
- if (!frame) {
+ while (decoder.needsUpdate()) {
+ background = decoder.decodeNextFrame();
+ if (!background) {
result = kLevel1Error;
break;
}
+ videoFrame = decoder.getCurFrame();
+ }
+ if (result == kLevel1Error)
+ break;
- const int currentFrame = decoder.getCurFrame();
- while (lastFrame < currentFrame && shield > 0) {
- ++lastFrame;
+ const uint32 elapsed = g_system->getMillis() - gameplayStartTime;
+ const int targetLogicFrame = (int)((uint64)elapsed * kLevel1FrameRate / 1000);
+ while (logicFrame < targetLogicFrame && shield > 0) {
+ ++logicFrame;
+ redraw = true;
+ const bool left = moveLeft || actionLeft || joystickAxisX < -joystickDeadzone;
+ const bool right = moveRight || actionRight || joystickAxisX > joystickDeadzone;
+ const bool up = moveUp || actionUp || joystickAxisY < -joystickDeadzone;
+ const bool down = moveDown || actionDown || joystickAxisY > joystickDeadzone;
+ if (thirdPersonView) {
+ updateLevel1Ship(ship, left, right, up, down);
+ } else {
updateLevel1Aim(aimX, aimY, aimVelocityX, aimVelocityY,
aimDirectionX, aimDirectionY,
- moveLeft || actionLeft || joystickAxisX < -joystickDeadzone,
- moveRight || actionRight || joystickAxisX > joystickDeadzone,
- moveUp || actionUp || joystickAxisY < -joystickDeadzone,
- moveDown || actionDown || joystickAxisY > joystickDeadzone);
-
- if (lastFrame >= nextSpawnAdjustment) {
- nextSpawnAdjustment = lastFrame + 20;
- spawnRange = MAX(40, spawnRange - 1);
- spawnBase = MAX(20, spawnBase - 1);
- }
+ left, right, up, down);
+ }
- 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 (logicFrame >= nextSpawnAdjustment) {
+ nextSpawnAdjustment = logicFrame + 20;
+ spawnRange = MAX(40, spawnRange - 1);
+ spawnBase = MAX(20, spawnBase - 1);
+ }
- updateLevel1Shots(shots);
+ int activeEnemies = 0;
+ for (int i = 0; i < 3; ++i)
+ activeEnemies += enemies[i].active ? 1 : 0;
+ --spawnDelay;
+ if (videoFrame < 1599 && activeEnemies < 3 && spawnDelay <= 0) {
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));
- }
- if (enemies[i].age >= enemies[i].lifetime) {
- enemies[i].active = false;
- if (_vm->_rnd.getRandomNumber(99) < 18)
- shield = MAX(0, shield - 12);
+ if (!enemies[i].active) {
+ spawnLevel1Enemy(enemies[i], _vm->_rnd);
+ break;
}
}
+ spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
}
- if (shield <= 0) {
- result = kLevel1Death;
- break;
+ updateLevel1Shots(shots);
+ 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));
+ }
+ if (enemies[i].age >= enemies[i].lifetime) {
+ enemies[i].active = false;
+ if (_vm->_rnd.getRandomNumber(99) < 18)
+ shield = MAX(0, shield - 12);
+ }
}
const bool heldFire = mouseFire || keyFire || actionFire;
const bool shootRequested = fireRequested ||
- (heldFire && currentFrame - lastShotFrame >= 12);
+ (heldFire && logicFrame - lastShotFrame >= 12);
fireRequested = false;
- if (shootRequested && currentFrame - lastShotFrame >= 4) {
- lastShotFrame = currentFrame;
- spawnLevel1Shot(shots, aimX, aimY);
+ if (shootRequested && logicFrame - lastShotFrame >= 4) {
+ int shotTargetX;
+ int shotTargetY;
+ if (!spawnLevel1Shot(shots, aimX, aimY,
+ thirdPersonView ? &ship : nullptr, shotTargetX, shotTargetY))
+ continue;
+ lastShotFrame = logicFrame;
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 dx = enemies[i].x - shotTargetX;
+ const float dy = enemies[i].y - shotTargetY;
const float distance = dx * dx + dy * dy;
const float radius = MAX(10.0f, enemies[i].size * 0.72f);
if (distance <= radius * radius && distance < hitDistance) {
@@ -531,24 +689,44 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &model,
}
}
}
+ }
+
+ if (shield <= 0) {
+ result = kLevel1Death;
+ break;
+ }
- renderer.beginFrame(*frame);
+ if (background && redraw) {
+ renderer.beginFrame(*background);
for (int i = 0; i < 3; ++i) {
if (enemies[i].active)
- renderer.renderModel(model, enemies[i].x, enemies[i].y, enemies[i].size,
+ renderer.renderModel(enemyModel, enemies[i].x, enemies[i].y, enemies[i].size,
enemies[i].pitch, enemies[i].yaw, enemies[i].roll);
}
renderLevel1Shots(renderer, laser, shots);
- const float crosshairX = (aimX - 160) * 620.0f / 640.0f;
- const float crosshairY = (aimY - 120) * 620.0f / 640.0f;
- renderer.renderModel(crosshair, aimX, aimY, 31.0f,
- crosshairY * 720.0f / 4096.0f,
- -crosshairX * 720.0f / 4096.0f, 0.0f, false);
+ if (thirdPersonView) {
+ float forwardX;
+ float forwardY;
+ float forwardZ;
+ float shipRoll;
+ getLevel1ShipOrientation(ship, forwardX, forwardY, forwardZ, shipRoll);
+ renderer.renderPerspectiveModel(shipModel, ship.x, ship.y, ship.z,
+ forwardX, forwardY, forwardZ, shipRoll, false);
+ } else {
+ const float crosshairX = (aimX - 160) * 620.0f / 640.0f;
+ const float crosshairY = (aimY - 120) * 620.0f / 640.0f;
+ renderer.renderModel(crosshair, aimX, aimY, 31.0f,
+ crosshairY * 720.0f / 4096.0f,
+ -crosshairX * 720.0f / 4096.0f, 0.0f, false);
+ }
Graphics::Surface output;
renderer.finishFrame(output);
- drawLevel1Effects(output, ui, enemies, explosions);
- ui.drawCockpit(output);
- ui.drawHUD(output, score, lives, shield, currentFrame);
+ const int laserTargetX = thirdPersonView ? 160 + ship.x * 640 / ship.z : 160;
+ const int laserTargetY = thirdPersonView ? 120 + ship.y * 640 / ship.z : output.h - 1;
+ drawLevel1Effects(output, ui, enemies, explosions, laserTargetX, laserTargetY);
+ if (!thirdPersonView)
+ ui.drawCockpit(output);
+ ui.drawHUD(output, score, lives, shield, logicFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
g_system->updateScreen();
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 4e31bb946db..d92941bb883 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -114,8 +114,8 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
return !_vm->shouldQuit();
}
-bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
- RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
+bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
+ RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -125,10 +125,12 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
if (!loaded)
return false;
- Common::Array<byte> modelData;
+ Common::Array<byte> enemyData;
+ Common::Array<byte> shipData;
Common::Array<byte> crosshairData;
Common::Array<byte> laserData;
- return archive.getMember("fOFS/TieFighter/main", modelData) && model.load(modelData) &&
+ return archive.getMember("fOFS/TieFighter/main", enemyData) && enemy.load(enemyData) &&
+ archive.getMember("fOFS/Ship", shipData) && ship.load(shipData) &&
archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
ui.load(archive);
@@ -136,11 +138,12 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
- RA2PSXModel model;
+ RA2PSXModel enemy;
+ RA2PSXModel ship;
RA2PSXModel crosshair;
RA2PSXModel laser;
RA2PSXLevel1UI ui;
- if (!loadLevel1Assets(model, crosshair, laser, ui))
+ if (!loadLevel1Assets(enemy, ship, crosshair, laser, ui))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
@@ -154,7 +157,7 @@ Common::Error Rebel2PSX::runGame() {
int lives = 3;
int score = 0;
while (!_vm->shouldQuit()) {
- const Level1Result result = playLevel1(model, crosshair, laser, ui, lives, score);
+ const Level1Result result = playLevel1(enemy, ship, crosshair, laser, ui, lives, score);
if (result == kLevel1Quit)
return Common::kNoError;
if (result == kLevel1Error)
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 075128b281d..c9623c2a25e 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -129,10 +129,11 @@ private:
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 loadLevel1Assets(RA2PSXModel &model, RA2PSXModel &crosshair,
- RA2PSXModel &laser, RA2PSXLevel1UI &ui);
- Level1Result playLevel1(const RA2PSXModel &model, const RA2PSXModel &crosshair,
- const RA2PSXModel &laser, const RA2PSXLevel1UI &ui, int lives, int &score);
+ bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
+ RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui);
+ Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
+ const RA2PSXModel &crosshair, const RA2PSXModel &laser,
+ const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
};
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index a5ac6b9c203..7fc8bde06ec 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1075,6 +1075,8 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
Common::KeymapArray keymaps = MetaEngine::initKeymaps(target);
Common::String gameId = ConfMan.get("gameid", target);
+ const bool isRebel2PSX = gameId == "rebel2" &&
+ parsePlatform(ConfMan.get("platform", target)) == kPlatformPSX;
Action *act;
if (gameId == "rebel1" || gameId == "rebel2") {
@@ -1305,10 +1307,16 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
act->addDefaultInputMapping("JOY_A");
rebel2Keymap->addAction(act);
- act = new Action("RA2COVER", _("Cover"));
+ act = new Action("RA2COVER", isRebel2PSX ? _("Change view") : _("Cover"));
act->setCustomEngineActionEvent(kScummActionInsaneSwitch);
- act->addDefaultInputMapping("JOY_X");
- act->addDefaultInputMapping("JOY_Y");
+ if (isRebel2PSX) {
+ act->addDefaultInputMapping("TAB");
+ act->addDefaultInputMapping("JOY_Y");
+ act->addDefaultInputMapping("JOY_BACK");
+ } else {
+ act->addDefaultInputMapping("JOY_X");
+ act->addDefaultInputMapping("JOY_Y");
+ }
rebel2Keymap->addAction(act);
act = new Action("RA2SKIP", _("Skip / back"));
Commit: ce8263d11f9c1697412c0928ee25e1d7dd671d8f
https://github.com/scummvm/scummvm/commit/ce8263d11f9c1697412c0928ee25e1d7dd671d8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: load textures in psx
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 4451015fbe0..ff7fe44fda6 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -240,27 +240,14 @@ static void transformLevel1ShipPoint(const RA2PSXLevel1Ship &ship,
float roll;
getLevel1ShipOrientation(ship, forwardX, forwardY, forwardZ, roll);
- float rightX = forwardZ;
- float rightZ = -forwardX;
- const float rightLength = sqrtf(rightX * rightX + rightZ * rightZ);
- rightX /= rightLength;
- rightZ /= rightLength;
- const float downX = forwardY * rightZ;
- const float downY = forwardZ * rightX - forwardX * rightZ;
- const float downZ = -forwardY * rightX;
const float angle = roll * 0.017453292519943295f;
const float cosine = cosf(angle);
const float sine = sinf(angle);
- const float modelXx = rightX * cosine + downX * sine;
- const float modelXy = downY * sine;
- const float modelXz = rightZ * cosine + downZ * sine;
- const float modelYx = downX * cosine - rightX * sine;
- const float modelYy = downY * cosine;
- const float modelYz = downZ * cosine - rightZ * sine;
-
- worldX = ship.x + modelXx * localX + modelYx * localY + forwardX * localZ;
- worldY = ship.y + modelXy * localX + modelYy * localY + forwardY * localZ;
- worldZ = ship.z + modelXz * localX + modelYz * localY + forwardZ * localZ;
+ worldX = ship.x + forwardZ * cosine * localX - forwardZ * sine * localY +
+ forwardX * localZ;
+ worldY = ship.y + sine * localX + cosine * localY + forwardY * localZ;
+ worldZ = ship.z - forwardX * cosine * localX + forwardX * sine * localY +
+ forwardZ * localZ;
}
static bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index 2fa82f17e0b..942ca1df5b1 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -12,6 +12,7 @@
#include "common/endian.h"
#include "common/system.h"
+#include "common/util.h"
#include "scumm/insane/rebel2/psx/psx.h"
@@ -33,13 +34,91 @@ static bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &valu
return true;
}
+bool loadRA2PSXTextures(const Common::Array<byte> &data,
+ Common::Array<RA2PSXTexture> &textures) {
+ const uint initialCount = textures.size();
+ uint32 offset = 0;
+ while (offset + 20 <= data.size()) {
+ Common::String name;
+ for (uint i = 0; i < 8 && data[offset + i]; ++i) {
+ if (data[offset + i] < 0x20 || data[offset + i] >= 0x7f)
+ return textures.size() > initialCount;
+ name += (char)data[offset + i];
+ }
+ if (name.empty())
+ break;
+
+ const uint16 recordSize = READ_LE_UINT16(data.data() + offset + 12);
+ const uint16 widthField = READ_LE_UINT16(data.data() + offset + 16);
+ const uint16 heightField = READ_LE_UINT16(data.data() + offset + 18);
+ const uint16 width = (widthField & 0xff) ? widthField & 0xff : 256;
+ const uint16 height = (heightField & 0xff) ? heightField & 0xff : 256;
+ const bool eightBit = (widthField & 0x100) != 0;
+ const uint32 paletteColors = eightBit ? 256 : 16;
+ const uint32 pixelCount = (uint32)width * height;
+ const uint32 pixelBytes = eightBit ? pixelCount : (pixelCount + 1) / 2;
+ const uint32 paletteOffset = offset + 20;
+ const uint32 pixelsOffset = paletteOffset + paletteColors * 2;
+ if (recordSize < 20 || offset + recordSize > data.size() ||
+ pixelsOffset + pixelBytes > offset + recordSize)
+ break;
+
+ RA2PSXTexture texture;
+ texture.name = name;
+ texture.width = width;
+ texture.height = height;
+ texture.pixels.resize(pixelCount);
+ for (uint32 i = 0; i < pixelCount; ++i) {
+ const byte packed = data[pixelsOffset + (eightBit ? i : i / 2)];
+ const byte paletteIndex = eightBit ? packed : ((i & 1) ? packed >> 4 : packed & 0xf);
+ const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
+ if (!value) {
+ texture.pixels[i] = 0;
+ continue;
+ }
+
+ const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
+ const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
+ const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
+ texture.pixels[i] = 0x01000000 | ((value & 0x8000) ? 0x02000000 : 0) |
+ (r << 16) | (g << 8) | b;
+ }
+ textures.push_back(texture);
+ offset += recordSize;
+ }
+ return textures.size() > initialCount;
+}
+
RA2PSXModel::RA2PSXModel() : _radius(1.0f) {
}
bool RA2PSXModel::load(const Common::Array<byte> &data) {
_vertices.clear();
_faces.clear();
+ _textures.clear();
_radius = 1.0f;
+
+ uint32 textureOffset;
+ if (!readU32(data, 16, textureOffset) || textureOffset + 4 > data.size())
+ return false;
+ const uint32 textureCount = READ_LE_UINT32(data.data() + textureOffset);
+ if (textureCount > 256 || textureCount > (data.size() - textureOffset - 4) / 8)
+ return false;
+ for (uint32 i = 0; i < textureCount; ++i) {
+ const uint32 nameOffset = textureOffset + 4 + i * 8;
+ RA2PSXTexture texture;
+ for (uint j = 0; j < 8 && data[nameOffset + j]; ++j) {
+ if (data[nameOffset + j] < 0x20 || data[nameOffset + j] >= 0x7f)
+ return false;
+ texture.name += (char)data[nameOffset + j];
+ }
+ if (texture.name.empty())
+ return false;
+ texture.width = 0;
+ texture.height = 0;
+ _textures.push_back(texture);
+ }
+
if (!parseModel(data, 0, 0) || _vertices.empty() || _faces.empty())
return false;
@@ -55,6 +134,31 @@ bool RA2PSXModel::load(const Common::Array<byte> &data) {
return true;
}
+bool RA2PSXModel::loadTextures(const Common::Array<byte> &data) {
+ Common::Array<RA2PSXTexture> decoded;
+ if (!loadRA2PSXTextures(data, decoded))
+ return false;
+
+ for (uint i = 0; i < _textures.size(); ++i) {
+ for (uint j = 0; j < decoded.size(); ++j) {
+ if (_textures[i].name.equalsIgnoreCase(decoded[j].name)) {
+ _textures[i] = decoded[j];
+ break;
+ }
+ }
+ if (_textures[i].pixels.empty())
+ return false;
+ }
+ return true;
+}
+
+const RA2PSXTexture *RA2PSXModel::texture(int index) const {
+ if (index < 0 || (uint)index >= _textures.size() || !_textures[index].width ||
+ !_textures[index].height || _textures[index].pixels.empty())
+ return nullptr;
+ return &_textures[index];
+}
+
bool RA2PSXModel::parseModel(const Common::Array<byte> &data, uint32 modelOffset, int depth) {
if (depth > 8 || modelOffset + 8 > data.size())
return false;
@@ -102,6 +206,9 @@ bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffs
vertex.z = (int16)READ_LE_UINT16(source + 4);
_vertices.push_back(vertex);
}
+ const uint32 normalOffset = vertexOffset + (uint32)vertexCount * 8;
+ if (normalOffset > primitiveOffset)
+ return false;
Common::HashMap<uint16, bool> seen;
for (uint32 listIndex = 0; listIndex < 8; ++listIndex) {
@@ -126,8 +233,10 @@ bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffs
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;
+ const byte mode = data[record + 12];
RA2PSXFace face = {};
+ face.mode = mode;
+ face.texture = -1;
face.vertexCount = (mode & 0x10) ? 4 : 3;
const uint16 offsets[4] = {
READ_LE_UINT16(data.data() + record),
@@ -140,11 +249,51 @@ bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffs
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;
+ if (!(mode & 0x44)) {
+ const uint16 normalOffsets[4] = {
+ READ_LE_UINT16(data.data() + record + 6),
+ READ_LE_UINT16(data.data() + record + 8),
+ READ_LE_UINT16(data.data() + record + 10),
+ READ_LE_UINT16(data.data() + record + 26)
+ };
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ const uint normalIndex = (mode & 1) ? vertexIndex : 0;
+ const uint16 sourceOffset = normalOffsets[normalIndex];
+ if ((sourceOffset & 7) || normalOffset + sourceOffset + 8 > primitiveOffset)
+ return false;
+ const byte *source = data.data() + normalOffset + sourceOffset;
+ face.normalX[vertexIndex] = (int16)READ_LE_UINT16(source);
+ face.normalY[vertexIndex] = (int16)READ_LE_UINT16(source + 2);
+ face.normalZ[vertexIndex] = (int16)READ_LE_UINT16(source + 4);
+ }
+ }
+ static const byte uvOffsets[4] = { 14, 20, 22, 28 };
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ face.u[vertexIndex] = data[record + uvOffsets[vertexIndex]];
+ face.v[vertexIndex] = data[record + uvOffsets[vertexIndex] + 1];
+ face.r[vertexIndex] = data[record + 16];
+ face.g[vertexIndex] = data[record + 17];
+ face.b[vertexIndex] = data[record + 18];
+ }
+ if (mode & 0x40) {
+ face.r[1] = data[record + 6];
+ face.g[1] = data[record + 7];
+ face.b[1] = data[record + 8];
+ face.r[2] = data[record + 9];
+ face.g[2] = data[record + 10];
+ face.b[2] = data[record + 11];
+ if (face.vertexCount == 4) {
+ face.r[3] = data[record + 26];
+ face.g[3] = data[record + 27];
+ face.b[3] = data[record + 28];
+ }
+ }
+ if ((mode & 2) && data[record + 19] >= 0x80) {
+ const uint texture = data[record + 19] - 0x80;
+ if (texture >= _textures.size())
+ return false;
+ face.texture = texture;
+ }
_faces.push_back(face);
}
if (!terminated)
@@ -155,7 +304,8 @@ bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffs
#ifdef USE_TINYGL
-RA2PSXTinyGLRenderer::RA2PSXTinyGLRenderer() : _context(nullptr), _width(0), _height(0) {
+RA2PSXTinyGLRenderer::RA2PSXTinyGLRenderer() : _context(nullptr), _activeTexture(nullptr),
+ _textureEnabled(false), _blendEnabled(false), _width(0), _height(0) {
}
RA2PSXTinyGLRenderer::~RA2PSXTinyGLRenderer() {
@@ -178,10 +328,128 @@ bool RA2PSXTinyGLRenderer::init(int width, int height) {
tglDepthFunc(TGL_LESS);
tglDisable(TGL_LIGHTING);
tglDisable(TGL_TEXTURE_2D);
+ tglDisable(TGL_ALPHA_TEST);
+ tglDisable(TGL_BLEND);
tglDisable(TGL_CULL_FACE);
+ tglAlphaFunc(TGL_GREATER, 0.0f);
+ tglBlendFunc(TGL_SRC_ALPHA, TGL_ONE_MINUS_SRC_ALPHA);
+ tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_MODULATE);
return true;
}
+TGLuint RA2PSXTinyGLRenderer::getTextureId(const RA2PSXTexture &texture) {
+ for (uint i = 0; i < _textureBindings.size(); ++i) {
+ if (_textureBindings[i].texture == &texture)
+ return _textureBindings[i].id;
+ }
+
+ Common::Array<byte> rgba;
+ rgba.resize(texture.pixels.size() * 4);
+ for (uint i = 0; i < texture.pixels.size(); ++i) {
+ const uint32 pixel = texture.pixels[i];
+ rgba[i * 4] = (pixel >> 16) & 0xff;
+ rgba[i * 4 + 1] = (pixel >> 8) & 0xff;
+ rgba[i * 4 + 2] = pixel & 0xff;
+ rgba[i * 4 + 3] = !(pixel & 0x01000000) ? 0 :
+ (pixel & 0x02000000) ? 0x80 : 0xff;
+ }
+
+ TextureBinding binding;
+ binding.texture = &texture;
+ tglGenTextures(1, &binding.id);
+ tglBindTexture(TGL_TEXTURE_2D, binding.id);
+ tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MIN_FILTER, TGL_NEAREST);
+ tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MAG_FILTER, TGL_NEAREST);
+ tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_S, TGL_CLAMP_TO_EDGE);
+ tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_T, TGL_CLAMP_TO_EDGE);
+ tglTexImage2D(TGL_TEXTURE_2D, 0, TGL_RGBA, texture.width, texture.height, 0,
+ TGL_RGBA, TGL_UNSIGNED_BYTE, rgba.data());
+ _textureBindings.push_back(binding);
+ return binding.id;
+}
+
+void RA2PSXTinyGLRenderer::setFaceState(const RA2PSXModel &model, const RA2PSXFace &face) {
+ const RA2PSXTexture *texture = model.texture(face.texture);
+ if (texture) {
+ if (!_textureEnabled) {
+ tglEnable(TGL_TEXTURE_2D);
+ tglEnable(TGL_ALPHA_TEST);
+ _textureEnabled = true;
+ }
+ if (_activeTexture != texture) {
+ tglBindTexture(TGL_TEXTURE_2D, getTextureId(*texture));
+ _activeTexture = texture;
+ }
+ } else if (_textureEnabled) {
+ tglDisable(TGL_TEXTURE_2D);
+ tglDisable(TGL_ALPHA_TEST);
+ _textureEnabled = false;
+ }
+
+ const bool blend = (face.mode & 8) != 0;
+ if (blend != _blendEnabled) {
+ if (blend)
+ tglEnable(TGL_BLEND);
+ else
+ tglDisable(TGL_BLEND);
+ _blendEnabled = blend;
+ }
+}
+
+static int getRA2PSXDepthCue(float depth, float focalLength) {
+ if (depth <= 0.0f)
+ return 4096;
+ const int quotient = MIN(0x1ffff, (int)(focalLength * 65536.0f / depth));
+ return CLIP<int>((0x04440000LL - 28416LL * quotient) >> 12, 0, 4096);
+}
+
+void RA2PSXTinyGLRenderer::setFaceColor(const RA2PSXFace &face, uint vertexIndex,
+ float normalX, float normalY, float normalZ, float depth) {
+ int r = face.r[vertexIndex];
+ int g = face.g[vertexIndex];
+ int b = face.b[vertexIndex];
+ if (face.texture >= 0 && (face.mode & 4)) {
+ r = g = b = 0xff;
+ } else if (!(face.mode & 0x40)) {
+ static const int lightMatrix[3][3] = {
+ { -3640, 1820, 455 },
+ { 0, -4096, 0 },
+ { -2816, 0, 1024 }
+ };
+ static const int colorMatrix[3][3] = {
+ { 1648, 128, 2000 },
+ { 1648, 128, 2000 },
+ { 1648, 1024, 2000 }
+ };
+ const float normal[3] = { normalX, normalY, normalZ };
+ int light[3];
+ for (uint i = 0; i < 3; ++i) {
+ float value = 0.0f;
+ for (uint j = 0; j < 3; ++j)
+ value += lightMatrix[i][j] * normal[j];
+ light[i] = CLIP<int>((int)(value / 4096.0f), 0, 0x7fff);
+ }
+
+ int effect[3];
+ for (uint i = 0; i < 3; ++i) {
+ int value = 0;
+ for (uint j = 0; j < 3; ++j)
+ value += colorMatrix[i][j] * light[j];
+ effect[i] = 110 + MAX(0, value / 4096) / 16;
+ }
+ const int depthScale = 4096 - getRA2PSXDepthCue(depth, _width * 2.0f);
+ r = MIN(255, r * effect[0] / 256) * depthScale / 4096;
+ g = MIN(255, g * effect[1] / 256) * depthScale / 4096;
+ b = MIN(255, b * effect[2] / 256) * depthScale / 4096;
+ }
+ if (face.texture >= 0 && !(face.mode & 4)) {
+ r = MIN(r * 2, 0xff);
+ g = MIN(g * 2, 0xff);
+ b = MIN(b * 2, 0xff);
+ }
+ tglColor4ub(r, g, b, (face.texture < 0 && (face.mode & 8)) ? 0x80 : 0xff);
+}
+
void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
if (!_context)
return;
@@ -222,14 +490,63 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
tglRotatef(yaw, 0.0f, 1.0f, 0.0f);
const float scale = size / model.radius();
tglScalef(scale, -scale, scale);
+ const float pitchAngle = pitch * 0.017453292519943295f;
+ const float yawAngle = yaw * 0.017453292519943295f;
+ const float rollAngle = roll * 0.017453292519943295f;
+ const float pitchCosine = cosf(pitchAngle);
+ const float pitchSine = sinf(pitchAngle);
+ const float yawCosine = cosf(yawAngle);
+ const float yawSine = sinf(yawAngle);
+ const float rollCosine = cosf(rollAngle);
+ const float rollSine = sinf(rollAngle);
+ const float depth = model.radius() * (_width * 2.0f) / size;
const Common::Array<RA2PSXVertex> &vertices = model.vertices();
+ struct FacingVertex {
+ float x;
+ float y;
+ };
+ Common::Array<FacingVertex> facingVertices;
+ facingVertices.resize(vertices.size());
+ for (uint i = 0; i < vertices.size(); ++i) {
+ const float modelX = vertices[i].x;
+ const float modelY = -vertices[i].y;
+ const float modelZ = vertices[i].z;
+ const float yawX = yawCosine * modelX + yawSine * modelZ;
+ const float yawZ = -yawSine * modelX + yawCosine * modelZ;
+ const float pitchY = pitchCosine * modelY - pitchSine * yawZ;
+ facingVertices[i].x = rollCosine * yawX - rollSine * pitchY;
+ facingVertices[i].y = rollSine * yawX + rollCosine * pitchY;
+ }
+
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);
+ if (!(face.mode & 0x60)) {
+ const FacingVertex &v0 = facingVertices[face.vertex[0]];
+ const FacingVertex &v1 = facingVertices[face.vertex[1]];
+ const FacingVertex &v2 = facingVertices[face.vertex[2]];
+ if (v0.x * (v1.y - v2.y) + v1.x * (v2.y - v0.y) +
+ v2.x * (v0.y - v1.y) <= 0.0f)
+ continue;
+ }
+ const RA2PSXTexture *texture = model.texture(face.texture);
+ setFaceState(model, face);
tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ const float normalX = face.normalX[vertexIndex];
+ const float normalY = -face.normalY[vertexIndex];
+ const float normalZ = face.normalZ[vertexIndex];
+ const float yawX = yawCosine * normalX + yawSine * normalZ;
+ const float yawZ = -yawSine * normalX + yawCosine * normalZ;
+ const float pitchY = pitchCosine * normalY - pitchSine * yawZ;
+ const float pitchZ = pitchSine * normalY + pitchCosine * yawZ;
+ setFaceColor(face, vertexIndex,
+ rollCosine * yawX - rollSine * pitchY,
+ rollSine * yawX + rollCosine * pitchY, pitchZ, depth);
+ if (texture)
+ tglTexCoord2f((face.u[vertexIndex] + 0.5f) / texture->width,
+ (face.v[vertexIndex] + 0.5f) / texture->height);
const RA2PSXVertex &vertex = vertices[face.vertex[vertexIndex]];
tglVertex3f((float)vertex.x, (float)vertex.y, (float)vertex.z);
}
@@ -281,6 +598,7 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
struct ProjectedVertex {
float x;
float y;
+ float z;
bool visible;
};
Common::Array<ProjectedVertex> projected;
@@ -293,6 +611,7 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
const float worldX = x + modelXx * vertex.x + modelYx * vertex.y + forwardX * vertex.z;
const float worldY = y + modelXy * vertex.x + modelYy * vertex.y + forwardY * vertex.z;
const float worldZ = z + modelXz * vertex.x + modelYz * vertex.y + forwardZ * vertex.z;
+ projected[i].z = worldZ;
projected[i].visible = worldZ > 1.0f;
if (projected[i].visible) {
projected[i].x = centerX + worldX * focalLength / worldZ;
@@ -311,10 +630,33 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
visible &= projected[face.vertex[vertexIndex]].visible;
if (!visible)
continue;
+ if (!(face.mode & 0x60)) {
+ const ProjectedVertex &v0 = projected[face.vertex[0]];
+ const ProjectedVertex &v1 = projected[face.vertex[1]];
+ const ProjectedVertex &v2 = projected[face.vertex[2]];
+ if (v0.x * (v1.y - v2.y) + v1.x * (v2.y - v0.y) +
+ v2.x * (v0.y - v1.y) <= 0.0f)
+ continue;
+ }
- tglColor3ub(face.r, face.g, face.b);
+ const RA2PSXTexture *texture = model.texture(face.texture);
+ setFaceState(model, face);
+ const float faceDepth = projected[face.vertex[2]].z;
tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ setFaceColor(face, vertexIndex,
+ modelXx * face.normalX[vertexIndex] +
+ modelYx * face.normalY[vertexIndex] +
+ forwardX * face.normalZ[vertexIndex],
+ modelXy * face.normalX[vertexIndex] +
+ modelYy * face.normalY[vertexIndex] +
+ forwardY * face.normalZ[vertexIndex],
+ modelXz * face.normalX[vertexIndex] +
+ modelYz * face.normalY[vertexIndex] +
+ forwardZ * face.normalZ[vertexIndex], faceDepth);
+ if (texture)
+ tglTexCoord2f((face.u[vertexIndex] + 0.5f) / texture->width,
+ (face.v[vertexIndex] + 0.5f) / texture->height);
const ProjectedVertex &vertex = projected[face.vertex[vertexIndex]];
tglVertex3f(vertex.x, vertex.y, 0.0f);
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index d92941bb883..6b37b003e46 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -129,8 +129,12 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
Common::Array<byte> shipData;
Common::Array<byte> crosshairData;
Common::Array<byte> laserData;
+ Common::Array<byte> enemyTextureData;
+ Common::Array<byte> shipTextureData;
return archive.getMember("fOFS/TieFighter/main", enemyData) && enemy.load(enemyData) &&
+ archive.getMember("tex/Ties", enemyTextureData) && enemy.loadTextures(enemyTextureData) &&
archive.getMember("fOFS/Ship", shipData) && ship.load(shipData) &&
+ archive.getMember("tex/BWingCockp", shipTextureData) && ship.loadTextures(shipTextureData) &&
archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
ui.load(archive);
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index c9623c2a25e..060a9a8d908 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -63,12 +63,29 @@ struct RA2PSXVertex {
int16 z;
};
+struct RA2PSXTexture {
+ Common::String name;
+ uint16 width;
+ uint16 height;
+ Common::Array<uint32> pixels;
+};
+
+bool loadRA2PSXTextures(const Common::Array<byte> &data,
+ Common::Array<RA2PSXTexture> &textures);
+
struct RA2PSXFace {
uint16 vertex[4];
+ int16 normalX[4];
+ int16 normalY[4];
+ int16 normalZ[4];
+ byte u[4];
+ byte v[4];
byte vertexCount;
- byte r;
- byte g;
- byte b;
+ byte mode;
+ byte r[4];
+ byte g[4];
+ byte b[4];
+ int16 texture;
};
class RA2PSXModel {
@@ -76,8 +93,10 @@ public:
RA2PSXModel();
bool load(const Common::Array<byte> &data);
+ bool loadTextures(const Common::Array<byte> &data);
const Common::Array<RA2PSXVertex> &vertices() const { return _vertices; }
const Common::Array<RA2PSXFace> &faces() const { return _faces; }
+ const RA2PSXTexture *texture(int index) const;
float radius() const { return _radius; }
private:
@@ -86,6 +105,7 @@ private:
Common::Array<RA2PSXVertex> _vertices;
Common::Array<RA2PSXFace> _faces;
+ Common::Array<RA2PSXTexture> _textures;
float _radius;
};
@@ -105,7 +125,21 @@ public:
void finishFrame(Graphics::Surface &surface);
private:
+ struct TextureBinding {
+ const RA2PSXTexture *texture;
+ TGLuint id;
+ };
+
+ void setFaceState(const RA2PSXModel &model, const RA2PSXFace &face);
+ void setFaceColor(const RA2PSXFace &face, uint vertexIndex,
+ float normalX, float normalY, float normalZ, float depth);
+ TGLuint getTextureId(const RA2PSXTexture &texture);
+
TinyGL::ContextHandle *_context;
+ Common::Array<TextureBinding> _textureBindings;
+ const RA2PSXTexture *_activeTexture;
+ bool _textureEnabled;
+ bool _blendEnabled;
int _width;
int _height;
};
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index a34df27bfc9..cb8066f8b0a 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -10,10 +10,8 @@
* (at your option) any later version.
*/
-#include "common/endian.h"
#include "common/util.h"
-#include "scumm/insane/rebel2/psx/psx.h"
#include "scumm/insane/rebel2/psx/ui.h"
namespace Scumm {
@@ -50,59 +48,10 @@ static RA2PSXUIColor shieldColor(const RA2PSXUIGradientStop *stops, uint count,
}
bool RA2PSXLevel1UI::loadTextures(const Common::Array<byte> &data) {
- const uint initialCount = _textures.size();
- uint32 offset = 0;
- while (offset + 20 <= data.size()) {
- Common::String name;
- for (uint i = 0; i < 8 && data[offset + i]; ++i) {
- if (data[offset + i] < 0x20 || data[offset + i] >= 0x7f)
- return _textures.size() > initialCount;
- name += (char)data[offset + i];
- }
- if (name.empty())
- break;
-
- const uint16 recordSize = READ_LE_UINT16(data.data() + offset + 12);
- const uint16 widthField = READ_LE_UINT16(data.data() + offset + 16);
- const uint16 heightField = READ_LE_UINT16(data.data() + offset + 18);
- const uint16 width = (widthField & 0xff) ? widthField & 0xff : 256;
- const uint16 height = (heightField & 0xff) ? heightField & 0xff : 256;
- const bool eightBit = (widthField & 0x100) != 0;
- const uint32 paletteColors = eightBit ? 256 : 16;
- const uint32 pixelCount = (uint32)width * height;
- const uint32 pixelBytes = eightBit ? pixelCount : (pixelCount + 1) / 2;
- const uint32 paletteOffset = offset + 20;
- const uint32 pixelsOffset = paletteOffset + paletteColors * 2;
- if (recordSize < 20 || offset + recordSize > data.size() ||
- pixelsOffset + pixelBytes > offset + recordSize)
- break;
-
- Texture texture;
- texture.name = name;
- texture.width = width;
- texture.height = height;
- texture.pixels.resize(pixelCount);
- for (uint32 i = 0; i < pixelCount; ++i) {
- const byte packed = data[pixelsOffset + (eightBit ? i : i / 2)];
- const byte paletteIndex = eightBit ? packed : ((i & 1) ? packed >> 4 : packed & 0xf);
- const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
- if (!value) {
- texture.pixels[i] = 0;
- continue;
- }
-
- const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
- const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
- const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
- texture.pixels[i] = 0x01000000 | (r << 16) | (g << 8) | b;
- }
- _textures.push_back(texture);
- offset += recordSize;
- }
- return _textures.size() > initialCount;
+ return loadRA2PSXTextures(data, _textures);
}
-const RA2PSXLevel1UI::Texture *RA2PSXLevel1UI::findTexture(const char *name) const {
+const RA2PSXTexture *RA2PSXLevel1UI::findTexture(const char *name) const {
for (uint i = 0; i < _textures.size(); ++i) {
if (_textures[i].name.equalsIgnoreCase(name))
return &_textures[i];
@@ -131,7 +80,7 @@ bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
void RA2PSXLevel1UI::drawTexture(Graphics::Surface &surface, const char *name,
int x, int y, const Common::Rect &source, int brightness, BlendMode blend) const {
- const Texture *texture = findTexture(name);
+ const RA2PSXTexture *texture = findTexture(name);
if (!texture)
return;
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 1adbe99831f..6665d5875cd 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -13,16 +13,13 @@
#ifndef SCUMM_INSANE_REBEL2_PSX_UI_H
#define SCUMM_INSANE_REBEL2_PSX_UI_H
-#include "common/array.h"
#include "common/rect.h"
-#include "common/str.h"
-#include "common/types.h"
#include "graphics/surface.h"
-namespace Scumm {
+#include "scumm/insane/rebel2/psx/psx.h"
-class RA2PSXArchive;
+namespace Scumm {
class RA2PSXLevel1UI {
public:
@@ -38,21 +35,14 @@ private:
kBlendAdditive
};
- struct Texture {
- Common::String name;
- uint16 width;
- uint16 height;
- Common::Array<uint32> pixels;
- };
-
bool loadTextures(const Common::Array<byte> &data);
- const Texture *findTexture(const char *name) const;
+ const RA2PSXTexture *findTexture(const char *name) const;
void drawTexture(Graphics::Surface &surface, const char *name,
int x, int y, const Common::Rect &source, int brightness = 0x80,
BlendMode blend = kBlendOpaque) const;
void drawShield(Graphics::Surface &surface, int shield, int xOffset, int yOffset) const;
- Common::Array<Texture> _textures;
+ Common::Array<RA2PSXTexture> _textures;
};
} // End of namespace Scumm
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 7fc8bde06ec..b191cbe5805 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1075,8 +1075,6 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
Common::KeymapArray keymaps = MetaEngine::initKeymaps(target);
Common::String gameId = ConfMan.get("gameid", target);
- const bool isRebel2PSX = gameId == "rebel2" &&
- parsePlatform(ConfMan.get("platform", target)) == kPlatformPSX;
Action *act;
if (gameId == "rebel1" || gameId == "rebel2") {
@@ -1252,6 +1250,7 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
}
if (gameId == "rebel2") {
+ const bool isRebel2PSX = parsePlatform(ConfMan.get("platform", target)) == kPlatformPSX;
Keymap *rebel2Keymap = new Keymap(Keymap::kKeymapTypeGame, "scumm-rebel2", _("Rebel Assault II controls"));
act = new Action("RA2UP", _("Aim up / menu up"));
Commit: f1ee431e61fd840e22db64e18ad02f6dcae568f3
https://github.com/scummvm/scummvm/commit/f1ee431e61fd840e22db64e18ad02f6dcae568f3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: render all the triangles for the psx release
Changed paths:
engines/scumm/insane/rebel2/psx/model.cpp
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index 942ca1df5b1..1ff211f14b6 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -10,6 +10,7 @@
* (at your option) any later version.
*/
+#include "common/algorithm.h"
#include "common/endian.h"
#include "common/system.h"
#include "common/util.h"
@@ -482,14 +483,7 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
TinyGL::setContext(_context);
if (!depthTest)
tglDisable(TGL_DEPTH_TEST);
- 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 float pitchAngle = pitch * 0.017453292519943295f;
const float yawAngle = yaw * 0.017453292519943295f;
const float rollAngle = roll * 0.017453292519943295f;
@@ -505,6 +499,7 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
struct FacingVertex {
float x;
float y;
+ float z;
};
Common::Array<FacingVertex> facingVertices;
facingVertices.resize(vertices.size());
@@ -517,22 +512,19 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
const float pitchY = pitchCosine * modelY - pitchSine * yawZ;
facingVertices[i].x = rollCosine * yawX - rollSine * pitchY;
facingVertices[i].y = rollSine * yawX + rollCosine * pitchY;
+ facingVertices[i].z = pitchSine * modelY + pitchCosine * yawZ;
}
const Common::Array<RA2PSXFace> &faces = model.faces();
for (uint faceIndex = 0; faceIndex < faces.size(); ++faceIndex) {
const RA2PSXFace &face = faces[faceIndex];
- if (!(face.mode & 0x60)) {
- const FacingVertex &v0 = facingVertices[face.vertex[0]];
- const FacingVertex &v1 = facingVertices[face.vertex[1]];
- const FacingVertex &v2 = facingVertices[face.vertex[2]];
- if (v0.x * (v1.y - v2.y) + v1.x * (v2.y - v0.y) +
- v2.x * (v0.y - v1.y) <= 0.0f)
- continue;
- }
const RA2PSXTexture *texture = model.texture(face.texture);
setFaceState(model, face);
- tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
+ float faceDepth = 0.0f;
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex)
+ faceDepth += facingVertices[face.vertex[vertexIndex]].z;
+ faceDepth = faceDepth * scale / face.vertexCount;
+ tglBegin(face.vertexCount == 4 ? TGL_QUAD_STRIP : TGL_TRIANGLES);
for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
const float normalX = face.normalX[vertexIndex];
const float normalY = -face.normalY[vertexIndex];
@@ -547,12 +539,11 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
if (texture)
tglTexCoord2f((face.u[vertexIndex] + 0.5f) / texture->width,
(face.v[vertexIndex] + 0.5f) / texture->height);
- const RA2PSXVertex &vertex = vertices[face.vertex[vertexIndex]];
- tglVertex3f((float)vertex.x, (float)vertex.y, (float)vertex.z);
+ const FacingVertex &vertex = facingVertices[face.vertex[vertexIndex]];
+ tglVertex3f(x + vertex.x * scale, y + vertex.y * scale, faceDepth);
}
tglEnd();
}
- tglPopMatrix();
if (!depthTest)
tglEnable(TGL_DEPTH_TEST);
}
@@ -618,31 +609,44 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
projected[i].y = centerY + worldY * focalLength / worldZ;
}
}
+ struct ProjectedFace {
+ uint index;
+ float depth;
- TinyGL::setContext(_context);
- if (!depthTest)
- tglDisable(TGL_DEPTH_TEST);
+ bool operator<(const ProjectedFace &other) const {
+ return depth != other.depth ? depth > other.depth : index < other.index;
+ }
+ };
+ Common::Array<ProjectedFace> projectedFaces;
const Common::Array<RA2PSXFace> &faces = model.faces();
for (uint faceIndex = 0; faceIndex < faces.size(); ++faceIndex) {
const RA2PSXFace &face = faces[faceIndex];
+ ProjectedFace projectedFace = { faceIndex, 0.0f };
bool visible = true;
- for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex)
- visible &= projected[face.vertex[vertexIndex]].visible;
- if (!visible)
- continue;
- if (!(face.mode & 0x60)) {
- const ProjectedVertex &v0 = projected[face.vertex[0]];
- const ProjectedVertex &v1 = projected[face.vertex[1]];
- const ProjectedVertex &v2 = projected[face.vertex[2]];
- if (v0.x * (v1.y - v2.y) + v1.x * (v2.y - v0.y) +
- v2.x * (v0.y - v1.y) <= 0.0f)
- continue;
+ for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
+ const ProjectedVertex &vertex = projected[face.vertex[vertexIndex]];
+ visible &= vertex.visible;
+ projectedFace.depth += vertex.z;
+ }
+ if (visible) {
+ projectedFace.depth /= face.vertexCount;
+ projectedFaces.push_back(projectedFace);
}
+ }
+ Common::sort(projectedFaces.begin(), projectedFaces.end());
+
+ TinyGL::setContext(_context);
+ if (!depthTest)
+ tglDisable(TGL_DEPTH_TEST);
+ for (uint faceIndex = 0; faceIndex < projectedFaces.size(); ++faceIndex) {
+ const ProjectedFace &projectedFace = projectedFaces[faceIndex];
+ const RA2PSXFace &face = faces[projectedFace.index];
+ const float orderingDepth = (z - projectedFace.depth) * 512.0f / model.radius();
const RA2PSXTexture *texture = model.texture(face.texture);
setFaceState(model, face);
const float faceDepth = projected[face.vertex[2]].z;
- tglBegin(face.vertexCount == 4 ? TGL_QUADS : TGL_TRIANGLES);
+ tglBegin(face.vertexCount == 4 ? TGL_QUAD_STRIP : TGL_TRIANGLES);
for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
setFaceColor(face, vertexIndex,
modelXx * face.normalX[vertexIndex] +
@@ -658,7 +662,7 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
tglTexCoord2f((face.u[vertexIndex] + 0.5f) / texture->width,
(face.v[vertexIndex] + 0.5f) / texture->height);
const ProjectedVertex &vertex = projected[face.vertex[vertexIndex]];
- tglVertex3f(vertex.x, vertex.y, 0.0f);
+ tglVertex3f(vertex.x, vertex.y, orderingDepth);
}
tglEnd();
}
Commit: 8875e80135a78742f9cdfb526c94b5242a743ef1
https://github.com/scummvm/scummvm/commit/8875e80135a78742f9cdfb526c94b5242a743ef1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: improved sound support for psx release
Changed paths:
A engines/scumm/insane/rebel2/psx/audio.cpp
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/resource.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
new file mode 100644
index 00000000000..17872212995
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -0,0 +1,679 @@
+/* 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 "audio/audiostream.h"
+#include "audio/decoders/xa.h"
+#include "audio/mixer.h"
+
+#include "common/endian.h"
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/util.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/psx/psx.h"
+
+namespace Scumm {
+
+namespace {
+
+class RA2PSXADSRStream : public Audio::RewindableAudioStream {
+public:
+ RA2PSXADSRStream(Audio::RewindableAudioStream *stream, uint16 attack,
+ uint16 decay, uint16 sustain)
+ : _stream(stream), _position(0) {
+ const uint32 rate = _stream->getRate();
+ _attackSamples = ((uint64)attack * rate + 999) / 1000;
+ _decaySamples = ((uint64)decay * rate + 999) / 1000;
+ _sustain = MIN<uint32>(sustain, 0x1000) * 0x10000 / 0x1000;
+ }
+
+ ~RA2PSXADSRStream() override { delete _stream; }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ const int count = _stream->readBuffer(buffer, numSamples);
+ if (count <= 0)
+ return count;
+
+ for (int i = 0; i < count; ++i, ++_position) {
+ uint32 gain;
+ if (_attackSamples && _position < _attackSamples) {
+ gain = (uint32)(_position * 0x10000 / _attackSamples);
+ } else if (_decaySamples && _position < _attackSamples + _decaySamples) {
+ const uint64 decayPosition = _position - _attackSamples;
+ gain = 0x10000 - (uint32)(decayPosition * (0x10000 - _sustain) /
+ _decaySamples);
+ } else {
+ gain = _sustain;
+ }
+ buffer[i] = (int16)((int64)buffer[i] * gain / 0x10000);
+ }
+ return count;
+ }
+
+ bool isStereo() const override { return _stream->isStereo(); }
+ bool endOfData() const override { return _stream->endOfData(); }
+ int getRate() const override { return _stream->getRate(); }
+
+ bool rewind() override {
+ _position = 0;
+ return _stream->rewind();
+ }
+
+private:
+ Audio::RewindableAudioStream *_stream;
+ uint64 _position;
+ uint64 _attackSamples;
+ uint64 _decaySamples;
+ uint32 _sustain;
+};
+
+static bool matchesTag(const Common::Array<byte> &data, uint32 offset, const char *tag) {
+ return offset + 4 <= data.size() && !memcmp(data.data() + offset, tag, 4);
+}
+
+static bool timeReached(uint32 now, uint32 target) {
+ return (int32)(now - target) >= 0;
+}
+
+static int soundBalance(int pan) {
+ return CLIP((pan - 64) * 2, -127, 127);
+}
+
+} // End of anonymous namespace
+
+bool RA2PSXSoundBank::load(const Common::Array<byte> &sampleData,
+ const Common::Array<byte> &projectData) {
+ Common::Array<Sample> samples;
+ Common::Array<SFX> sfx;
+ Common::Array<Macro> macros;
+ Common::Array<ADSR> adsrs;
+
+ if (sampleData.size() < 12 || projectData.size() < 32 ||
+ !matchesTag(projectData, 0, "PROJ"))
+ return false;
+
+ const uint32 directorySize = READ_LE_UINT32(sampleData.data());
+ if (!directorySize || directorySize % 12 || directorySize > sampleData.size())
+ return false;
+
+ for (uint32 offset = 0; offset < directorySize; offset += 12) {
+ Sample sample;
+ sample.offset = READ_LE_UINT32(sampleData.data() + offset);
+ sample.id = READ_LE_UINT16(sampleData.data() + offset + 4);
+ sample.blocks = READ_LE_UINT16(sampleData.data() + offset + 6);
+ sample.rate = READ_LE_UINT16(sampleData.data() + offset + 8);
+ const uint64 end = (uint64)sample.offset + (uint64)sample.blocks * 16;
+ if (sample.offset < directorySize || !sample.blocks || !sample.rate ||
+ end > sampleData.size())
+ return false;
+ samples.push_back(sample);
+ }
+
+ uint32 macroTag = 0xffffffff;
+ uint32 sfxTag = 0xffffffff;
+ for (uint32 offset = 0; offset + 4 <= projectData.size(); ++offset) {
+ if (macroTag == 0xffffffff && matchesTag(projectData, offset, "MACR"))
+ macroTag = offset;
+ if (matchesTag(projectData, offset, "SFXG"))
+ sfxTag = offset;
+ }
+ if (macroTag == 0xffffffff || macroTag + 16 > projectData.size() ||
+ sfxTag == 0xffffffff || sfxTag + 24 > projectData.size())
+ return false;
+
+ const uint32 macroSection = READ_LE_UINT32(projectData.data() + macroTag + 12);
+ if (macroSection > projectData.size() - 20)
+ return false;
+ const uint32 macroPointer = macroSection + 16;
+ const uint64 macroBase64 = (uint64)macroPointer +
+ READ_LE_UINT32(projectData.data() + macroPointer);
+ if (macroBase64 < 4 || macroBase64 > projectData.size())
+ return false;
+ const uint32 macroBase = (uint32)macroBase64;
+ for (uint32 record = macroBase - 4; record + 8 <= projectData.size() &&
+ macros.size() < 256; record += 8) {
+ const int32 relative = (int32)READ_LE_UINT32(projectData.data() + record + 4);
+ const int64 target = (int64)macroBase + relative;
+ if (target < 0 || target + 8 > (int64)projectData.size() || (target & 3))
+ break;
+ Macro macro;
+ macro.id = READ_LE_UINT32(projectData.data() + record);
+ macro.offset = (uint32)target;
+ macros.push_back(macro);
+ }
+ if (macros.empty())
+ return false;
+
+ const uint32 sfxCount = READ_LE_UINT32(projectData.data() + sfxTag + 20);
+ if (!sfxCount || sfxCount > 256 ||
+ (uint64)sfxTag + 24 + (uint64)sfxCount * 10 > projectData.size())
+ return false;
+ for (uint32 i = 0; i < sfxCount; ++i) {
+ const uint32 record = sfxTag + 24 + i * 10;
+ SFX entry;
+ entry.id = READ_LE_UINT16(projectData.data() + record);
+ entry.macro = READ_LE_UINT16(projectData.data() + record + 2);
+ entry.priority = projectData[record + 4];
+ entry.maxVoices = projectData[record + 5];
+ sfx.push_back(entry);
+ }
+
+ uint32 tableEnd = 0xffffffff;
+ if (projectData.size() >= 8) {
+ for (uint32 offset = projectData.size() - 8;; offset -= 4) {
+ if (READ_LE_UINT32(projectData.data() + offset) == 0xffffffff &&
+ READ_LE_UINT32(projectData.data() + offset + 4) == 0xffffffff) {
+ tableEnd = offset;
+ break;
+ }
+ if (offset < 4)
+ break;
+ }
+ }
+ if (tableEnd == 0xffffffff)
+ return false;
+
+ uint32 tableStart = tableEnd;
+ int32 expectedRelative = -8;
+ while (tableStart >= 8) {
+ const uint32 record = tableStart - 8;
+ const int32 relative = (int32)READ_LE_UINT32(projectData.data() + record);
+ const uint32 id = READ_LE_UINT32(projectData.data() + record + 4);
+ if (relative != expectedRelative || id > 0xffff)
+ break;
+ tableStart = record;
+ expectedRelative -= 8;
+ }
+ for (uint32 record = tableStart; record < tableEnd; record += 8) {
+ const int32 relative = (int32)READ_LE_UINT32(projectData.data() + record);
+ const int64 target = (int64)tableStart + relative;
+ if (target < 0 || target + 8 > tableStart)
+ return false;
+ ADSR adsr;
+ adsr.id = (uint16)READ_LE_UINT32(projectData.data() + record + 4);
+ adsr.attack = READ_LE_UINT16(projectData.data() + target);
+ adsr.decay = READ_LE_UINT16(projectData.data() + target + 2);
+ adsr.sustain = READ_LE_UINT16(projectData.data() + target + 4);
+ adsr.release = READ_LE_UINT16(projectData.data() + target + 6);
+ adsrs.push_back(adsr);
+ }
+ if (adsrs.empty())
+ return false;
+
+ _data = sampleData;
+ _projectData = projectData;
+ _samples = samples;
+ _sfx = sfx;
+ _macros = macros;
+ _adsrs = adsrs;
+ return true;
+}
+
+const RA2PSXSoundBank::Sample *RA2PSXSoundBank::findSample(uint16 id) const {
+ for (uint i = 0; i < _samples.size(); ++i) {
+ if (_samples[i].id == id)
+ return &_samples[i];
+ }
+ return nullptr;
+}
+
+const RA2PSXSoundBank::ADSR *RA2PSXSoundBank::findADSR(uint16 id) const {
+ for (uint i = 0; i < _adsrs.size(); ++i) {
+ if (_adsrs[i].id == id)
+ return &_adsrs[i];
+ }
+ return nullptr;
+}
+
+bool RA2PSXSoundBank::getSFX(uint16 id, uint16 ¯o, byte &priority,
+ byte &maxVoices) const {
+ for (uint i = 0; i < _sfx.size(); ++i) {
+ if (_sfx[i].id != id)
+ continue;
+ macro = _sfx[i].macro;
+ priority = _sfx[i].priority;
+ maxVoices = _sfx[i].maxVoices;
+ return true;
+ }
+ return false;
+}
+
+bool RA2PSXSoundBank::getMacroCommand(uint16 macro, uint16 step, byte *command) const {
+ for (uint i = 0; i < _macros.size(); ++i) {
+ if (_macros[i].id != macro)
+ continue;
+ const uint64 offset = (uint64)_macros[i].offset + (uint64)step * 8;
+ if (offset + 8 > _projectData.size())
+ return false;
+ memcpy(command, _projectData.data() + offset, 8);
+ return true;
+ }
+ return false;
+}
+
+Audio::RewindableAudioStream *RA2PSXSoundBank::makeStream(uint16 id, uint32 rate,
+ uint16 adsrId) const {
+ const Sample *sample = findSample(id);
+ if (!sample)
+ return nullptr;
+
+ const uint32 size = (uint32)sample->blocks * 16;
+ byte *copy = (byte *)malloc(size);
+ if (!copy)
+ return nullptr;
+ memcpy(copy, _data.data() + sample->offset, size);
+
+ Common::SeekableReadStream *source =
+ new Common::MemoryReadStream(copy, size, DisposeAfterUse::YES);
+ Audio::RewindableAudioStream *stream =
+ Audio::makeXAStream(source, rate ? rate : sample->rate);
+ const ADSR *adsr = findADSR(adsrId);
+ if (adsr)
+ stream = new RA2PSXADSRStream(stream, adsr->attack, adsr->decay, adsr->sustain);
+ return stream;
+}
+
+struct RA2PSXSoundPlayer::Impl {
+ enum {
+ kVoiceCount = 24,
+ kGroupCount = 64,
+ kMacroTicksPerSecond = 60,
+ kVoiceStartupTicks = 4
+ };
+
+ struct Voice {
+ Voice() : active(false), waiting(false), waitForSampleEnd(false), macroDone(false),
+ root(false), sound(0), macro(0), step(0), adsr(0xffff), rate(0),
+ rateOverride(-1), volume(0), pan(64), priority(0), born(0), readyTick(0),
+ startedAt(0), waitUntil(0) {}
+
+ bool active;
+ bool waiting;
+ bool waitForSampleEnd;
+ bool macroDone;
+ bool root;
+ SoundId sound;
+ uint16 macro;
+ uint16 step;
+ uint16 adsr;
+ uint32 rate;
+ int rateOverride;
+ int volume;
+ int pan;
+ byte priority;
+ uint32 born;
+ uint32 readyTick;
+ uint32 startedAt;
+ uint32 waitUntil;
+ Audio::SoundHandle handle;
+ };
+
+ struct Group {
+ Group() : active(false), hasExpiry(false), sound(0), born(0), expiry(0) {}
+
+ bool active;
+ bool hasExpiry;
+ SoundId sound;
+ uint32 born;
+ uint32 expiry;
+ };
+
+ Impl(ScummEngine_v7 *engine, const RA2PSXSoundBank &soundBank)
+ : vm(engine), bank(soundBank), nextSound(1), tick(1), serial(1),
+ nextUpdate(0), tickRemainder(0), randomState(0x13579bdf) {}
+
+ ScummEngine_v7 *vm;
+ const RA2PSXSoundBank &bank;
+ Voice voices[kVoiceCount];
+ Group groups[kGroupCount];
+ SoundId nextSound;
+ uint32 tick;
+ uint32 serial;
+ uint32 nextUpdate;
+ uint32 tickRemainder;
+ uint32 randomState;
+
+ int findGroup(SoundId sound) const {
+ for (int i = 0; i < kGroupCount; ++i) {
+ if (groups[i].active && groups[i].sound == sound)
+ return i;
+ }
+ return -1;
+ }
+
+ void clearVoice(int index) {
+ if (voices[index].active)
+ vm->_mixer->stopHandle(voices[index].handle);
+ voices[index] = Voice();
+ }
+
+ void stopGroup(int index) {
+ const SoundId sound = groups[index].sound;
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (voices[i].active && voices[i].sound == sound)
+ clearVoice(i);
+ }
+ groups[index] = Group();
+ }
+
+ int allocateGroup(SoundId sound) {
+ int slot = -1;
+ for (int i = 0; i < kGroupCount; ++i) {
+ if (!groups[i].active) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0) {
+ slot = 0;
+ for (int i = 1; i < kGroupCount; ++i) {
+ if (groups[i].born < groups[slot].born)
+ slot = i;
+ }
+ stopGroup(slot);
+ }
+ groups[slot].active = true;
+ groups[slot].sound = sound;
+ groups[slot].born = serial++;
+ return slot;
+ }
+
+ int allocateVoice(uint16 macro, byte priority, byte maxVoices, int excluded) {
+ int sameMacro = 0;
+ int oldestSame = -1;
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (!voices[i].active || voices[i].macro != macro || i == excluded)
+ continue;
+ ++sameMacro;
+ if (oldestSame < 0 || voices[i].born < voices[oldestSame].born)
+ oldestSame = i;
+ }
+ if (maxVoices < kVoiceCount && sameMacro >= maxVoices) {
+ if (oldestSame < 0)
+ return -1;
+ clearVoice(oldestSame);
+ return oldestSame;
+ }
+
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (!voices[i].active)
+ return i;
+ }
+
+ int victim = -1;
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (i == excluded || voices[i].priority > priority)
+ continue;
+ if (victim < 0 || voices[i].priority < voices[victim].priority ||
+ (voices[i].priority == voices[victim].priority &&
+ voices[i].born < voices[victim].born))
+ victim = i;
+ }
+ if (victim >= 0)
+ clearVoice(victim);
+ return victim;
+ }
+
+ int startVoice(SoundId sound, uint16 macro, uint16 step, byte priority,
+ byte maxVoices, int volume, int pan, int rateOverride, bool root,
+ uint32 now, int excluded) {
+ const int slot = allocateVoice(macro, priority, maxVoices, excluded);
+ if (slot < 0)
+ return -1;
+
+ Voice &voice = voices[slot];
+ voice.active = true;
+ voice.root = root;
+ voice.sound = sound;
+ voice.macro = macro;
+ voice.step = step;
+ voice.rateOverride = rateOverride;
+ voice.volume = CLIP(volume, 0, 127);
+ voice.pan = CLIP(pan, 0, 127);
+ voice.priority = priority;
+ voice.born = serial++;
+ voice.readyTick = tick + kVoiceStartupTicks;
+ voice.startedAt = now;
+ return slot;
+ }
+
+ void setGroupExpiry(SoundId sound, uint32 expiry) {
+ const int group = findGroup(sound);
+ if (group >= 0) {
+ groups[group].hasExpiry = true;
+ groups[group].expiry = expiry;
+ }
+ }
+
+ bool nextCommandEnds(const Voice &voice) const {
+ byte command[8];
+ return bank.getMacroCommand(voice.macro, voice.step, command) &&
+ ((command[0] & 0x3f) == 0 || (command[0] & 0x3f) == 1);
+ }
+
+ void finishVoice(int index) {
+ const SoundId sound = voices[index].sound;
+ if (voices[index].root) {
+ const int group = findGroup(sound);
+ if (group >= 0)
+ stopGroup(group);
+ return;
+ }
+ voices[index].macroDone = true;
+ if (!vm->_mixer->isSoundHandleActive(voices[index].handle))
+ clearVoice(index);
+ }
+
+ void runVoice(int index, uint32 now) {
+ for (int commandCount = 0; commandCount < 64; ++commandCount) {
+ if (!voices[index].active)
+ return;
+ Voice &voice = voices[index];
+ byte command[8];
+ if (!bank.getMacroCommand(voice.macro, voice.step++, command)) {
+ finishVoice(index);
+ return;
+ }
+
+ switch (command[0] & 0x3f) {
+ case 0:
+ case 1:
+ finishVoice(index);
+ return;
+ case 7: {
+ uint32 milliseconds = READ_LE_UINT16(command + 6);
+ if (command[2] && milliseconds) {
+ randomState = randomState * 1103515245 + 12345;
+ milliseconds = randomState % milliseconds;
+ }
+ voice.waitUntil = command[4] ? voice.startedAt + milliseconds : now + milliseconds;
+ voice.waitForSampleEnd = command[3] != 0;
+ voice.waiting = milliseconds != 0;
+ if (voice.root && nextCommandEnds(voice))
+ setGroupExpiry(voice.sound, voice.waitUntil);
+ if (voice.waiting)
+ return;
+ break;
+ }
+ case 8: {
+ const uint16 macro = READ_LE_UINT16(command + 2);
+ const uint16 step = READ_LE_UINT16(command + 4);
+ if (macro != voice.macro) {
+ startVoice(voice.sound, macro, step, command[6], command[7],
+ voice.volume, voice.pan, -1, false, now, index);
+ }
+ break;
+ }
+ case 0xc:
+ voice.adsr = READ_LE_UINT16(command + 1);
+ break;
+ case 0x10: {
+ vm->_mixer->stopHandle(voice.handle);
+ const uint32 rate = voice.rateOverride >= 0 ?
+ (uint32)voice.rateOverride : voice.rate;
+ if (rate != 0 || voice.rateOverride < 0) {
+ Audio::RewindableAudioStream *stream = bank.makeStream(
+ READ_LE_UINT16(command + 1), rate, voice.adsr);
+ if (stream) {
+ vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &voice.handle,
+ stream, -1,
+ voice.volume * Audio::Mixer::kMaxChannelVolume / 127,
+ soundBalance(voice.pan));
+ }
+ }
+ break;
+ }
+ case 0x1f:
+ voice.rate = (uint32)command[1] | ((uint32)command[2] << 8) |
+ ((uint32)command[3] << 16);
+ break;
+ default:
+ break;
+ }
+ }
+ finishVoice(index);
+ }
+
+ SoundId play(uint16 sfxId, int volume, int pan, int rate) {
+ uint16 macro;
+ byte priority;
+ byte maxVoices;
+ if (!bank.getSFX(sfxId, macro, priority, maxVoices))
+ return kInvalidSoundId;
+
+ SoundId sound = nextSound++;
+ if (sound == kInvalidSoundId)
+ sound = nextSound++;
+ const int group = allocateGroup(sound);
+ const uint32 now = g_system->getMillis();
+ const int voice = startVoice(sound, macro, 0, priority, maxVoices,
+ volume, pan, rate, true, now, -1);
+ if (voice < 0) {
+ groups[group] = Group();
+ return kInvalidSoundId;
+ }
+ return sound;
+ }
+
+ void update() {
+ const uint32 now = g_system->getMillis();
+ if (nextUpdate && !timeReached(now, nextUpdate))
+ return;
+ uint32 tickMillis = 1000 / kMacroTicksPerSecond;
+ tickRemainder += 1000 % kMacroTicksPerSecond;
+ if (tickRemainder >= kMacroTicksPerSecond) {
+ ++tickMillis;
+ tickRemainder -= kMacroTicksPerSecond;
+ }
+ nextUpdate = now + tickMillis;
+ ++tick;
+
+ for (int i = 0; i < kGroupCount; ++i) {
+ if (groups[i].active && groups[i].hasExpiry && timeReached(now, groups[i].expiry))
+ stopGroup(i);
+ }
+
+ for (int i = 0; i < kVoiceCount; ++i) {
+ Voice &voice = voices[i];
+ if (!voice.active || voice.readyTick > tick)
+ continue;
+ if (voice.macroDone) {
+ if (!vm->_mixer->isSoundHandleActive(voice.handle))
+ clearVoice(i);
+ continue;
+ }
+ if (voice.waiting) {
+ const bool sampleEnded = voice.waitForSampleEnd &&
+ !vm->_mixer->isSoundHandleActive(voice.handle);
+ if (!sampleEnded && !timeReached(now, voice.waitUntil))
+ continue;
+ voice.waiting = false;
+ }
+ runVoice(i, now);
+ }
+
+ for (int i = 0; i < kGroupCount; ++i) {
+ if (!groups[i].active || groups[i].hasExpiry)
+ continue;
+ bool hasVoice = false;
+ for (int j = 0; j < kVoiceCount; ++j)
+ hasVoice |= voices[j].active && voices[j].sound == groups[i].sound;
+ if (!hasVoice)
+ groups[i] = Group();
+ }
+ }
+
+ void setPan(SoundId sound, int pan) {
+ pan = CLIP(pan, 0, 127);
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (!voices[i].active || voices[i].sound != sound)
+ continue;
+ voices[i].pan = pan;
+ if (vm->_mixer->isSoundHandleActive(voices[i].handle))
+ vm->_mixer->setChannelBalance(voices[i].handle, soundBalance(pan));
+ }
+ }
+
+ void stop(SoundId sound) {
+ const int group = findGroup(sound);
+ if (group >= 0)
+ stopGroup(group);
+ }
+
+ void stopAll() {
+ for (int i = 0; i < kGroupCount; ++i) {
+ if (groups[i].active)
+ stopGroup(i);
+ }
+ }
+};
+
+RA2PSXSoundPlayer::RA2PSXSoundPlayer(ScummEngine_v7 *vm, const RA2PSXSoundBank &bank)
+ : _impl(new Impl(vm, bank)) {
+}
+
+RA2PSXSoundPlayer::~RA2PSXSoundPlayer() {
+ _impl->stopAll();
+ delete _impl;
+}
+
+RA2PSXSoundPlayer::SoundId RA2PSXSoundPlayer::play(uint16 sfx, int volume,
+ int pan, int rate) {
+ return _impl->play(sfx, volume, pan, rate);
+}
+
+void RA2PSXSoundPlayer::update() {
+ _impl->update();
+}
+
+void RA2PSXSoundPlayer::setPan(SoundId sound, int pan) {
+ _impl->setPan(sound, pan);
+}
+
+void RA2PSXSoundPlayer::stop(SoundId sound) {
+ _impl->stop(sound);
+}
+
+void RA2PSXSoundPlayer::stopAll() {
+ _impl->stopAll();
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index ff7fe44fda6..c98d58bc80b 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -39,6 +39,10 @@ namespace Scumm {
#ifdef USE_TINYGL
static const int kLevel1FrameRate = 30;
+static int getLevel1SoundPan(float screenX) {
+ return CLIP<int>((int)(screenX * 127.0f / 320.0f), 0, 127);
+}
+
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),
@@ -381,6 +385,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
RA2PSXLevel1Explosion explosions[3];
RA2PSXLevel1Shot shots[8];
RA2PSXLevel1Ship ship;
+ RA2PSXSoundPlayer soundPlayer(_vm, _soundBank);
+ RA2PSXSoundPlayer::SoundId approachSounds[3] = {};
int aimX = 160;
int aimY = 113;
int aimVelocityX = 0;
@@ -577,6 +583,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
if (result == kLevel1Error)
break;
+ soundPlayer.update();
const uint32 elapsed = g_system->getMillis() - gameplayStartTime;
const int targetLogicFrame = (int)((uint64)elapsed * kLevel1FrameRate / 1000);
@@ -606,12 +613,19 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
activeEnemies += enemies[i].active ? 1 : 0;
--spawnDelay;
if (videoFrame < 1599 && activeEnemies < 3 && spawnDelay <= 0) {
+ int spawnedEnemy = -1;
for (int i = 0; i < 3; ++i) {
if (!enemies[i].active) {
spawnLevel1Enemy(enemies[i], _vm->_rnd);
+ spawnedEnemy = i;
break;
}
}
+ if (spawnedEnemy >= 0) {
+ const uint16 sfx = _vm->_rnd.getRandomNumber(999) < 800 ? 0x19 : 0x1a;
+ const int rate = 0x1c18 + _vm->_rnd.getRandomNumber(1999);
+ approachSounds[spawnedEnemy] = soundPlayer.play(sfx, 0x5e, 0x40, rate);
+ }
spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
}
@@ -626,15 +640,26 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
--enemies[i].laserFrames;
++enemies[i].age;
updateLevel1Enemy(enemies[i]);
+ const int soundPan = getLevel1SoundPan(enemies[i].x);
+ soundPlayer.setPan(approachSounds[i], soundPan);
if (enemies[i].age == enemies[i].fireFrame) {
enemies[i].laserFrames = 4;
- if (_vm->_rnd.getRandomNumber(99) < 38)
+ soundPlayer.play(0x17, 0x4e, soundPan);
+ if (_vm->_rnd.getRandomNumber(99) < 38) {
shield = MAX(0, shield - (int)_vm->_rnd.getRandomNumberRng(6, 10));
+ soundPlayer.play(0x36, 0x7f, 0x40);
+ }
}
if (enemies[i].age >= enemies[i].lifetime) {
+ const int rate = _vm->_rnd.getRandomNumber(0x3fff);
+ soundPlayer.play(0x1b, 0x5a, soundPan, rate);
+ soundPlayer.stop(approachSounds[i]);
+ approachSounds[i] = RA2PSXSoundPlayer::kInvalidSoundId;
enemies[i].active = false;
- if (_vm->_rnd.getRandomNumber(99) < 18)
+ if (_vm->_rnd.getRandomNumber(99) < 18) {
shield = MAX(0, shield - 12);
+ soundPlayer.play(0x36, 0x7f, 0x40);
+ }
}
}
@@ -648,6 +673,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
if (!spawnLevel1Shot(shots, aimX, aimY,
thirdPersonView ? &ship : nullptr, shotTargetX, shotTargetY))
continue;
+ soundPlayer.play(0x18, 0x3f, 0x40);
lastShotFrame = logicFrame;
int hitEnemy = -1;
float hitDistance = 1000000.0f;
@@ -664,6 +690,11 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
}
if (hitEnemy >= 0) {
+ const int soundPan = getLevel1SoundPan(enemies[hitEnemy].x);
+ const int rate = _vm->_rnd.getRandomNumber(0x3fff);
+ soundPlayer.play(0x1b, 0x5a, soundPan, rate);
+ soundPlayer.stop(approachSounds[hitEnemy]);
+ approachSounds[hitEnemy] = RA2PSXSoundPlayer::kInvalidSoundId;
enemies[hitEnemy].active = false;
score = MIN(9999999, score + 100);
for (int i = 0; i < 3; ++i) {
@@ -700,11 +731,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
renderer.renderPerspectiveModel(shipModel, ship.x, ship.y, ship.z,
forwardX, forwardY, forwardZ, shipRoll, false);
} else {
- const float crosshairX = (aimX - 160) * 620.0f / 640.0f;
- const float crosshairY = (aimY - 120) * 620.0f / 640.0f;
renderer.renderModel(crosshair, aimX, aimY, 31.0f,
- crosshairY * 720.0f / 4096.0f,
- -crosshairX * 720.0f / 4096.0f, 0.0f, false);
+ 0.0f, 0.0f, 0.0f, false);
}
Graphics::Surface output;
renderer.finishFrame(output);
@@ -720,6 +748,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
g_system->delayMillis(5);
}
+ soundPlayer.stopAll();
decoder.close();
CursorMan.showMouse(cursorWasVisible);
return _vm->shouldQuit() ? kLevel1Quit : result;
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 6b37b003e46..058cedecbfe 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -116,6 +116,19 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
+ Common::SeekableReadStream *soundStream = openResource(0);
+ if (!soundStream)
+ return false;
+ RA2PSXArchive soundArchive;
+ const bool soundArchiveLoaded = soundArchive.load(*soundStream);
+ delete soundStream;
+ Common::Array<byte> soundData;
+ Common::Array<byte> soundProjectData;
+ if (!soundArchiveLoaded || !soundArchive.getMember("SNDsmp", soundData) ||
+ !soundArchive.getMember("sNDdata", soundProjectData) ||
+ !_soundBank.load(soundData, soundProjectData))
+ return false;
+
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 060a9a8d908..2dafcc98e50 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -29,6 +29,10 @@ namespace Common {
class SeekableReadStream;
}
+namespace Audio {
+class RewindableAudioStream;
+}
+
namespace Scumm {
class ScummEngine_v7;
@@ -57,6 +61,69 @@ private:
Common::Array<byte> _data;
};
+class RA2PSXSoundBank {
+public:
+ bool load(const Common::Array<byte> &sampleData, const Common::Array<byte> &projectData);
+ Audio::RewindableAudioStream *makeStream(uint16 id, uint32 rate,
+ uint16 adsrId = 0xffff) const;
+ bool getSFX(uint16 id, uint16 ¯o, byte &priority, byte &maxVoices) const;
+ bool getMacroCommand(uint16 macro, uint16 step, byte *command) const;
+
+private:
+ struct Sample {
+ uint32 offset;
+ uint16 id;
+ uint16 blocks;
+ uint16 rate;
+ };
+ struct SFX {
+ uint16 id;
+ uint16 macro;
+ byte priority;
+ byte maxVoices;
+ };
+ struct Macro {
+ uint32 id;
+ uint32 offset;
+ };
+ struct ADSR {
+ uint16 id;
+ uint16 attack;
+ uint16 decay;
+ uint16 sustain;
+ uint16 release;
+ };
+
+ const Sample *findSample(uint16 id) const;
+ const ADSR *findADSR(uint16 id) const;
+
+ Common::Array<byte> _data;
+ Common::Array<byte> _projectData;
+ Common::Array<Sample> _samples;
+ Common::Array<SFX> _sfx;
+ Common::Array<Macro> _macros;
+ Common::Array<ADSR> _adsrs;
+};
+
+class RA2PSXSoundPlayer {
+public:
+ typedef uint32 SoundId;
+ enum { kInvalidSoundId = 0 };
+
+ RA2PSXSoundPlayer(ScummEngine_v7 *vm, const RA2PSXSoundBank &bank);
+ ~RA2PSXSoundPlayer();
+
+ SoundId play(uint16 sfx, int volume, int pan, int rate = -1);
+ void update();
+ void setPan(SoundId sound, int pan);
+ void stop(SoundId sound);
+ void stopAll();
+
+private:
+ struct Impl;
+ Impl *_impl;
+};
+
struct RA2PSXVertex {
int16 x;
int16 y;
@@ -170,6 +237,7 @@ private:
const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
+ RA2PSXSoundBank _soundBank;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/resource.cpp b/engines/scumm/insane/rebel2/psx/resource.cpp
index 4c397da44ba..a9c07b1d96e 100644
--- a/engines/scumm/insane/rebel2/psx/resource.cpp
+++ b/engines/scumm/insane/rebel2/psx/resource.cpp
@@ -101,7 +101,9 @@ bool RA2PSXArchive::decompress(const byte *source, uint32 sourceSize, uint32 exp
bool RA2PSXArchive::unpack(const Entry &entry, Common::Array<byte> &data) const {
if (!entry.unpackedSize) {
- data.clear();
+ data.resize(entry.endOffset - entry.offset);
+ if (!data.empty())
+ memcpy(data.data(), _data.data() + entry.offset, data.size());
return true;
}
return decompress(_data.data() + entry.offset, entry.endOffset - entry.offset,
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index c91e53f3756..a4ca3f905c7 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -179,6 +179,7 @@ MODULE_OBJS += \
ifdef ENABLE_REBEL2_PSX
MODULE_OBJS += \
+ insane/rebel2/psx/audio.o \
insane/rebel2/psx/level1.o \
insane/rebel2/psx/model.o \
insane/rebel2/psx/psx.o \
Commit: 4b8f11a32ae830fb08a9e8939906933ce4f4c363
https://github.com/scummvm/scummvm/commit/4b8f11a32ae830fb08a9e8939906933ce4f4c363
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: added initial support for main meny in the psx release
Changed paths:
A engines/scumm/insane/rebel2/psx/menu.cpp
A engines/scumm/insane/rebel2/shared.h
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/psx/audio.cpp
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/metaengine.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index de13ab5a26d..51bbee2f9cd 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -37,6 +37,7 @@
#include "scumm/smush/rebel/smush_multi_font.h"
#include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel2/shared.h"
namespace Scumm {
@@ -120,41 +121,14 @@ int InsaneRebel2::processMenuInput() {
switch (event.type) {
case Common::EVENT_KEYDOWN:
_menuInactivityTimer = 0;
-
- switch (event.kbd.keycode) {
- case Common::KEYCODE_UP:
- _menuSelection--;
- if (_menuSelection < 0) {
- _menuSelection = _menuItemCount - 1;
- }
- _menuRepeatDelay = 3;
- debugC(DEBUG_INSANE, "Menu: Selection changed to %d (UP)", _menuSelection);
- break;
-
- case Common::KEYCODE_DOWN:
- _menuSelection++;
- if (_menuSelection >= _menuItemCount) {
- _menuSelection = 0;
- }
- _menuRepeatDelay = 3;
- debugC(DEBUG_INSANE, "Menu: Selection changed to %d (DOWN)", _menuSelection);
- break;
-
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
- result = _menuSelection;
- debugC(DEBUG_INSANE, "Menu: Item %d selected (ENTER)", _menuSelection);
- }
- break;
-
- case Common::KEYCODE_ESCAPE:
- result = _menuItemCount - 1;
- debugC(DEBUG_INSANE, "Menu: Back action - selecting quit (item %d)", result);
- break;
-
- default:
- break;
+ {
+ const Rebel2MenuCommand command = getRebel2MenuCommand(event.kbd);
+ const int oldSelection = _menuSelection;
+ result = applyRebel2MenuCommand(command, _menuItemCount, _menuSelection);
+ if (result == kRebel2MenuResultCancel)
+ result = _menuItemCount - 1;
+ if (_menuSelection != oldSelection)
+ _menuRepeatDelay = 3;
}
break;
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
index 17872212995..4f99c1904ea 100644
--- a/engines/scumm/insane/rebel2/psx/audio.cpp
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -32,8 +32,6 @@
namespace Scumm {
-namespace {
-
class RA2PSXADSRStream : public Audio::RewindableAudioStream {
public:
RA2PSXADSRStream(Audio::RewindableAudioStream *stream, uint16 attack,
@@ -97,8 +95,6 @@ static int soundBalance(int pan) {
return CLIP((pan - 64) * 2, -127, 127);
}
-} // End of anonymous namespace
-
bool RA2PSXSoundBank::load(const Common::Array<byte> &sampleData,
const Common::Array<byte> &projectData) {
Common::Array<Sample> samples;
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index c98d58bc80b..729dfd9d5b0 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -28,6 +28,7 @@
#include "graphics/surface.h"
#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/shared.h"
#include "scumm/insane/rebel2/psx/psx.h"
#include "scumm/insane/rebel2/psx/ui.h"
#include "scumm/insane/rebel2/psx/video.h"
@@ -400,7 +401,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
int nextSpawnAdjustment = 0;
int logicFrame = -1;
int videoFrame = -1;
- int lastShotFrame = -4;
+ int16 rapidFireCounter = 0;
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
@@ -415,10 +416,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
bool keyFire = false;
bool actionFire = false;
bool fireRequested = false;
+ bool fireWasPressed = false;
bool thirdPersonView = true;
Level1Result result = kLevel1Complete;
const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
+ const bool rapidFire = ConfMan.hasKey("rebel2_rapid_fire") ?
+ ConfMan.getBool("rebel2_rapid_fire") : true;
const bool cursorWasVisible = CursorMan.isVisible();
CursorMan.showMouse(false);
@@ -475,8 +479,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
moveDown = true;
} else if (event.kbd.keycode == Common::KEYCODE_SPACE ||
event.kbd.keycode == Common::KEYCODE_RETURN) {
+ if (!keyFire && !event.kbdRepeat)
+ fireRequested = true;
keyFire = true;
- fireRequested = true;
}
break;
case Common::EVENT_KEYUP:
@@ -664,17 +669,18 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
const bool heldFire = mouseFire || keyFire || actionFire;
- const bool shootRequested = fireRequested ||
- (heldFire && logicFrame - lastShotFrame >= 12);
+ const bool triggerShot = updateRebel2Fire(heldFire, fireWasPressed,
+ rapidFire, false, rapidFireCounter);
+ fireWasPressed = heldFire;
+ const bool shootRequested = fireRequested || triggerShot;
fireRequested = false;
- if (shootRequested && logicFrame - lastShotFrame >= 4) {
+ if (shootRequested) {
int shotTargetX;
int shotTargetY;
if (!spawnLevel1Shot(shots, aimX, aimY,
thirdPersonView ? &ship : nullptr, shotTargetX, shotTargetY))
continue;
soundPlayer.play(0x18, 0x3f, 0x40);
- lastShotFrame = logicFrame;
int hitEnemy = -1;
float hitDistance = 1000000.0f;
for (int i = 0; i < 3; ++i) {
diff --git a/engines/scumm/insane/rebel2/psx/menu.cpp b/engines/scumm/insane/rebel2/psx/menu.cpp
new file mode 100644
index 00000000000..0bbf2d2fbc4
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/menu.cpp
@@ -0,0 +1,155 @@
+/* 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/system.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/surface.h"
+
+#include "engines/dialogs.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/shared.h"
+#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/ui.h"
+
+namespace Scumm {
+
+static Rebel2MenuCommand getPSXMenuCommand(const Common::Event &event) {
+ if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat)
+ return getRebel2MenuCommand(event.kbd);
+ if (event.type != Common::EVENT_CUSTOM_ENGINE_ACTION_START)
+ return kRebel2MenuCommandNone;
+
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ return kRebel2MenuCommandUp;
+ case kScummActionInsaneDown:
+ return kRebel2MenuCommandDown;
+ case kScummActionInsaneAttack:
+ return kRebel2MenuCommandAccept;
+ case kScummActionInsaneBack:
+ case kScummActionInsaneSkip:
+ return kRebel2MenuCommandCancel;
+ default:
+ return kRebel2MenuCommandNone;
+ }
+}
+
+Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui) {
+ Graphics::Surface surface;
+ surface.create(_vm->_screenWidth, _vm->_screenHeight, g_system->getScreenFormat());
+
+ const bool cursorWasVisible = CursorMan.isVisible();
+ CursorMan.showMouse(true);
+ int selection = 0;
+ MenuResult result = kMenuQuit;
+ bool redraw = true;
+
+ while (!_vm->shouldQuit()) {
+ bool openGlobalMenu = false;
+ bool openOptions = false;
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _vm->quitGame();
+ break;
+ }
+
+ if (event.type == Common::EVENT_MAINMENU ||
+ (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat &&
+ event.kbd.keycode == Common::KEYCODE_ESCAPE)) {
+ openGlobalMenu = true;
+ continue;
+ }
+
+ if (event.type == Common::EVENT_MOUSEMOVE ||
+ event.type == Common::EVENT_LBUTTONDOWN) {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ for (int i = 0; i < 2; ++i) {
+ Common::Rect rect = ui.itemRect(i);
+ rect.translate(xOffset, yOffset);
+ if (!rect.contains(event.mouse.x, event.mouse.y))
+ continue;
+ if (selection != i) {
+ selection = i;
+ redraw = true;
+ }
+ if (event.type == Common::EVENT_LBUTTONDOWN) {
+ if (i == 0)
+ result = kMenuStart;
+ else
+ openOptions = true;
+ }
+ break;
+ }
+ }
+
+ const Rebel2MenuCommand command = getPSXMenuCommand(event);
+ const int oldSelection = selection;
+ const int commandResult = applyRebel2MenuCommand(command, 2, selection);
+ if (selection != oldSelection)
+ redraw = true;
+ if (commandResult == kRebel2MenuResultCancel)
+ openGlobalMenu = true;
+ else if (commandResult == 0)
+ result = kMenuStart;
+ else if (commandResult == 1)
+ openOptions = true;
+ }
+
+ if (_vm->shouldQuit())
+ break;
+ if (result == kMenuStart)
+ break;
+ if (openGlobalMenu) {
+ _vm->openMainMenuDialog();
+ redraw = true;
+ continue;
+ }
+ if (openOptions) {
+ GUI::ConfigDialog dialog;
+ dialog.runModal();
+ g_system->applyBackendSettings();
+ _vm->syncSoundSettings();
+ redraw = true;
+ continue;
+ }
+
+ if (redraw) {
+ surface.fillRect(Common::Rect(surface.w, surface.h), 0);
+ ui.draw(surface, selection);
+ g_system->copyRectToScreen(surface.getPixels(), surface.pitch,
+ 0, 0, surface.w, surface.h);
+ g_system->updateScreen();
+ redraw = false;
+ }
+ g_system->delayMillis(10);
+ }
+
+ surface.free();
+ CursorMan.showMouse(cursorWasVisible);
+ return _vm->shouldQuit() ? kMenuQuit : result;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 058cedecbfe..9a2b7b284d9 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -24,6 +24,7 @@
#include "common/translation.h"
#include "common/util.h"
+#include "graphics/cursorman.h"
#include "graphics/surface.h"
#include "scumm/scumm_v7.h"
@@ -77,6 +78,8 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
return false;
}
decoder.start();
+ const bool cursorWasVisible = CursorMan.isVisible();
+ CursorMan.showMouse(false);
g_system->fillScreen(0);
bool skipped = false;
@@ -111,24 +114,41 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
g_system->delayMillis(5);
}
decoder.close();
+ CursorMan.showMouse(cursorWasVisible);
return !_vm->shouldQuit();
}
-bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
- RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
- Common::SeekableReadStream *soundStream = openResource(0);
- if (!soundStream)
+bool Rebel2PSX::playIntroSequence() {
+ static const char *const videos[] = {
+ "LEC_LOGO.STR",
+ "F5_LOGO.STR",
+ "OPENING.STR"
+ };
+ for (uint i = 0; i < ARRAYSIZE(videos); ++i) {
+ if (!playVideo(videos[i], 1, false))
+ return false;
+ }
+ return true;
+}
+
+bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
+ Common::SeekableReadStream *stream = openResource(0);
+ if (!stream)
return false;
- RA2PSXArchive soundArchive;
- const bool soundArchiveLoaded = soundArchive.load(*soundStream);
- delete soundStream;
+
+ RA2PSXArchive archive;
+ const bool loaded = archive.load(*stream);
+ delete stream;
Common::Array<byte> soundData;
Common::Array<byte> soundProjectData;
- if (!soundArchiveLoaded || !soundArchive.getMember("SNDsmp", soundData) ||
- !soundArchive.getMember("sNDdata", soundProjectData) ||
- !_soundBank.load(soundData, soundProjectData))
- return false;
+ return loaded && menu.load(archive) &&
+ archive.getMember("SNDsmp", soundData) &&
+ archive.getMember("sNDdata", soundProjectData) &&
+ _soundBank.load(soundData, soundProjectData);
+}
+bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
+ RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -155,6 +175,21 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
+ RA2PSXMainMenuUI menu;
+ if (!loadGlobalAssets(menu))
+ return Common::Error(Common::kReadingFailed,
+ _("Could not load the PlayStation menu resources"));
+ if (!playIntroSequence()) {
+ if (_vm->shouldQuit())
+ return Common::kNoError;
+ return Common::Error(Common::kReadingFailed,
+ _("Could not play the PlayStation introduction"));
+ }
+
+ const MenuResult menuResult = runMainMenu(menu);
+ if (menuResult == kMenuQuit)
+ return Common::kNoError;
+
RA2PSXModel enemy;
RA2PSXModel ship;
RA2PSXModel crosshair;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 2dafcc98e50..2bae0b86328 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -37,6 +37,7 @@ namespace Scumm {
class ScummEngine_v7;
class RA2PSXLevel1UI;
+class RA2PSXMainMenuUI;
class RA2PSXArchive {
public:
@@ -220,6 +221,11 @@ public:
Common::Error runGame();
private:
+ enum MenuResult {
+ kMenuStart,
+ kMenuQuit
+ };
+
enum Level1Result {
kLevel1Quit,
kLevel1Complete,
@@ -230,6 +236,9 @@ private:
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 playIntroSequence();
+ MenuResult runMainMenu(const RA2PSXMainMenuUI &ui);
+ bool loadGlobalAssets(RA2PSXMainMenuUI &menu);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui);
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index cb8066f8b0a..f3a2e16238a 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -47,11 +47,11 @@ static RA2PSXUIColor shieldColor(const RA2PSXUIGradientStop *stops, uint count,
return stops[count - 1].color;
}
-bool RA2PSXLevel1UI::loadTextures(const Common::Array<byte> &data) {
+bool RA2PSXTextureSet::append(const Common::Array<byte> &data) {
return loadRA2PSXTextures(data, _textures);
}
-const RA2PSXTexture *RA2PSXLevel1UI::findTexture(const char *name) const {
+const RA2PSXTexture *RA2PSXTextureSet::find(const char *name) const {
for (uint i = 0; i < _textures.size(); ++i) {
if (_textures[i].name.equalsIgnoreCase(name))
return &_textures[i];
@@ -59,28 +59,9 @@ const RA2PSXTexture *RA2PSXLevel1UI::findTexture(const char *name) const {
return nullptr;
}
-bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
- Common::Array<byte> data;
- _textures.clear();
- if (!archive.getMember("tex/Common", data) || !loadTextures(data))
- return false;
- if (!archive.getMember("tex/BWingCockp", data) || !loadTextures(data))
- return false;
-
- static const char *const required[] = {
- "COCKPITL", "COCKPITR", "PANEL1", "PANEL2", "PANEL3", "STATTEXT",
- "FONT8X9", "REBLSIGN", "ENRGYMSK", "SMALLEX"
- };
- for (uint i = 0; i < ARRAYSIZE(required); ++i) {
- if (!findTexture(required[i]))
- return false;
- }
- return true;
-}
-
-void RA2PSXLevel1UI::drawTexture(Graphics::Surface &surface, const char *name,
+void RA2PSXTextureSet::draw(Graphics::Surface &surface, const char *name,
int x, int y, const Common::Rect &source, int brightness, BlendMode blend) const {
- const RA2PSXTexture *texture = findTexture(name);
+ const RA2PSXTexture *texture = find(name);
if (!texture)
return;
@@ -130,17 +111,99 @@ void RA2PSXLevel1UI::drawTexture(Graphics::Surface &surface, const char *name,
}
}
+void RA2PSXTextureSet::drawText(Graphics::Surface &surface, const char *font,
+ const char *text, int x, int y) const {
+ static const char glyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.,+/C ";
+ static const byte widths[] = {
+ 6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 8, 6, 7, 6, 6, 4, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 2, 2, 2, 6, 6, 8, 2
+ };
+ static_assert(ARRAYSIZE(glyphs) == ARRAYSIZE(widths) + 1,
+ "RA2 PSX glyph widths do not match the font map");
+
+ for (; *text; ++text) {
+ int glyph = -1;
+ for (uint i = 0; i < ARRAYSIZE(widths); ++i) {
+ if (*text == glyphs[i]) {
+ glyph = i;
+ break;
+ }
+ }
+ if (glyph < 0)
+ continue;
+
+ const int sourceX = (glyph % 12) * 8;
+ const int sourceY = (glyph / 12) * 8;
+ draw(surface, font, x, y, Common::Rect(sourceX, sourceY,
+ sourceX + widths[glyph], sourceY + 8));
+ x += widths[glyph] + 2;
+ }
+}
+
+bool RA2PSXMainMenuUI::load(const RA2PSXArchive &archive) {
+ Common::Array<byte> data;
+ _textures.clear();
+ return archive.getMember("menuTex", data) && _textures.append(data) &&
+ _textures.has("BACK_L") && _textures.has("BACK_R") &&
+ _textures.has("TITLE") && _textures.has("STD_FT2") &&
+ _textures.has("STD_FT4") && _textures.has("STD_FT6");
+}
+
+void RA2PSXMainMenuUI::draw(Graphics::Surface &surface, int selection) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ _textures.draw(surface, "BACK_L", xOffset, yOffset, Common::Rect(0, 0, 224, 240));
+ _textures.draw(surface, "BACK_R", xOffset + 224, yOffset, Common::Rect(0, 0, 96, 240));
+ _textures.draw(surface, "TITLE", xOffset + 72, yOffset + 22, Common::Rect(0, 0, 176, 124));
+
+ static const char *const items[] = { "start", "options" };
+ static const int itemX[] = { 141, 134 };
+ for (uint i = 0; i < ARRAYSIZE(items); ++i) {
+ _textures.drawText(surface, i == (uint)selection ? "STD_FT4" : "STD_FT6",
+ items[i], xOffset + itemX[i], yOffset + 166 + i * 10);
+ }
+ _textures.drawText(surface, "STD_FT2", "developed by factor 5",
+ xOffset + 80, yOffset + 202);
+ _textures.drawText(surface, "STD_FT2", "C 1996 lucasarts entertainment company",
+ xOffset + 16, yOffset + 212);
+}
+
+Common::Rect RA2PSXMainMenuUI::itemRect(int item) const {
+ return Common::Rect(120, 164 + item * 10, 200, 174 + item * 10);
+}
+
+bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
+ Common::Array<byte> data;
+ _textures.clear();
+ if (!archive.getMember("tex/Common", data) || !_textures.append(data))
+ return false;
+ if (!archive.getMember("tex/BWingCockp", data) || !_textures.append(data))
+ return false;
+
+ static const char *const required[] = {
+ "COCKPITL", "COCKPITR", "PANEL1", "PANEL2", "PANEL3", "STATTEXT",
+ "FONT8X9", "REBLSIGN", "ENRGYMSK", "SMALLEX"
+ };
+ for (uint i = 0; i < ARRAYSIZE(required); ++i) {
+ if (!_textures.has(required[i]))
+ return false;
+ }
+ return true;
+}
+
void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface) const {
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2 + 120;
- drawTexture(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
- drawTexture(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
+ _textures.draw(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
+ _textures.draw(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
}
void RA2PSXLevel1UI::drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const {
const int stage = CLIP<int>(frame / 2, 0, 4);
- drawTexture(surface, "SMALLEX", x - 8, y - 8,
- Common::Rect(stage * 16, 0, stage * 16 + 16, 16), 0x80, kBlendAdditive);
+ _textures.draw(surface, "SMALLEX", x - 8, y - 8,
+ Common::Rect(stage * 16, 0, stage * 16 + 16, 16), 0x80,
+ RA2PSXTextureSet::kBlendAdditive);
}
void RA2PSXLevel1UI::drawShield(Graphics::Surface &surface, int shield,
@@ -185,8 +248,9 @@ void RA2PSXLevel1UI::drawShield(Graphics::Surface &surface, int shield,
const int sourceLeft = 77 - width - row;
if (sourceLeft < 68)
- drawTexture(surface, "ENRGYMSK", left, y,
- Common::Rect(MAX(0, sourceLeft), row, 68, row + 1), 0x50, kBlendAdditive);
+ _textures.draw(surface, "ENRGYMSK", left, y,
+ Common::Rect(MAX(0, sourceLeft), row, 68, row + 1), 0x50,
+ RA2PSXTextureSet::kBlendAdditive);
}
}
@@ -195,37 +259,37 @@ void RA2PSXLevel1UI::drawHUD(Graphics::Surface &surface, int score, int lives,
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2;
- drawTexture(surface, "PANEL3", xOffset + 2, yOffset + 17, Common::Rect(0, 0, 126, 21));
- drawTexture(surface, "PANEL2", xOffset + 262, yOffset + 17, Common::Rect(0, 0, 56, 29));
- drawTexture(surface, "PANEL1", xOffset + 210, yOffset + 207, Common::Rect(0, 0, 108, 21));
+ _textures.draw(surface, "PANEL3", xOffset + 2, yOffset + 17, Common::Rect(0, 0, 126, 21));
+ _textures.draw(surface, "PANEL2", xOffset + 262, yOffset + 17, Common::Rect(0, 0, 56, 29));
+ _textures.draw(surface, "PANEL1", xOffset + 210, yOffset + 207, Common::Rect(0, 0, 108, 21));
drawShield(surface, shield, xOffset, yOffset);
- drawTexture(surface, "STATTEXT", xOffset + 14, yOffset + 21,
+ _textures.draw(surface, "STATTEXT", xOffset + 14, yOffset + 21,
Common::Rect(42, 0, 62, 13), 0x5a);
score = CLIP<int>(score, 0, 9999999);
int divisor = 1000000;
for (int digit = 0; digit < 7; ++digit) {
const int value = score / divisor % 10;
- drawTexture(surface, "FONT8X9", xOffset + 39 + digit * 10, yOffset + 23,
+ _textures.draw(surface, "FONT8X9", xOffset + 39 + digit * 10, yOffset + 23,
Common::Rect(value * 8, 0, value * 8 + 8, 9));
divisor /= 10;
}
const int rebelFrame = (MAX(frame, 0) / 2) % 12;
- drawTexture(surface, "REBLSIGN", xOffset + 292, yOffset + 22,
+ _textures.draw(surface, "REBLSIGN", xOffset + 292, yOffset + 22,
Common::Rect((rebelFrame % 6) * 20, (rebelFrame / 6) * 19,
(rebelFrame % 6 + 1) * 20, (rebelFrame / 6 + 1) * 19));
const int reserveLives = CLIP<int>(lives - 1, 0, 9);
- drawTexture(surface, "FONT8X9", xOffset + 274, yOffset + 28,
+ _textures.draw(surface, "FONT8X9", xOffset + 274, yOffset + 28,
Common::Rect(reserveLives * 8, 0, reserveLives * 8 + 8, 9));
- drawTexture(surface, "FONT8X9", xOffset + 283, yOffset + 29, Common::Rect(80, 0, 88, 9));
+ _textures.draw(surface, "FONT8X9", xOffset + 283, yOffset + 29, Common::Rect(80, 0, 88, 9));
int shieldLabelBrightness = 0x5a;
if (shield <= 31) {
const int phase = (MAX(frame, 0) / 2) % 14;
shieldLabelBrightness += phase < 7 ? -50 + phase * 10 : 20 - (phase - 7) * 10;
}
- drawTexture(surface, "STATTEXT", xOffset + 291, yOffset + 211,
+ _textures.draw(surface, "STATTEXT", xOffset + 291, yOffset + 211,
Common::Rect(0, 0, 20, 13), shieldLabelBrightness);
}
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 6665d5875cd..2e9ce8e6a1d 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -21,6 +21,37 @@
namespace Scumm {
+class RA2PSXTextureSet {
+public:
+ enum BlendMode {
+ kBlendOpaque,
+ kBlendAdditive
+ };
+
+ void clear() { _textures.clear(); }
+ bool append(const Common::Array<byte> &data);
+ bool has(const char *name) const { return find(name) != nullptr; }
+ void draw(Graphics::Surface &surface, const char *name, int x, int y,
+ const Common::Rect &source, int brightness = 0x80,
+ BlendMode blend = kBlendOpaque) const;
+ void drawText(Graphics::Surface &surface, const char *font, const char *text,
+ int x, int y) const;
+
+private:
+ const RA2PSXTexture *find(const char *name) const;
+ Common::Array<RA2PSXTexture> _textures;
+};
+
+class RA2PSXMainMenuUI {
+public:
+ bool load(const RA2PSXArchive &archive);
+ void draw(Graphics::Surface &surface, int selection) const;
+ Common::Rect itemRect(int item) const;
+
+private:
+ RA2PSXTextureSet _textures;
+};
+
class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
@@ -30,19 +61,9 @@ public:
void drawHUD(Graphics::Surface &surface, int score, int lives, int shield, int frame) const;
private:
- enum BlendMode {
- kBlendOpaque,
- kBlendAdditive
- };
-
- bool loadTextures(const Common::Array<byte> &data);
- const RA2PSXTexture *findTexture(const char *name) const;
- void drawTexture(Graphics::Surface &surface, const char *name,
- int x, int y, const Common::Rect &source, int brightness = 0x80,
- BlendMode blend = kBlendOpaque) const;
void drawShield(Graphics::Surface &surface, int shield, int xOffset, int yOffset) const;
- Common::Array<RA2PSXTexture> _textures;
+ RA2PSXTextureSet _textures;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 79315879380..882da9df8fc 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -44,6 +44,7 @@
#include "scumm/smush/rebel/font_rebel2.h"
#include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel2/shared.h"
#include "common/config-manager.h"
#include "audio/audiostream.h"
@@ -1516,7 +1517,7 @@ int32 InsaneRebel2::processMouse() {
bool leftPressed = (currentButtons & 1) != 0;
bool leftWasPressed = (_prevMouseButtons & 1) != 0;
- bool leftEdge = leftPressed && !leftWasPressed;
+ const bool leftEdge = leftPressed && !leftWasPressed;
bool rightPressed = (currentButtons & 2) != 0;
bool rightWasPressed = (_prevMouseButtons & 2) != 0;
@@ -1548,16 +1549,8 @@ int32 InsaneRebel2::processMouse() {
if (autoFire)
_rebelControlMode |= 1;
- // Rapid fire injects a held-button shot every 5th frame from the press.
- if (leftEdge)
- _rapidFireCounter = 0;
- bool rapidFireShot = false;
- if (!_rebelAutoPlay && _optRapidFire) {
- rapidFireShot = (_rapidFireCounter % 5 == 0) && leftPressed;
- _rapidFireCounter++;
- }
-
- bool triggerShot = leftEdge || rapidFireShot || autoFire;
+ const bool triggerShot = updateRebel2Fire(leftPressed, leftWasPressed,
+ !_rebelAutoPlay && _optRapidFire, autoFire, _rapidFireCounter);
if (_rebelHandler == 8) {
_shipFiring = triggerShot && canShoot;
}
diff --git a/engines/scumm/insane/rebel2/shared.h b/engines/scumm/insane/rebel2/shared.h
new file mode 100644
index 00000000000..3476a8c40de
--- /dev/null
+++ b/engines/scumm/insane/rebel2/shared.h
@@ -0,0 +1,89 @@
+/* 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.
+ */
+
+#ifndef SCUMM_INSANE_REBEL2_SHARED_H
+#define SCUMM_INSANE_REBEL2_SHARED_H
+
+#include "common/keyboard.h"
+
+namespace Scumm {
+
+enum Rebel2MenuCommand {
+ kRebel2MenuCommandNone,
+ kRebel2MenuCommandUp,
+ kRebel2MenuCommandDown,
+ kRebel2MenuCommandAccept,
+ kRebel2MenuCommandCancel
+};
+
+enum {
+ kRebel2MenuResultCancel = -2,
+ kRebel2MenuResultNone = -1
+};
+
+inline Rebel2MenuCommand getRebel2MenuCommand(const Common::KeyState &key) {
+ switch (key.keycode) {
+ case Common::KEYCODE_UP:
+ return kRebel2MenuCommandUp;
+ case Common::KEYCODE_DOWN:
+ return kRebel2MenuCommandDown;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ return kRebel2MenuCommandAccept;
+ case Common::KEYCODE_ESCAPE:
+ return kRebel2MenuCommandCancel;
+ default:
+ return kRebel2MenuCommandNone;
+ }
+}
+
+inline int applyRebel2MenuCommand(Rebel2MenuCommand command, int itemCount,
+ int &selection) {
+ if (itemCount <= 0)
+ return kRebel2MenuResultNone;
+
+ switch (command) {
+ case kRebel2MenuCommandUp:
+ if (--selection < 0)
+ selection = itemCount - 1;
+ break;
+ case kRebel2MenuCommandDown:
+ if (++selection >= itemCount)
+ selection = 0;
+ break;
+ case kRebel2MenuCommandAccept:
+ return selection >= 0 && selection < itemCount ? selection : kRebel2MenuResultNone;
+ case kRebel2MenuCommandCancel:
+ return kRebel2MenuResultCancel;
+ default:
+ break;
+ }
+ return kRebel2MenuResultNone;
+}
+
+inline bool updateRebel2Fire(bool pressed, bool wasPressed,
+ bool rapidFire, bool autoFire, int16 &rapidFireCounter) {
+ const bool pressedEdge = pressed && !wasPressed;
+ if (pressedEdge)
+ rapidFireCounter = 0;
+
+ bool rapidFireShot = false;
+ if (rapidFire) {
+ rapidFireShot = pressed && rapidFireCounter % 5 == 0;
+ rapidFireCounter = (rapidFireCounter + 1) % 5;
+ }
+ return pressedEdge || rapidFireShot || autoFire;
+}
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index b191cbe5805..d69986ea5e0 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -238,6 +238,11 @@ bool ScummMetaEngine::hasFeature(MetaEngineFeature f) const {
}
bool ScummEngine::hasFeature(EngineFeature f) const {
+#ifdef ENABLE_REBEL2_PSX
+ if (_game.id == GID_REBEL2 && _game.platform == Common::kPlatformPSX &&
+ (f == kSupportsLoadingDuringRuntime || f == kSupportsSavingDuringRuntime))
+ return false;
+#endif
return
(f == kSupportsReturnToLauncher) ||
(f == kSupportsLoadingDuringRuntime) ||
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index a4ca3f905c7..93edc2ba909 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -181,6 +181,7 @@ ifdef ENABLE_REBEL2_PSX
MODULE_OBJS += \
insane/rebel2/psx/audio.o \
insane/rebel2/psx/level1.o \
+ insane/rebel2/psx/menu.o \
insane/rebel2/psx/model.o \
insane/rebel2/psx/psx.o \
insane/rebel2/psx/resource.o \
Commit: a21f47775965b07f4abfdd448f2ba903992aff73
https://github.com/scummvm/scummvm/commit/a21f47775965b07f4abfdd448f2ba903992aff73
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
PLAYGROUND3D: Added a quad strip test
Changed paths:
engines/playground3d/gfx.h
engines/playground3d/gfx_opengl.cpp
engines/playground3d/gfx_opengl.h
engines/playground3d/gfx_opengl_shaders.cpp
engines/playground3d/gfx_opengl_shaders.h
engines/playground3d/gfx_tinygl.cpp
engines/playground3d/gfx_tinygl.h
engines/playground3d/playground3d.cpp
diff --git a/engines/playground3d/gfx.h b/engines/playground3d/gfx.h
index 606b68c96aa..f518836057d 100644
--- a/engines/playground3d/gfx.h
+++ b/engines/playground3d/gfx.h
@@ -76,6 +76,7 @@ public:
virtual void loadTextureRGBA4444(Graphics::Surface *texture) = 0;
virtual void drawCube(const Math::Vector3d &pos, const Math::Vector3d &roll) = 0;
virtual void drawPolyOffsetTest(const Math::Vector3d &pos, const Math::Vector3d &roll) = 0;
+ virtual void drawQuadStripTest() = 0;
virtual void dimRegionInOut(float fade) = 0;
virtual void drawInViewport() = 0;
virtual void drawRgbaTexture() = 0;
diff --git a/engines/playground3d/gfx_opengl.cpp b/engines/playground3d/gfx_opengl.cpp
index b84e2f61da7..60b4dffa96e 100644
--- a/engines/playground3d/gfx_opengl.cpp
+++ b/engines/playground3d/gfx_opengl.cpp
@@ -230,6 +230,46 @@ void OpenGLRenderer::drawPolyOffsetTest(const Math::Vector3d &pos, const Math::V
glDisable(GL_POLYGON_OFFSET_FILL);
}
+void OpenGLRenderer::drawQuadStripTest() {
+ glMatrixMode(GL_PROJECTION);
+ glPushMatrix();
+ glLoadIdentity();
+
+ glMatrixMode(GL_MODELVIEW);
+ glPushMatrix();
+ glLoadIdentity();
+
+ glDisable(GL_BLEND);
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+ glDepthMask(GL_FALSE);
+ glDisable(GL_TEXTURE_2D);
+
+ glColor4f(0.0f, 0.75f, 0.2f, 1.0f);
+ glBegin(GL_QUAD_STRIP);
+ glVertex3f(-0.8f, 0.7f, 0.0f);
+ glVertex3f( 0.8f, 0.7f, 0.0f);
+ glVertex3f(-0.8f, -0.7f, 0.0f);
+ glVertex3f( 0.8f, -0.7f, 0.0f);
+ glEnd();
+
+ glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
+ glBegin(GL_TRIANGLES);
+ glVertex3f(-0.12f, 0.12f, 0.0f);
+ glVertex3f( 0.12f, 0.12f, 0.0f);
+ glVertex3f( 0.0f, -0.12f, 0.0f);
+ glEnd();
+
+ glDepthMask(GL_TRUE);
+ glEnable(GL_DEPTH_TEST);
+
+ glMatrixMode(GL_MODELVIEW);
+ glPopMatrix();
+
+ glMatrixMode(GL_PROJECTION);
+ glPopMatrix();
+}
+
void OpenGLRenderer::dimRegionInOut(float fade) {
glMatrixMode(GL_PROJECTION);
glPushMatrix();
diff --git a/engines/playground3d/gfx_opengl.h b/engines/playground3d/gfx_opengl.h
index 97328af21b1..9b35c203ccd 100644
--- a/engines/playground3d/gfx_opengl.h
+++ b/engines/playground3d/gfx_opengl.h
@@ -55,6 +55,7 @@ public:
void setupViewport(int x, int y, int width, int height) override;
void drawCube(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
void drawPolyOffsetTest(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
+ void drawQuadStripTest() override;
void dimRegionInOut(float fade) override;
void drawInViewport() override;
void drawRgbaTexture() override;
diff --git a/engines/playground3d/gfx_opengl_shaders.cpp b/engines/playground3d/gfx_opengl_shaders.cpp
index 79cc37cc9cf..0fa196f0160 100644
--- a/engines/playground3d/gfx_opengl_shaders.cpp
+++ b/engines/playground3d/gfx_opengl_shaders.cpp
@@ -71,6 +71,14 @@ static const GLfloat boxVertices[] = {
0.1f, 0.1f,
-0.1f, -0.1f,
0.1f, -0.1f,
+ // quad strip test
+ -0.8f, 0.7f,
+ 0.8f, 0.7f,
+ -0.8f, -0.7f,
+ 0.8f, -0.7f,
+ -0.12f, 0.12f,
+ 0.12f, 0.12f,
+ 0.0f, -0.12f,
};
static const GLfloat bitmapVertices[] = {
@@ -283,6 +291,24 @@ void ShaderRenderer::drawPolyOffsetTest(const Math::Vector3d &pos, const Math::V
glDisable(GL_POLYGON_OFFSET_FILL);
}
+void ShaderRenderer::drawQuadStripTest() {
+ glDisable(GL_BLEND);
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+ glDepthMask(GL_FALSE);
+
+ _viewportShader->use();
+ _viewportShader->setUniform("offset", Math::Vector2d(0.0f, 0.0f));
+ _viewportShader->setUniform("color", Math::Vector3d(0.0f, 0.75f, 0.2f));
+ glDrawArrays(GL_TRIANGLE_STRIP, 8, 4);
+ _viewportShader->setUniform("color", Math::Vector3d(1.0f, 0.0f, 0.0f));
+ glDrawArrays(GL_TRIANGLES, 12, 3);
+ _viewportShader->unbind();
+
+ glDepthMask(GL_TRUE);
+ glEnable(GL_DEPTH_TEST);
+}
+
void ShaderRenderer::dimRegionInOut(float fade) {
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
diff --git a/engines/playground3d/gfx_opengl_shaders.h b/engines/playground3d/gfx_opengl_shaders.h
index e6a606206e6..dd877d8a7bb 100644
--- a/engines/playground3d/gfx_opengl_shaders.h
+++ b/engines/playground3d/gfx_opengl_shaders.h
@@ -55,6 +55,7 @@ public:
void setupViewport(int x, int y, int width, int height) override;
void drawCube(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
void drawPolyOffsetTest(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
+ void drawQuadStripTest() override;
void dimRegionInOut(float fade) override;
void drawInViewport() override;
void drawRgbaTexture() override;
diff --git a/engines/playground3d/gfx_tinygl.cpp b/engines/playground3d/gfx_tinygl.cpp
index 1e5f7f997d9..f15e8fef910 100644
--- a/engines/playground3d/gfx_tinygl.cpp
+++ b/engines/playground3d/gfx_tinygl.cpp
@@ -266,6 +266,46 @@ void TinyGLRenderer::drawPolyOffsetTest(const Math::Vector3d &pos, const Math::V
tglDisable(TGL_POLYGON_OFFSET_FILL);
}
+void TinyGLRenderer::drawQuadStripTest() {
+ tglMatrixMode(TGL_PROJECTION);
+ tglPushMatrix();
+ tglLoadIdentity();
+
+ tglMatrixMode(TGL_MODELVIEW);
+ tglPushMatrix();
+ tglLoadIdentity();
+
+ tglDisable(TGL_BLEND);
+ tglDisable(TGL_CULL_FACE);
+ tglDisable(TGL_DEPTH_TEST);
+ tglDepthMask(TGL_FALSE);
+ tglDisable(TGL_TEXTURE_2D);
+
+ tglColor4f(0.0f, 0.75f, 0.2f, 1.0f);
+ tglBegin(TGL_QUAD_STRIP);
+ tglVertex3f(-0.8f, 0.7f, 0.0f);
+ tglVertex3f( 0.8f, 0.7f, 0.0f);
+ tglVertex3f(-0.8f, -0.7f, 0.0f);
+ tglVertex3f( 0.8f, -0.7f, 0.0f);
+ tglEnd();
+
+ tglColor4f(1.0f, 0.0f, 0.0f, 1.0f);
+ tglBegin(TGL_TRIANGLES);
+ tglVertex3f(-0.12f, 0.12f, 0.0f);
+ tglVertex3f( 0.12f, 0.12f, 0.0f);
+ tglVertex3f( 0.0f, -0.12f, 0.0f);
+ tglEnd();
+
+ tglDepthMask(TGL_TRUE);
+ tglEnable(TGL_DEPTH_TEST);
+
+ tglMatrixMode(TGL_MODELVIEW);
+ tglPopMatrix();
+
+ tglMatrixMode(TGL_PROJECTION);
+ tglPopMatrix();
+}
+
void TinyGLRenderer::flipBuffer() {
Common::List<Common::Rect> dirtyAreas;
TinyGL::presentBuffer(dirtyAreas);
diff --git a/engines/playground3d/gfx_tinygl.h b/engines/playground3d/gfx_tinygl.h
index 489c5ca6e90..406317b1ea9 100644
--- a/engines/playground3d/gfx_tinygl.h
+++ b/engines/playground3d/gfx_tinygl.h
@@ -50,6 +50,7 @@ public:
void setupViewport(int x, int y, int width, int height) override;
void drawCube(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
void drawPolyOffsetTest(const Math::Vector3d &pos, const Math::Vector3d &roll) override;
+ void drawQuadStripTest() override;
void dimRegionInOut(float fade) override;
void drawInViewport() override;
void drawRgbaTexture() override;
diff --git a/engines/playground3d/playground3d.cpp b/engines/playground3d/playground3d.cpp
index e18a30a611e..b8db73c2844 100644
--- a/engines/playground3d/playground3d.cpp
+++ b/engines/playground3d/playground3d.cpp
@@ -109,6 +109,7 @@ Common::Error Playground3dEngine::run() {
// 3 - fade in/out
// 4 - moving filled rectangle in viewport
// 5 - drawing RGBA pattern texture to check endian correctness
+ // 6 - quad strip followed by a triangle
_testId = 1;
_fogEnable = false;
_scissorEnable = false;
@@ -138,6 +139,9 @@ Common::Error Playground3dEngine::run() {
}
break;
}
+ case 6:
+ _clearColor = Math::Vector4d(0.25f, 0.25f, 0.25f, 1.0f);
+ break;
default:
assert(false);
}
@@ -167,7 +171,7 @@ void Playground3dEngine::processInput() {
switch (event.customType) {
case kActionSwitchTest:
_testId++;
- if (_testId > 5)
+ if (_testId > 6)
_testId = 1;
switch (_testId) {
case 1:
@@ -190,6 +194,9 @@ void Playground3dEngine::processInput() {
}
break;
}
+ case 6:
+ _clearColor = Math::Vector4d(0.25f, 0.25f, 0.25f, 1.0f);
+ break;
default:
assert(false);
}
@@ -313,6 +320,9 @@ void Playground3dEngine::drawFrame() {
_gfx->loadTextureRGBA4444(_rgba4444Texture);
drawRgbaTexture();
break;
+ case 6:
+ _gfx->drawQuadStripTest();
+ break;
default:
assert(false);
}
Commit: c81277189f014757d9e1ab4888d8227a7e6482d8
https://github.com/scummvm/scummvm/commit/c81277189f014757d9e1ab4888d8227a7e6482d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
TINYGL: Fix deferred quad strip rendering
Changed paths:
graphics/tinygl/zdirtyrect.cpp
diff --git a/graphics/tinygl/zdirtyrect.cpp b/graphics/tinygl/zdirtyrect.cpp
index 6af86ae35a6..b8016326205 100644
--- a/graphics/tinygl/zdirtyrect.cpp
+++ b/graphics/tinygl/zdirtyrect.cpp
@@ -362,7 +362,6 @@ void RasterizationDrawCall::execute(bool restoreState, const Common::Rect *clipp
c->draw_triangle_front = (gl_draw_triangle_func)_drawTriangleFront;
c->draw_triangle_back = (gl_draw_triangle_func)_drawTriangleBack;
- int n = c->vertex_n;
int cnt = c->vertex_cnt;
switch (c->begin_type) {
@@ -419,12 +418,9 @@ void RasterizationDrawCall::execute(bool restoreState, const Common::Rect *clipp
}
break;
case TGL_QUAD_STRIP:
- for( ; n >= 4; n -= 2) {
- c->gl_draw_triangle(&c->vertex[0], &c->vertex[1], &c->vertex[2]);
- c->gl_draw_triangle(&c->vertex[1], &c->vertex[3], &c->vertex[2]);
- for (int i = 0; i < 2; i++) {
- c->vertex[i] = c->vertex[i + 2];
- }
+ for (int i = 0; i + 3 < cnt; i += 2) {
+ c->gl_draw_triangle(&c->vertex[i], &c->vertex[i + 1], &c->vertex[i + 2]);
+ c->gl_draw_triangle(&c->vertex[i + 1], &c->vertex[i + 3], &c->vertex[i + 2]);
}
break;
case TGL_POLYGON: {
Commit: 91bd24ea249f225944b9facc108569449d8e2267
https://github.com/scummvm/scummvm/commit/91bd24ea249f225944b9facc108569449d8e2267
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: font rendering support for psx release
Changed paths:
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 9a2b7b284d9..82465dbe4b0 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -65,7 +65,8 @@ Common::SeekableReadStream *Rebel2PSX::openRawFile(const Common::Path &path, int
return file;
}
-bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version2) {
+bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version2,
+ const RA2PSXMovieText *movieText, RA2PSXMovieTextSequence textSequence) {
Common::SeekableReadStream *stream = openRawFile(path, discNumber);
if (!stream)
return false;
@@ -95,6 +96,13 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
const int destY = (_vm->_screenHeight - height) / 2;
g_system->copyRectToScreen(frame->getBasePtr(sourceX, sourceY), frame->pitch,
destX, destY, width, height);
+ if (movieText && textSequence != kRA2PSXMovieTextNone) {
+ Graphics::Surface *screen = g_system->lockScreen();
+ // The original callback advances the STR's one-based frame ID.
+ movieText->draw(*screen, textSequence, decoder.getCurFrame() + 2,
+ (_vm->_screenWidth - 320) / 2, (_vm->_screenHeight - 240) / 2);
+ g_system->unlockScreen();
+ }
g_system->updateScreen();
}
}
@@ -118,14 +126,16 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
return !_vm->shouldQuit();
}
-bool Rebel2PSX::playIntroSequence() {
+bool Rebel2PSX::playIntroSequence(const RA2PSXMovieText &movieText) {
static const char *const videos[] = {
"LEC_LOGO.STR",
"F5_LOGO.STR",
"OPENING.STR"
};
for (uint i = 0; i < ARRAYSIZE(videos); ++i) {
- if (!playVideo(videos[i], 1, false))
+ const RA2PSXMovieTextSequence textSequence = i == 2 ?
+ kRA2PSXMovieTextOpening : kRA2PSXMovieTextNone;
+ if (!playVideo(videos[i], 1, false, &movieText, textSequence))
return false;
}
return true;
@@ -147,6 +157,11 @@ bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
_soundBank.load(soundData, soundProjectData);
}
+bool Rebel2PSX::loadMovieTextAssets(RA2PSXMovieText &movieText) {
+ Common::File executable;
+ return executable.open("SLUS_003.81") && movieText.load(executable);
+}
+
bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
@@ -176,10 +191,14 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
RA2PSXMainMenuUI menu;
+ RA2PSXMovieText movieText;
if (!loadGlobalAssets(menu))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation menu resources"));
- if (!playIntroSequence()) {
+ if (!loadMovieTextAssets(movieText))
+ return Common::Error(Common::kReadingFailed,
+ _("Could not load the PlayStation movie fonts"));
+ if (!playIntroSequence(movieText)) {
if (_vm->shouldQuit())
return Common::kNoError;
return Common::Error(Common::kReadingFailed,
@@ -199,7 +218,8 @@ Common::Error Rebel2PSX::runGame() {
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
- if (!playVideo("S1/L01_INTR.STR", 1, false)) {
+ if (!playVideo("S1/L01_INTR.STR", 1, false, &movieText,
+ kRA2PSXMovieTextChapter1)) {
if (_vm->shouldQuit())
return Common::kNoError;
return Common::Error(Common::kReadingFailed,
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 2bae0b86328..fb6ad53ccb5 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -38,6 +38,13 @@ namespace Scumm {
class ScummEngine_v7;
class RA2PSXLevel1UI;
class RA2PSXMainMenuUI;
+class RA2PSXMovieText;
+
+enum RA2PSXMovieTextSequence {
+ kRA2PSXMovieTextNone,
+ kRA2PSXMovieTextOpening,
+ kRA2PSXMovieTextChapter1
+};
class RA2PSXArchive {
public:
@@ -235,10 +242,13 @@ private:
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 playIntroSequence();
+ bool playVideo(const Common::Path &path, int discNumber, bool version2,
+ const RA2PSXMovieText *movieText = nullptr,
+ RA2PSXMovieTextSequence textSequence = kRA2PSXMovieTextNone);
+ bool playIntroSequence(const RA2PSXMovieText &movieText);
MenuResult runMainMenu(const RA2PSXMainMenuUI &ui);
bool loadGlobalAssets(RA2PSXMainMenuUI &menu);
+ bool loadMovieTextAssets(RA2PSXMovieText &movieText);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui);
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index f3a2e16238a..bc25de6c95b 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -10,6 +10,8 @@
* (at your option) any later version.
*/
+#include "common/memstream.h"
+#include "common/stream.h"
#include "common/util.h"
#include "scumm/insane/rebel2/psx/ui.h"
@@ -51,6 +53,29 @@ bool RA2PSXTextureSet::append(const Common::Array<byte> &data) {
return loadRA2PSXTextures(data, _textures);
}
+bool RA2PSXTextureSet::appendRaw24(const char *name, const Common::Array<byte> &data,
+ uint16 width, uint16 height) {
+ if (!width || !height || data.size() != (uint32)width * height * 3)
+ return false;
+
+ RA2PSXTexture texture;
+ texture.name = name;
+ texture.width = width;
+ texture.height = height;
+ texture.pixels.resize((uint32)width * height);
+ for (uint32 i = 0; i < texture.pixels.size(); ++i) {
+ const byte *pixel = data.data() + i * 3;
+ if (!(pixel[0] | pixel[1] | pixel[2])) {
+ texture.pixels[i] = 0;
+ continue;
+ }
+ texture.pixels[i] = 0x01000000 | ((uint32)pixel[0] << 16) |
+ ((uint32)pixel[1] << 8) | pixel[2];
+ }
+ _textures.push_back(texture);
+ return true;
+}
+
const RA2PSXTexture *RA2PSXTextureSet::find(const char *name) const {
for (uint i = 0; i < _textures.size(); ++i) {
if (_textures[i].name.equalsIgnoreCase(name))
@@ -141,6 +166,284 @@ void RA2PSXTextureSet::drawText(Graphics::Surface &surface, const char *font,
}
}
+struct RA2PSXMovieFont {
+ const char *texture;
+ const char *characters;
+ const byte *widths;
+ const byte *advances;
+ int fixedAdvance;
+ int height;
+ int rowStep;
+};
+
+struct RA2PSXMovieGlyph {
+ int x;
+ int row;
+ int width;
+};
+
+struct RA2PSXMovieTextRecord {
+ uint16 firstFrame;
+ uint16 duration;
+ byte style;
+ byte color;
+ byte initialCharacters;
+ int16 x;
+ int16 y;
+ const char *text;
+};
+
+static const byte kMovieBigAdvances[] = {
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
+};
+
+static const byte kMovieBigWidths[] = {
+ 6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 10, 6, 6, 6, 6,
+ 6, 6, 6, 6, 8, 14, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6,
+ 6, 2, 4, 6, 2, 10, 6, 4, 6, 6, 6, 4, 6, 6, 6, 6, 10,
+ 6, 6, 6, 8, 2, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 8, 4
+};
+
+static const byte kMovieSmallWidths[] = {
+ 6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 8, 6, 6, 6, 6, 4, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 2, 2, 6, 2, 6, 10, 10, 10, 10, 2
+};
+
+static const byte kMovieTinyWidths[] = {
+ 10, 10, 10, 10, 10, 10, 12, 12, 4, 10, 10, 8, 14, 10, 12,
+ 10, 12, 10, 10, 12, 12, 12, 14, 10, 12, 10, 8, 8, 8, 8,
+ 8, 6, 8, 8, 4, 6, 8, 4, 12, 8, 8, 8, 8, 6, 8, 6, 8, 8,
+ 12, 10, 8, 8, 8, 6, 8, 8, 10, 8, 8, 8, 8, 8, 4, 6, 10,
+ 4, 10, 4, 6, 4, 4, 6, 6, 8, 4, 8, 8, 14, 10, 4, 8, 8,
+ 6, 6, 6, 6, 8, 6, 4, 6
+};
+
+static const RA2PSXMovieFont kMovieBigFont = {
+ "fNT24b",
+ "ABCDEFGHIJKLMN" "\x01" "OPQRSTUVWXYZ-" "\x01"
+ "abcdefghijklmn," "\x01" "opqrstuvwxyz%." "\x01" "0123456789? ",
+ kMovieBigWidths, kMovieBigAdvances, 0, 16, 16
+};
+
+static const RA2PSXMovieFont kMovieSmallFont = {
+ "fNT24s",
+ "abcdefghijkl" "\x01" "mnopqrstuvwx" "\x01" "yz0123456789" "\x01"
+ "%-:.? /{}[]|",
+ kMovieSmallWidths, nullptr, 12, 10, 10
+};
+
+static const RA2PSXMovieFont kMovieTinyFont = {
+ "fNT24t",
+ "ABCDEFGHIJ" "\x01" "KLMNOPQRST" "\x01" "UVWXYZabcd" "\x01"
+ "efghijklmn" "\x01" "opqrstuvwx" "\x01" "yz01234567" "\x01"
+ "89!'+,-./:" "\x01" "^()*;?=%_ " "\x01" "<>`[\\]@{|}",
+ kMovieTinyWidths, nullptr, 16, 16, 17
+};
+
+static const RA2PSXMovieTextRecord kOpeningText[] = {
+ { 720, 30, 1, 2, 40, 0, 65, "starring" },
+ { 720, 30, 6, 129, 40, 70, 85, "Jamison Jones" },
+ { 720, 30, 6, 129, 40, 250, 100, "Julie Eccles" },
+ { 758, 30, 1, 2, 40, 0, 85, "original design and story" },
+ { 758, 30, 7, 129, 40, 0, 100, "Vince Lee" },
+ { 796, 30, 1, 2, 40, 0, 85, "lead programmer" },
+ { 796, 30, 7, 129, 40, 0, 100, "Jens Petersam" },
+ { 834, 30, 1, 2, 40, 0, 85, "lead artist/animator" },
+ { 834, 30, 7, 129, 40, 0, 100, "Richard Green" },
+ { 872, 30, 1, 2, 40, 0, 80, "sound designers" },
+ { 872, 30, 7, 129, 40, 0, 95, "Rudolf Stember" },
+ { 872, 30, 7, 129, 40, 0, 115, "Larry the O" },
+ { 910, 30, 1, 2, 40, 0, 85, "director of live action" },
+ { 910, 30, 7, 129, 40, 0, 100, "Hal Barwood" },
+ { 948, 68, 0, 2, 40, 146, 55, "artist-animators" },
+ { 948, 34, 6, 129, 40, 60, 70, "Richard Green" },
+ { 948, 34, 6, 129, 40, -1, 70, "Garry M.Gaber" },
+ { 948, 34, 6, 129, 40, 60, 90, "Jon Knoles" },
+ { 948, 34, 6, 129, 40, -1, 90, "Craig Rundels" },
+ { 948, 34, 6, 129, 40, 60, 110, "Daniel Colon Jr." },
+ { 948, 34, 6, 129, 40, -1, 110, "Clint Young" },
+ { 948, 34, 6, 129, 40, 60, 130, "Ron K. Lussier" },
+ { 948, 34, 6, 129, 40, -1, 130, "Seth Piezas" },
+ { 982, 34, 6, 129, 40, 60, 70, "Eric Ingerson" },
+ { 982, 34, 6, 129, 40, -1, 70, "Bill Stoneham" },
+ { 982, 34, 6, 129, 40, 60, 90, "Mario Wagner" },
+ { 982, 34, 6, 129, 40, -1, 90, "Andreas Escher" },
+ { 982, 34, 0, 2, 40, 214, 110, "and" },
+ { 982, 34, 6, 129, 40, 176, 130, "Mechadeus" },
+ { 1024, 30, 0, 2, 40, 146, 85, "real-time models" },
+ { 1024, 30, 6, 129, 40, 150, 100, "Tobias J. Richter" },
+ { 1062, 30, 0, 2, 40, 40, 50, "lead video effects compositor" },
+ { 1062, 30, 6, 129, 40, 100, 65, "Mark Christiansen" },
+ { 1062, 30, 0, 2, 40, 160, 100, "video effects compositor" },
+ { 1062, 30, 6, 129, 40, 230, 115, "Chris Weakley" },
+ { 1100, 30, 0, 2, 40, 100, 50, "lead art technician" },
+ { 1100, 30, 6, 129, 40, 120, 65, "Aaron Muszalski" },
+ { 1100, 30, 0, 2, 40, 200, 100, "art technician" },
+ { 1100, 30, 6, 129, 40, 200, 115, "Doug Shannon" },
+ { 1138, 30, 1, 2, 40, 0, 80, "additional programmers" },
+ { 1138, 30, 7, 129, 40, 0, 95, "Thomas Engel" },
+ { 1138, 30, 7, 129, 40, 0, 115, "Holger Schmidt" },
+ { 1176, 30, 1, 2, 40, 0, 85, "voice director and producer" },
+ { 1176, 30, 7, 129, 40, 0, 100, "Tamlynn Barra" },
+ { 1214, 30, 1, 2, 40, 0, 85, "lead tester" },
+ { 1214, 30, 7, 129, 40, 0, 100, "Matthew Azeveda" },
+ { 1252, 30, 1, 2, 40, 0, 85, "producer - factor 5" },
+ { 1252, 30, 7, 129, 40, 0, 100, "Julian Eggebrecht" },
+ { 1290, 30, 1, 2, 40, 0, 85, "director of production" },
+ { 1290, 30, 7, 129, 40, 0, 100, "Steve Dauterman" },
+ { 1328, 30, 1, 2, 40, 0, 80, "production coordinators" },
+ { 1328, 30, 7, 129, 40, 0, 95, "Rachel Bryant" },
+ { 1328, 30, 7, 129, 40, 0, 115, "Peggy Stok" },
+ { 1366, 30, 1, 2, 40, 0, 85, "special thanks to" },
+ { 1366, 30, 7, 129, 40, 0, 100, "George Lucas" }
+};
+
+static const RA2PSXMovieTextRecord kChapter1Text[] = {
+ { 30, 80, 1, 1, 0, 0, 24, "chapter 1" },
+ { 40, 70, 3, 128, 0, 0, 37, "The Dreighton Triangle" }
+};
+
+static bool findMovieGlyph(const RA2PSXMovieFont &font, char character,
+ RA2PSXMovieGlyph &glyph) {
+ int metric = 0;
+ int row = 0;
+ int x = 0;
+ for (const char *entry = font.characters; *entry; ++entry) {
+ if (*entry == '\x01') {
+ ++row;
+ x = 0;
+ continue;
+ }
+ if (*entry == character) {
+ glyph.x = x;
+ glyph.row = row;
+ glyph.width = font.widths[metric];
+ return true;
+ }
+ x += font.advances ? font.advances[metric] : font.fixedAdvance;
+ ++metric;
+ }
+ return false;
+}
+
+static const RA2PSXMovieFont &getMovieFont(byte style) {
+ if (style == 2 || style == 3)
+ return kMovieBigFont;
+ if (style == 6 || style == 7)
+ return kMovieTinyFont;
+ return kMovieSmallFont;
+}
+
+static int measureMovieText(const RA2PSXMovieFont &font, const char *text,
+ uint characters, int spacing) {
+ int width = 0;
+ uint drawn = 0;
+ for (; *text && drawn < characters; ++text, ++drawn) {
+ RA2PSXMovieGlyph glyph;
+ if (findMovieGlyph(font, *text, glyph))
+ width += glyph.width + spacing;
+ }
+ return width ? width - spacing : 0;
+}
+
+static int scaleMovieX(int x) {
+ const int remainder = x % 3;
+ if (remainder)
+ x += 3 - remainder;
+ return x * 2 / 3;
+}
+
+static int findMovieFontArchive(const Common::Array<byte> &data) {
+ for (uint offset = 0; offset + 40 <= data.size(); offset += 4) {
+ if (!memcmp(data.data() + offset, "fNT24s", 7) &&
+ !memcmp(data.data() + offset + 16, "fNT24b", 7) &&
+ !memcmp(data.data() + offset + 32, "fNT24t", 7))
+ return offset;
+ }
+ return -1;
+}
+
+bool RA2PSXMovieText::load(Common::SeekableReadStream &executable) {
+ if (executable.size() <= 0 || executable.size() > 2 * 1024 * 1024)
+ return false;
+
+ Common::Array<byte> executableData;
+ executableData.resize((uint32)executable.size());
+ executable.seek(0);
+ if (executable.read(executableData.data(), executableData.size()) != executableData.size())
+ return false;
+ const int archiveOffset = findMovieFontArchive(executableData);
+ if (archiveOffset < 0)
+ return false;
+
+ Common::MemoryReadStream stream(executableData.data() + archiveOffset,
+ executableData.size() - archiveOffset);
+ RA2PSXArchive archive;
+ if (!archive.load(stream))
+ return false;
+
+ Common::Array<byte> data;
+ _textures.clear();
+ // These sheets are packed RGB24, despite being uploaded as 16-bit VRAM words.
+ return archive.getMember("fNT24s", data) &&
+ _textures.appendRaw24("fNT24s", data, 144, 120) &&
+ archive.getMember("fNT24b", data) &&
+ _textures.appendRaw24("fNT24b", data, 128, 80) &&
+ archive.getMember("fNT24t", data) &&
+ _textures.appendRaw24("fNT24t", data, 160, 153);
+}
+
+void RA2PSXMovieText::draw(Graphics::Surface &surface,
+ RA2PSXMovieTextSequence sequence, int frame, int xOffset, int yOffset) const {
+ const RA2PSXMovieTextRecord *records = nullptr;
+ uint recordCount = 0;
+ if (sequence == kRA2PSXMovieTextOpening) {
+ records = kOpeningText;
+ recordCount = ARRAYSIZE(kOpeningText);
+ } else if (sequence == kRA2PSXMovieTextChapter1) {
+ records = kChapter1Text;
+ recordCount = ARRAYSIZE(kChapter1Text);
+ }
+
+ for (uint i = 0; i < recordCount; ++i) {
+ const RA2PSXMovieTextRecord &record = records[i];
+ if (frame < record.firstFrame || frame >= record.firstFrame + record.duration)
+ continue;
+
+ const RA2PSXMovieFont &font = getMovieFont(record.style);
+ const uint characters = MIN<uint>(40,
+ record.initialCharacters + frame - record.firstFrame);
+ const int spacing = record.style < 4 ? 2 : 0;
+ const int textWidth = measureMovieText(font, record.text, characters, spacing);
+ int movieX;
+ if (record.style & 1)
+ movieX = (480 - textWidth * 3 / 2) / 2;
+ else if (record.x < 0)
+ movieX = 360 - textWidth;
+ else
+ movieX = record.x;
+ int x = xOffset + scaleMovieX(movieX);
+
+ uint drawn = 0;
+ for (const char *text = record.text; *text && drawn < characters; ++text, ++drawn) {
+ RA2PSXMovieGlyph glyph;
+ if (!findMovieGlyph(font, *text, glyph))
+ continue;
+ int sourceY = glyph.row * font.rowStep;
+ if (record.style < 2)
+ sourceY += MIN<int>(record.color, 2) * 40;
+ _textures.draw(surface, font.texture, x, yOffset + record.y,
+ Common::Rect(glyph.x, sourceY, glyph.x + glyph.width,
+ sourceY + font.height));
+ x += glyph.width + spacing;
+ }
+ }
+}
+
bool RA2PSXMainMenuUI::load(const RA2PSXArchive &archive) {
Common::Array<byte> data;
_textures.clear();
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 2e9ce8e6a1d..04d23f629b7 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -30,6 +30,8 @@ public:
void clear() { _textures.clear(); }
bool append(const Common::Array<byte> &data);
+ bool appendRaw24(const char *name, const Common::Array<byte> &data,
+ uint16 width, uint16 height);
bool has(const char *name) const { return find(name) != nullptr; }
void draw(Graphics::Surface &surface, const char *name, int x, int y,
const Common::Rect &source, int brightness = 0x80,
@@ -42,6 +44,16 @@ private:
Common::Array<RA2PSXTexture> _textures;
};
+class RA2PSXMovieText {
+public:
+ bool load(Common::SeekableReadStream &executable);
+ void draw(Graphics::Surface &surface, RA2PSXMovieTextSequence sequence,
+ int frame, int xOffset, int yOffset) const;
+
+private:
+ RA2PSXTextureSet _textures;
+};
+
class RA2PSXMainMenuUI {
public:
bool load(const RA2PSXArchive &archive);
Commit: 1fb9617cfa5a5df05f416d1fee213612ba194710
https://github.com/scummvm/scummvm/commit/1fb9617cfa5a5df05f416d1fee213612ba194710
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-24T13:32:28+02:00
Commit Message:
SCUMM: RA2: re-use common L1 code for psx release
Changed paths:
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
engines/scumm/insane/rebel2/shared.h
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 82465dbe4b0..36265c007d2 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -28,6 +28,7 @@
#include "graphics/surface.h"
#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/shared.h"
#include "scumm/insane/rebel2/psx/psx.h"
#include "scumm/insane/rebel2/psx/ui.h"
#include "scumm/insane/rebel2/psx/video.h"
@@ -37,6 +38,79 @@ namespace Scumm {
Rebel2PSX::Rebel2PSX(ScummEngine_v7 *vm) : _vm(vm) {
}
+class Rebel2PSX::Level1Handler : public Rebel2Level1Handler {
+public:
+ enum Error {
+ kGameplayError,
+ kEndingError,
+ kDeathVideoError,
+ kGameOverVideoError
+ };
+
+ Level1Handler(Rebel2PSX &psx, const RA2PSXModel &enemy, const RA2PSXModel &ship,
+ const RA2PSXModel &crosshair, const RA2PSXModel &laser,
+ const RA2PSXLevel1UI &ui, int &score) :
+ _psx(psx), _enemy(enemy), _ship(ship), _crosshair(crosshair), _laser(laser),
+ _ui(ui), _score(score), _error(kGameplayError) {
+ }
+
+ bool shouldQuit() const override {
+ return _psx._vm->shouldQuit();
+ }
+
+ Result playAttempt(int &lives) override {
+ switch (_psx.playLevel1(_enemy, _ship, _crosshair, _laser, _ui, lives, _score)) {
+ case Rebel2PSX::kLevel1Quit:
+ return kQuit;
+ case Rebel2PSX::kLevel1Complete:
+ return kComplete;
+ case Rebel2PSX::kLevel1Death:
+ return kDeath;
+ case Rebel2PSX::kLevel1Error:
+ return kError;
+ }
+
+ return kError;
+ }
+
+ bool playComplete() override {
+ return playVideo("S1/L01_EXTR.STR", kEndingError);
+ }
+
+ bool playDeath() override {
+ return playVideo("S1/L01_DIE.STR", kDeathVideoError);
+ }
+
+ bool playRetry(int) override {
+ return true;
+ }
+
+ bool playGameOver(int) override {
+ return playVideo("S1/L01_OVER.STR", kGameOverVideoError);
+ }
+
+ Error getError() const {
+ return _error;
+ }
+
+private:
+ bool playVideo(const char *path, Error error) {
+ if (_psx.playVideo(path, 1, false))
+ return true;
+ _error = error;
+ return false;
+ }
+
+ Rebel2PSX &_psx;
+ const RA2PSXModel &_enemy;
+ const RA2PSXModel &_ship;
+ const RA2PSXModel &_crosshair;
+ const RA2PSXModel &_laser;
+ const RA2PSXLevel1UI &_ui;
+ int &_score;
+ Error _error;
+};
+
Common::SeekableReadStream *Rebel2PSX::openResource(int number) {
const Common::Path path(Common::String::format("RESOURCE.%03d", number));
Common::File *file = new Common::File();
@@ -228,34 +302,24 @@ Common::Error Rebel2PSX::runGame() {
int lives = 3;
int score = 0;
- while (!_vm->shouldQuit()) {
- const Level1Result result = playLevel1(enemy, ship, crosshair, laser, ui, lives, score);
- 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;
- }
+ Level1Handler handler(*this, enemy, ship, crosshair, laser, ui, score);
+ if (runRebel2Level1(handler, lives) != Rebel2Level1Handler::kError)
+ 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;
- }
+ switch (handler.getError()) {
+ case Level1Handler::kEndingError:
+ return Common::Error(Common::kReadingFailed,
+ _("Could not play the PlayStation Level 1 ending"));
+ case Level1Handler::kDeathVideoError:
+ return Common::Error(Common::kReadingFailed,
+ _("Could not play the PlayStation Level 1 death video"));
+ case Level1Handler::kGameOverVideoError:
+ return Common::Error(Common::kReadingFailed,
+ _("Could not play the PlayStation game-over video"));
+ default:
+ return Common::Error(Common::kReadingFailed,
+ _("Could not run the PlayStation Level 1 gameplay"));
}
- return Common::kNoError;
#else
return Common::Error(Common::kUnsupportedGameidError,
_s("Rebel Assault II PlayStation support requires TinyGL"));
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index fb6ad53ccb5..3f09bb80398 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -228,6 +228,8 @@ public:
Common::Error runGame();
private:
+ class Level1Handler;
+
enum MenuResult {
kMenuStart,
kMenuQuit
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index d3af0ee0c96..ed180e99ffc 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -270,6 +270,7 @@ public:
Common::String getLevelPrefix(int levelId);
// Per-level handlers.
+ class Level1Handler;
int runLevel1();
int runLevel2();
int runLevel3();
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 96da9fd3a58..66c0f2889bc 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -26,53 +26,112 @@
#include "scumm/smush/smush_player.h"
#include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel2/shared.h"
namespace Scumm {
-int InsaneRebel2::runLevel1() {
- playLevelBegin(1);
- if (_vm->shouldQuit())
- return kLevelQuit;
+Rebel2Level1Handler::Result runRebel2Level1(Rebel2Level1Handler &handler, int lives) {
+ while (!handler.shouldQuit()) {
+ const Rebel2Level1Handler::Result result = handler.playAttempt(lives);
+ if (result == Rebel2Level1Handler::kQuit || result == Rebel2Level1Handler::kError)
+ return result;
+ if (result == Rebel2Level1Handler::kComplete) {
+ if (!handler.playComplete())
+ return handler.shouldQuit() ? Rebel2Level1Handler::kQuit : Rebel2Level1Handler::kError;
+ return Rebel2Level1Handler::kComplete;
+ }
- while (!_vm->shouldQuit()) {
- _playerShield = 255;
- _playerDamage = 0;
- _deathFrame = 0;
- resetExplosions();
+ if (!handler.playDeath())
+ return handler.shouldQuit() ? Rebel2Level1Handler::kQuit : Rebel2Level1Handler::kError;
+ if (handler.shouldQuit())
+ return Rebel2Level1Handler::kQuit;
- clearBit(0);
- _rebelKillCounter = 0;
- _rebelHitCounter = 0;
+ --lives;
+ if (lives <= 0) {
+ if (!handler.playGameOver(lives))
+ return handler.shouldQuit() ? Rebel2Level1Handler::kQuit : Rebel2Level1Handler::kError;
+ return Rebel2Level1Handler::kGameOver;
+ }
- if (!playLevelSegment("LEV01/01P01.SAN", 0x28))
- return kLevelQuit;
+ if (!handler.playRetry(lives))
+ return handler.shouldQuit() ? Rebel2Level1Handler::kQuit : Rebel2Level1Handler::kError;
+ if (handler.shouldQuit())
+ return Rebel2Level1Handler::kQuit;
+ }
- if (_playerShield > 0) {
- int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
- debugC(DEBUG_INSANE, "Level 1 completed!");
- playLevelEnd(1, accuracy, -1, false);
- _levelUnlocked[1] = true;
- return kLevelNextLevel;
- }
+ return Rebel2Level1Handler::kQuit;
+}
- debugC(DEBUG_INSANE, "Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
- playLevelDeathVariant(1, 1, _deathFrame);
+class InsaneRebel2::Level1Handler : public Rebel2Level1Handler {
+public:
+ explicit Level1Handler(InsaneRebel2 &rebel) : _rebel(rebel) {}
- if (_vm->shouldQuit())
- return kLevelQuit;
+ bool shouldQuit() const override {
+ return _rebel._vm->shouldQuit();
+ }
- _playerLives--;
- if (_playerLives <= 0) {
- playLevelGameOver(1);
- return kLevelGameOver;
- }
+ Result playAttempt(int &lives) override {
+ _rebel._playerLives = lives;
+ _rebel._playerShield = 255;
+ _rebel._playerDamage = 0;
+ _rebel._deathFrame = 0;
+ _rebel.resetExplosions();
- playLevelRetry(1);
- if (_vm->shouldQuit())
- return kLevelQuit;
+ _rebel.clearBit(0);
+ _rebel._rebelKillCounter = 0;
+ _rebel._rebelHitCounter = 0;
+
+ if (!_rebel.playLevelSegment("LEV01/01P01.SAN", 0x28))
+ return kQuit;
+
+ lives = _rebel._playerLives;
+ return _rebel._playerShield > 0 ? kComplete : kDeath;
}
- return kLevelQuit;
+ bool playComplete() override {
+ const int accuracy = _rebel.calculateAccuracy(_rebel._rebelKillCounter, _rebel._rebelHitCounter);
+ debugC(DEBUG_INSANE, "Level 1 completed!");
+ _rebel.playLevelEnd(1, accuracy, -1, false);
+ _rebel._levelUnlocked[1] = true;
+ return true;
+ }
+
+ bool playDeath() override {
+ debugC(DEBUG_INSANE, "Level 1 death at frame %d, lives=%d", _rebel._deathFrame, _rebel._playerLives - 1);
+ _rebel.playLevelDeathVariant(1, 1, _rebel._deathFrame);
+ return true;
+ }
+
+ bool playRetry(int lives) override {
+ _rebel._playerLives = lives;
+ _rebel.playLevelRetry(1);
+ return true;
+ }
+
+ bool playGameOver(int lives) override {
+ _rebel._playerLives = lives;
+ _rebel.playLevelGameOver(1);
+ return true;
+ }
+
+private:
+ InsaneRebel2 &_rebel;
+};
+
+int InsaneRebel2::runLevel1() {
+ playLevelBegin(1);
+ if (_vm->shouldQuit())
+ return kLevelQuit;
+
+ Level1Handler handler(*this);
+ switch (runRebel2Level1(handler, _playerLives)) {
+ case Rebel2Level1Handler::kComplete:
+ return kLevelNextLevel;
+ case Rebel2Level1Handler::kGameOver:
+ return kLevelGameOver;
+ default:
+ return kLevelQuit;
+ }
}
InsaneRebel2::WaveEndResult InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
diff --git a/engines/scumm/insane/rebel2/shared.h b/engines/scumm/insane/rebel2/shared.h
index 3476a8c40de..f143d3488f1 100644
--- a/engines/scumm/insane/rebel2/shared.h
+++ b/engines/scumm/insane/rebel2/shared.h
@@ -30,6 +30,27 @@ enum {
kRebel2MenuResultNone = -1
};
+class Rebel2Level1Handler {
+public:
+ enum Result {
+ kQuit,
+ kComplete,
+ kDeath,
+ kGameOver,
+ kError
+ };
+
+ virtual ~Rebel2Level1Handler() {}
+ virtual bool shouldQuit() const = 0;
+ virtual Result playAttempt(int &lives) = 0;
+ virtual bool playComplete() = 0;
+ virtual bool playDeath() = 0;
+ virtual bool playRetry(int lives) = 0;
+ virtual bool playGameOver(int lives) = 0;
+};
+
+Rebel2Level1Handler::Result runRebel2Level1(Rebel2Level1Handler &handler, int lives);
+
inline Rebel2MenuCommand getRebel2MenuCommand(const Common::KeyState &key) {
switch (key.keycode) {
case Common::KEYCODE_UP:
More information about the Scummvm-git-logs
mailing list