[Scummvm-git-logs] scummvm master -> 81d3f76d302b9c28be438dfab4f76a93b2e9c662
neuromancer
noreply at scummvm.org
Wed Oct 14 18:59:22 UTC 2026
This automated email contains information about 21 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
65ddda86b4 SCUMM: MORTIMER: first POC to support demo
82d9ef19a4 SCUMM: MORTIMER: improved flight progression code and removed POC naming
322730bb23 SCUMM: MORTIMER: implemented UI details like shooting, status bars, targets among others
af9e3bf7ee SCUMM: MORTIMER: implemented intro video decoding and playback
bbb9d83c86 SCUMM: MORTIMER: implemented basic interview handling
1c2a61c56e SCUMM: MORTIMER: improved video playback to avoid slowdowns
c866c4ade3 SCUMM: MORTIMER: added full game detection
8b1b2cc97c SCUMM: MORTIMER: full game intro implemented
cce591bdbf SCUMM: MORTIMER: implement campaign progression
4f4543fa81 SCUMM: MORTIMER: use copyRectToSurfaceWithKey for sprite drawing
5996c0daa7 SCUMM: MORTIMER: shared SAUD/SAD reader for RA1 and mortimer
6a4ad1273e SCUMM: MORTIMER: implement shellshock-related code and indicators
625e660068 SCUMM: MORTIMER: added restore content option for the demo with some extra unused videos
eaa45832a0 SCUMM: MORTIMER: implemented dificulty levels
972d6b1a9a SCUMM: MORTIMER: enable view jolt when taking damage in demo
09f67f2a8e SCUMM: MORTIMER: abstracted SAUD code and use it for Mortimer and RA
f8a95c9e4c SCUMM: MORTIMER: initial implementation of castle gameplay
e5c3219592 SCUMM: MORTIMER: implemented cheats and easter eggs
30cdd215a4 SCUMM: MORTIMER: adjusted the position of some elements in the full game
bc691b52ca SCUMM: MORTIMER: automatic saving per profile
81d3f76d30 SCUMM: MORTIMER: added support for high score save/load
Commit: 65ddda86b4232af98f55745e8d248091c0c7a46a
https://github.com/scummvm/scummvm/commit/65ddda86b4232af98f55745e8d248091c0c7a46a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: first POC to support demo
Changed paths:
A engines/scumm/insane/mortimer/flight.cpp
A engines/scumm/insane/mortimer/flight.h
A engines/scumm/insane/mortimer/mortimer.cpp
A engines/scumm/insane/mortimer/mortimer.h
devtools/scumm-md5.txt
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/metaengine.cpp
engines/scumm/module.mk
engines/scumm/scumm-md5.h
diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index fd97698ba56..7a0e29c97f3 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -449,6 +449,9 @@ ft Full Throttle
9b7452b5cd6d3ffb2b2f5118010af84f 116463537 en Mac Demo Demo Mac bundle Fingolfin, Joachim Eberhard
8ead2c01c48c2e3d15d6a89b86cd84ab 19697 he DOS - - - Hebrew fan translation
+mortimer Mortimer and the Riddles of the Medallion
+ 3450a722d29c150a4bdac5ba9b5f3b46 290304 en Windows - Demo (flight POC) Windows demo launcher; first Savanna flight only -
+
rebel1 Star Wars: Rebel Assault
0ca9c07185b4969764eaf3ecd0de06e9 111530 en DOS Demo v0.67 Demo v0.67 Non-interactive demo -
a29730a42dfcff7bc2e10acfca1e76a2 212951 en DOS Demo v1.5 Demo v1.5 CD-ROM demo (chapters 2 and 10) -
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 2214472fef6..4cb7b85d199 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -274,7 +274,8 @@ enum ScummGameId {
GID_BASKETBALL,
GID_MOONBASE,
GID_PJGAMES,
- GID_HECUP // CUP demos
+ GID_HECUP, // CUP demos
+ GID_MORTIMER
};
struct RuScummPatcher {
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 222ca6f7bbb..b6bd4f14739 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -77,6 +77,7 @@ static const PlainGameDescriptor gameDescriptions[] = {
{ "ft", "Full Throttle" },
{ "rebel1", "Star Wars: Rebel Assault" },
{ "rebel2", "Star Wars: Rebel Assault II: The Hidden Empire" },
+ { "mortimer", "Mortimer and the Riddles of the Medallion" },
{ "dig", "The Dig" },
{ "comi", "The Curse of Monkey Island" },
@@ -250,6 +251,8 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
+ {"mortimer", 0, "flight-poc", GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Steam", "steam", GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -518,6 +521,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
+ { "mortimer", "MORTIMER.EXE", kGenUnchanged, Common::EN_ANY, Common::kPlatformWindows, 0 },
+
{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo v1.51" },
{ "rebel1", "REBEL.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo 1994-04-13" },
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
new file mode 100644
index 00000000000..f0cddef206c
--- /dev/null
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -0,0 +1,529 @@
+/* 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 "scumm/insane/mortimer/flight.h"
+#include "scumm/smush/codec37.h"
+#include "scumm/smush/rebel/codec_ra2.h"
+
+#include "common/endian.h"
+#include "common/stream.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+namespace {
+
+struct Chunk {
+ uint32 tag, offset, size;
+};
+
+bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk, bool padded = true) {
+ if (pos > end || end - pos < 8)
+ return false;
+ chunk.tag = READ_BE_UINT32(data + pos);
+ chunk.size = READ_BE_UINT32(data + pos + 4);
+ chunk.offset = pos + 8;
+ if (chunk.size > end - chunk.offset)
+ return false;
+ pos = chunk.offset + chunk.size;
+ if (padded && (chunk.size & 1)) {
+ if (pos == end)
+ return false;
+ ++pos;
+ }
+ return true;
+}
+
+bool readData(Common::SeekableReadStream &stream, Common::Array<byte> &data) {
+ const int64 size = stream.size();
+ if (size < 8 || size > 64 * 1024 * 1024 || !stream.seek(0))
+ return false;
+ data.resize(size);
+ return stream.read(data.begin(), data.size()) == data.size();
+}
+
+bool isContainer(const Common::Array<byte> &data, uint32 tag) {
+ return data.size() >= 8 && READ_BE_UINT32(data.begin()) == tag &&
+ READ_BE_UINT32(data.begin() + 4) == data.size() - 8;
+}
+
+bool validRows(const byte *src, uint32 size, int height, bool remap) {
+ uint32 pos = 0;
+ // Remap masks can include additional rows below the FOBJ clipping height.
+ for (int y = 0; y < height || (remap && pos + 1 < size); ++y) {
+ if (size - pos < 2)
+ return false;
+ const uint len = READ_LE_UINT16(src + pos);
+ pos += 2;
+ if (len > size - pos)
+ return false;
+ const uint32 end = pos + len;
+ if (remap) {
+ // Skip/run pairs; a final skip without a run is also permitted.
+ pos = end;
+ } else {
+ while (pos < end) {
+ const byte code = src[pos++];
+ const uint count = (code & 1) ? 1 : (code >> 1) + 1;
+ if (count > end - pos)
+ return false;
+ pos += count;
+ }
+ }
+ }
+ // FOBJ includes the encoder's final byte of alignment in its payload size.
+ return pos == size || (pos + 1 == size && src[pos] == 0);
+}
+
+// Codec37's existing decoder has no input-length argument. Validate the two
+// encodings actually present in DEMO_S1 before passing any data to it.
+bool validBackground(const byte *src, uint32 size) {
+ if (size < 16 || src[1] != 1 || src[12] != 4)
+ return false;
+ uint32 pos = 16;
+ if (src[0] == 2) {
+ if (READ_LE_UINT32(src + 4) != MortimerFlight::kWidth * MortimerFlight::kHeight)
+ return false;
+ uint remaining = MortimerFlight::kWidth * MortimerFlight::kHeight;
+ while (remaining) {
+ if (pos == size)
+ return false;
+ const byte code = src[pos++];
+ const uint run = (code >> 1) + 1;
+ const uint count = (code & 1) ? 1 : run;
+ if (count > size - pos || run > remaining)
+ return false;
+ pos += count;
+ remaining -= run;
+ }
+ } else if (src[0] == 3) {
+ for (uint block = 0; block < MortimerFlight::kWidth * MortimerFlight::kHeight / 16; ++block) {
+ if (pos == size)
+ return false;
+ const byte code = src[pos++];
+ const uint count = code == 0xff ? 16 : code == 0xfe ? 4 : code == 0xfd ? 1 : 0;
+ if (count > size - pos)
+ return false;
+ pos += count;
+ }
+ } else {
+ return false;
+ }
+ return pos <= size;
+}
+
+bool validObject(const byte *data, uint32 size, bool sprite = false) {
+ if (size < 14)
+ return false;
+ const int width = READ_LE_INT16(data + 6), height = READ_LE_INT16(data + 8);
+ if (width <= 0 || height <= 0 || width > 640 || height > 480)
+ return false;
+ if (READ_LE_INT16(data + 2) + width > 32767 || READ_LE_INT16(data + 4) + height > 32767)
+ return false;
+ const byte codec = data[0];
+ if (sprite)
+ return codec == 1 && validRows(data + 14, size - 14, height, false);
+ if (codec == 37)
+ return width == MortimerFlight::kWidth && height == MortimerFlight::kHeight &&
+ READ_LE_UINT32(data + 2) == 0 && validBackground(data + 14, size - 14);
+ if (codec != 3 && codec != 23)
+ return false;
+ uint32 offset = 14;
+ if (codec == 23) {
+ const uint16 mode = READ_LE_UINT16(data + 12);
+ if (mode != 0x100 && mode != 0x101)
+ return false;
+ if (mode == 0x100)
+ offset += 256;
+ }
+ return offset <= size && validRows(data + offset, size - offset, height, codec == 23);
+}
+
+bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
+ if (!isContainer(data, MKTAG('S', 'A', 'U', 'D')))
+ return false;
+ sound.samples.clear();
+ uint32 pos = 8;
+ while (pos < data.size()) {
+ Chunk chunk;
+ // SAUD chunks are not word-aligned, unlike the surrounding SMUSH file.
+ if (!nextChunk(data.begin(), data.size(), pos, chunk, false))
+ return false;
+ const byte *src = data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('S', 'D', 'A', 'T')) {
+ if (!sound.samples.empty())
+ return false;
+ sound.samples.assign(src, src + chunk.size);
+ } else if (chunk.tag == MKTAG('S', 'H', 'D', 'R') && chunk.size == 4) {
+ sound.rate = READ_LE_UINT32(src);
+ } else if (chunk.tag == MKTAG('S', 'T', 'R', 'K') && chunk.size >= 14 && src[0] == 6 && src[1] == 12) {
+ sound.rate = READ_BE_UINT32(src + 10);
+ }
+ }
+ return !sound.samples.empty() && sound.rate >= 4000 && sound.rate <= 48000;
+}
+
+} // End of anonymous namespace
+
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites) {
+ Common::Array<byte> data;
+ sprites.clear();
+ if (!readData(stream, data) || !isContainer(data, MKTAG('A', 'N', 'I', 'M')))
+ return false;
+ uint32 pos = 8;
+ while (pos < data.size()) {
+ Chunk frame;
+ if (!nextChunk(data.begin(), data.size(), pos, frame))
+ return false;
+ if (frame.tag != MKTAG('F', 'R', 'M', 'E'))
+ continue;
+ uint32 subpos = frame.offset;
+ const uint before = sprites.size();
+ while (subpos < frame.offset + frame.size) {
+ Chunk chunk;
+ if (!nextChunk(data.begin(), frame.offset + frame.size, subpos, chunk))
+ return false;
+ if (chunk.tag != MKTAG('F', 'O', 'B', 'J'))
+ continue;
+ const byte *src = data.begin() + chunk.offset;
+ if (!validObject(src, chunk.size, true))
+ return false;
+ MortimerSprite sprite;
+ sprite.x = READ_LE_INT16(src + 2);
+ sprite.y = READ_LE_INT16(src + 4);
+ sprite.width = READ_LE_INT16(src + 6);
+ sprite.height = READ_LE_INT16(src + 8);
+ sprite.pixels.resize(sprite.width * sprite.height);
+ memset(sprite.pixels.begin(), 0, sprite.pixels.size());
+ smushDecodeRLEOpaque(sprite.pixels.begin(), src + 14, 0, 0, sprite.width, sprite.height, sprite.width, chunk.size - 14);
+ sprites.push_back(sprite);
+ }
+ if (sprites.size() != before + 1)
+ return false;
+ }
+ return !sprites.empty();
+}
+
+bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound) {
+ Common::Array<byte> data;
+ return readData(stream, data) && decodeSound(data, sound);
+}
+
+MortimerFlight::MortimerFlight() : _decoder(nullptr), _nextFrame(0), _frameRate(10), _rescuedCount(0), _rescuedSpecies(0) {
+ _background.resize(kWidth * kHeight);
+ _pixels.resize(kWidth * kHeight);
+ memset(_palette, 0, sizeof(_palette));
+ reset();
+}
+
+MortimerFlight::~MortimerFlight() {
+ delete _decoder;
+}
+
+bool MortimerFlight::fail(const char *message) {
+ _error = message;
+ _frames.clear();
+ return false;
+}
+
+bool MortimerFlight::load(Common::SeekableReadStream &stream) {
+ _frames.clear();
+ _sounds.clear();
+ _error.clear();
+ reset();
+ if (!readData(stream, _data) || !isContainer(_data, MKTAG('A', 'N', 'I', 'M')))
+ return fail("Invalid Mortimer flight ANIM container");
+
+ struct Track {
+ uint16 id, nextBlock;
+ MortimerSound sound;
+ Common::Array<byte> bytes;
+ };
+ Common::Array<Track> tracks;
+ uint32 pos = 8;
+ uint expectedFrames = 0;
+ while (pos < _data.size()) {
+ Chunk chunk;
+ if (!nextChunk(_data.begin(), _data.size(), pos, chunk))
+ return fail("Truncated Mortimer flight chunk");
+ const byte *src = _data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('A', 'H', 'D', 'R')) {
+ if (expectedFrames || chunk.size < 778 || READ_LE_UINT16(src) != 2)
+ return fail("Unsupported Mortimer flight header");
+ expectedFrames = READ_LE_UINT16(src + 2);
+ _frameRate = READ_LE_UINT32(src + 774);
+ if (!expectedFrames || _frameRate != 10)
+ return fail("Unsupported Mortimer flight timing");
+ memcpy(_palette, src + 6, sizeof(_palette));
+ } else if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
+ if (!expectedFrames)
+ return fail("Mortimer flight has no header");
+ uint32 subpos = chunk.offset;
+ uint backgrounds = 0;
+ while (subpos < chunk.offset + chunk.size) {
+ Chunk sub;
+ if (!nextChunk(_data.begin(), chunk.offset + chunk.size, subpos, sub))
+ return fail("Truncated Mortimer flight frame");
+ const byte *body = _data.begin() + sub.offset;
+ if (sub.tag == MKTAG('F', 'O', 'B', 'J')) {
+ if (!validObject(body, sub.size))
+ return fail("Unsupported or truncated Mortimer flight image");
+ if (body[0] == 37) {
+ ++backgrounds;
+ if (_frames.empty() && body[14] != 2)
+ return fail("Mortimer flight does not start with a keyframe");
+ }
+ } else if (sub.tag == MKTAG('S', 'K', 'I', 'P')) {
+ if (sub.size != 4 || READ_LE_UINT16(body) >= 256 || READ_LE_UINT16(body + 2) >= 256)
+ return fail("Invalid Mortimer object toggle");
+ } else if (sub.tag == MKTAG('I', 'A', 'C', 'T')) {
+ if (sub.size < 8)
+ return fail("Truncated Mortimer flight action");
+ const uint16 code = READ_LE_UINT16(body);
+ if ((code >= 2 && code <= 4 && (sub.size < (code == 4 ? 18U : 12U) || READ_LE_UINT16(body + 8) >= 256)) ||
+ (code == 5 && sub.size < 28))
+ return fail("Invalid Mortimer flight action");
+ } else if (sub.tag == MKTAG('P', 'S', 'A', 'D')) {
+ if (sub.size < 10)
+ return fail("Truncated Mortimer speech block");
+ const uint16 id = READ_LE_UINT16(body), block = READ_LE_UINT16(body + 2);
+ uint index = 0;
+ while (index < tracks.size() && tracks[index].id != id)
+ ++index;
+ if (index == tracks.size()) {
+ Track track;
+ track.id = id;
+ track.nextBlock = 0;
+ track.sound.frame = _frames.size();
+ track.sound.volume = MIN(255, body[8] * 2);
+ track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
+ tracks.push_back(track);
+ }
+ if (tracks[index].nextBlock++ != block)
+ return fail("Missing Mortimer speech block");
+ Common::Array<byte> &bytes = tracks[index].bytes;
+ const uint oldSize = bytes.size();
+ bytes.resize(oldSize + sub.size - 10);
+ memcpy(bytes.begin() + oldSize, body + 10, sub.size - 10);
+ }
+ }
+ if (backgrounds != 1)
+ return fail("Mortimer flight frame needs one background");
+ _frames.push_back({chunk.offset, chunk.size});
+ }
+ }
+ if (_frames.size() != expectedFrames || !expectedFrames)
+ return fail("Incomplete Mortimer flight");
+ for (uint i = 0; i < tracks.size(); ++i) {
+ if (!decodeSound(tracks[i].bytes, tracks[i].sound))
+ return fail("Unsupported or incomplete Mortimer speech");
+ _sounds.push_back(tracks[i].sound);
+ }
+ return true;
+}
+
+void MortimerFlight::reset() {
+ delete _decoder;
+ _decoder = new SmushDeltaBlocksDecoder(kWidth, kHeight);
+ _nextFrame = _rescuedCount = _rescuedSpecies = 0;
+ _targets.clear();
+ _objects.clear();
+ _obstacles.clear();
+ memset(_toggles, 0, sizeof(_toggles));
+ memset(_background.begin(), 0, _background.size());
+ memset(_pixels.begin(), 0, _pixels.size());
+ for (uint i = 0; i < 256; ++i)
+ _remap[i] = i;
+}
+
+MortimerFlight::Target *MortimerFlight::findTarget(uint16 id) {
+ for (uint i = 0; i < _targets.size(); ++i)
+ if (_targets[i].id == id)
+ return &_targets[i];
+ return nullptr;
+}
+
+void MortimerFlight::handleAction(const byte *data, uint32 size) {
+ const uint16 code = READ_LE_UINT16(data), flags = READ_LE_UINT16(data + 2);
+ const uint16 mode = READ_LE_UINT16(data + 4), id = READ_LE_UINT16(data + 6);
+ if (code >= 2 && code <= 4 && mode == 1) {
+ const uint16 toggle = READ_LE_UINT16(data + 8);
+ if (id == 2000) {
+ // Offscreen control object: the remap SKIPs compare against this
+ // false toggle, rather than against the individual stone objects.
+ _toggles[toggle] = false;
+ return;
+ }
+ if (id >= 1000) {
+ Target *target = findTarget(id - 1000);
+ if (target)
+ target->remapToggle = toggle;
+ // Briefly show the rescued animal's silhouette before removing it
+ // from the scene. Flash duration is POC tuning.
+ _toggles[toggle] = !target || !target->rescued || frame() - target->rescuedAt >= 5;
+ return;
+ }
+ Target *target = findTarget(id);
+ if (!target && code == 2) {
+ Target fresh;
+ fresh.id = id;
+ fresh.toggle = toggle;
+ fresh.remapToggle = 0xffff;
+ fresh.rescuedAt = 0;
+ fresh.active = fresh.rescued = false;
+ _targets.push_back(fresh);
+ target = &_targets.back();
+ }
+ if (!target)
+ return;
+ target->toggle = toggle;
+ _toggles[toggle] = target->rescued;
+ if (code == 4) {
+ const int x = READ_LE_INT16(data + 10), y = READ_LE_INT16(data + 12);
+ const int w = READ_LE_INT16(data + 14), h = READ_LE_INT16(data + 16);
+ target->rect = Common::Rect(CLIP<int>(x, 0, kWidth), CLIP<int>(y, 0, kHeight),
+ CLIP<int>(x + MAX(0, w), 0, kWidth), CLIP<int>(y + MAX(0, h), 0, kHeight));
+ target->active = !target->rect.isEmpty();
+ } else if (code == 3) {
+ target->active = false;
+ }
+ } else if (code == 5 && flags == 14 && size >= 28) {
+ Obstacle obstacle;
+ for (uint i = 0; i < 4; ++i)
+ obstacle.corners[i] = Common::Point(READ_LE_INT16(data + 12 + i * 4), READ_LE_INT16(data + 14 + i * 4));
+ _obstacles.push_back(obstacle);
+ }
+ // Flight transitions (8/46), prefetches and shell/interface callbacks are
+ // deliberately outside this single-segment POC.
+}
+
+bool MortimerFlight::advance() {
+ if (_nextFrame >= _frames.size())
+ return false;
+ _objects.clear();
+ _obstacles.clear();
+ for (uint i = 0; i < _targets.size(); ++i)
+ _targets[i].active = false;
+ const Frame &frame = _frames[_nextFrame++];
+ uint32 pos = frame.offset;
+ uint16 skipA = 0xffff, skipB = 0;
+ while (pos < frame.offset + frame.size) {
+ Chunk chunk;
+ if (!nextChunk(_data.begin(), frame.offset + frame.size, pos, chunk))
+ return fail("Truncated Mortimer flight frame");
+ const byte *src = _data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('S', 'K', 'I', 'P')) {
+ skipA = READ_LE_UINT16(src);
+ skipB = READ_LE_UINT16(src + 2);
+ continue;
+ }
+ if (chunk.tag == MKTAG('I', 'A', 'C', 'T')) {
+ handleAction(src, chunk.size);
+ } else if (chunk.tag == MKTAG('F', 'O', 'B', 'J')) {
+ if (src[0] == 37) {
+ _decoder->decode(_background.begin(), src + 14);
+ } else {
+ if (src[0] == 23 && READ_LE_UINT16(src + 12) == 0x100)
+ memcpy(_remap, src + 14, sizeof(_remap));
+ _objects.push_back({chunk.offset, chunk.size, skipA, skipB});
+ }
+ }
+ skipA = 0xffff;
+ skipB = 0;
+ }
+ render();
+ return true;
+}
+
+void MortimerFlight::render() {
+ memcpy(_pixels.begin(), _background.begin(), _pixels.size());
+ for (uint i = 0; i < _objects.size(); ++i) {
+ const Object &object = _objects[i];
+ if (object.skipA != 0xffff && (object.skipB ? _toggles[object.skipA] != _toggles[object.skipB] : _toggles[object.skipA]))
+ continue;
+ const byte *src = _data.begin() + object.offset;
+ const int x = READ_LE_INT16(src + 2), y = READ_LE_INT16(src + 4);
+ const int width = READ_LE_INT16(src + 6), height = READ_LE_INT16(src + 8);
+ Common::Rect clip(x, y, x + width, y + height);
+ clip.clip(Common::Rect(kWidth, kHeight));
+ if (clip.isEmpty())
+ continue;
+ // Decode into an object-sized buffer so offscreen sprites cannot write
+ // beyond the flight surface. Remap objects must start with its pixels.
+ _scratch.resize(width * height);
+ memset(_scratch.begin(), 0, _scratch.size());
+ if (src[0] == 23) {
+ for (int row = clip.top; row < clip.bottom; ++row)
+ memcpy(_scratch.begin() + (row - y) * width + clip.left - x,
+ _pixels.begin() + row * kWidth + clip.left, clip.width());
+ const uint offset = READ_LE_UINT16(src + 12) == 0x100 ? 270 : 14;
+ smushDecodeRA2SkipRemap(_scratch.begin(), src + offset, 0, 0, width, height, width, object.size - offset, _remap, 0);
+ } else {
+ smushDecodeRLEOpaque(_scratch.begin(), src + 14, 0, 0, width, height, width, object.size - 14);
+ }
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ const byte *line = _scratch.begin() + (row - y) * width + clip.left - x;
+ byte *dest = _pixels.begin() + row * kWidth + clip.left;
+ for (int col = 0; col < clip.width(); ++col) {
+ // Mortimer treats zero as transparent in literal runs as well.
+ if (src[0] == 23 || line[col])
+ dest[col] = line[col];
+ }
+ }
+ }
+}
+
+bool MortimerFlight::rescue(const Common::Point &point) {
+ // Later objects are drawn on top of earlier ones.
+ for (int i = int(_targets.size()) - 1; i >= 0; --i) {
+ Target &target = _targets[i];
+ if (!target.active || target.rescued || !target.rect.contains(point))
+ continue;
+ target.rescued = true;
+ target.rescuedAt = frame();
+ _toggles[target.toggle] = true;
+ if (target.remapToggle != 0xffff)
+ _toggles[target.remapToggle] = false;
+ ++_rescuedCount;
+ if (target.id / 100 < 7)
+ _rescuedSpecies |= 1 << (target.id / 100);
+ render();
+ return true;
+ }
+ return false;
+}
+
+bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
+ for (uint n = 0; n < _obstacles.size(); ++n) {
+ const Obstacle &obstacle = _obstacles[n];
+ bool inside = false;
+ for (uint i = 0, j = 3; i < 4; j = i++) {
+ const Common::Point &a = obstacle.corners[i], &b = obstacle.corners[j];
+ if ((a.y > point.y) != (b.y > point.y) &&
+ point.x < a.x + int64(b.x - a.x) * (point.y - a.y) / (b.y - a.y))
+ inside = !inside;
+ }
+ if (inside)
+ return true;
+ }
+ return false;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
new file mode 100644
index 00000000000..23a25475ec2
--- /dev/null
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -0,0 +1,115 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_FLIGHT_H
+#define SCUMM_INSANE_MORTIMER_FLIGHT_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Scumm {
+
+class SmushDeltaBlocksDecoder;
+
+struct MortimerSprite {
+ int x, y, width, height;
+ Common::Array<byte> pixels;
+};
+
+struct MortimerSound {
+ uint frame = 0;
+ uint rate = 11025;
+ byte volume = 255;
+ int8 balance = 0;
+ Common::Array<byte> samples;
+};
+
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites);
+bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
+
+// The first Windows demo flight only. No ScummEngine, InsaneRebel or SmushPlayer
+// state is used here; only the existing, stateless RLE helpers and a private
+// codec37 decoder are shared with the other SMUSH games.
+class MortimerFlight {
+public:
+ enum { kWidth = 384, kHeight = 288 };
+
+ MortimerFlight();
+ ~MortimerFlight();
+ bool load(Common::SeekableReadStream &stream);
+ void reset();
+ bool advance();
+ bool rescue(const Common::Point &point);
+ bool hitsObstacle(const Common::Point &point) const;
+
+ const byte *pixels() const { return _pixels.begin(); }
+ const byte *palette() const { return _palette; }
+ uint frame() const { return _nextFrame ? _nextFrame - 1 : 0; }
+ uint frameCount() const { return _frames.size(); }
+ uint frameRate() const { return _frameRate; }
+ uint rescuedCount() const { return _rescuedCount; }
+ uint rescuedSpecies() const { return _rescuedSpecies; }
+ const Common::Array<MortimerSound> &sounds() const { return _sounds; }
+ const Common::String &errorMessage() const { return _error; }
+
+private:
+ struct Frame {
+ uint32 offset, size;
+ };
+ struct Object {
+ uint32 offset, size;
+ uint16 skipA, skipB;
+ };
+ struct Target {
+ uint16 id, toggle, remapToggle;
+ uint rescuedAt;
+ Common::Rect rect;
+ bool active, rescued;
+ };
+ struct Obstacle {
+ Common::Point corners[4];
+ };
+
+ bool fail(const char *message);
+ void handleAction(const byte *data, uint32 size);
+ void render();
+ Target *findTarget(uint16 id);
+
+ Common::Array<byte> _data, _background, _pixels, _scratch;
+ Common::Array<Frame> _frames;
+ Common::Array<Object> _objects;
+ Common::Array<Target> _targets;
+ Common::Array<Obstacle> _obstacles;
+ Common::Array<MortimerSound> _sounds;
+ SmushDeltaBlocksDecoder *_decoder;
+ Common::String _error;
+ byte _palette[768], _remap[256];
+ bool _toggles[256];
+ uint _nextFrame, _frameRate, _rescuedCount, _rescuedSpecies;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
new file mode 100644
index 00000000000..eef05d64aef
--- /dev/null
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -0,0 +1,279 @@
+/* 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 "scumm/insane/mortimer/mortimer.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "common/events.h"
+#include "common/file.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "engines/util.h"
+#include "graphics/cursorman.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/paletteman.h"
+
+namespace Scumm {
+
+MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
+ _mouse(320, 180), _view(32, 24), _nextFrameTime(0), _pauseTime(0),
+ _health(100), _lastDamageFrame(-10), _finished(false) {
+}
+
+MortimerEngine::~MortimerEngine() {
+ stopSounds();
+ _screen.free();
+}
+
+bool MortimerEngine::hasFeature(EngineFeature feature) const {
+ return feature == kSupportsReturnToLauncher;
+}
+
+bool MortimerEngine::loadResources() {
+ const char *flightName = "RESOURCE/DEMO/DEMO_S1.SAN";
+ Common::File file;
+ if (!file.open(Common::Path(flightName))) {
+ _loadError = Common::String::format("Missing %s", flightName);
+ return false;
+ }
+ if (!_flight.load(file)) {
+ _loadError = _flight.errorMessage();
+ return false;
+ }
+ file.close();
+
+ struct SpriteBank {
+ const char *name;
+ Common::Array<MortimerSprite> *sprites;
+ uint minFrames;
+ };
+ const SpriteBank banks[] = {
+ {"RESOURCE/GENERAL/COCKPT_H.NUT", &_cockpit, 3},
+ {"RESOURCE/GENERAL/DASH_H.NUT", &_dashboard, 36},
+ {"RESOURCE/GENERAL/RETICLEH.NUT", &_reticles, 1},
+ {"RESOURCE/SAVANNA/S_ICON_H.NUT", &_icons, 14}
+ };
+ for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
+ if (!file.open(Common::Path(banks[i].name)) || !loadMortimerSprites(file, *banks[i].sprites) || banks[i].sprites->size() < banks[i].minFrames) {
+ _loadError = Common::String::format("Missing or invalid %s", banks[i].name);
+ return false;
+ }
+ file.close();
+ }
+ struct Effect {
+ const char *name;
+ MortimerSound *sound;
+ };
+ const Effect effects[] = {
+ {"RESOURCE/GENERAL/SFX/REVITAL2.SAD", &_scopeSound},
+ {"RESOURCE/GENERAL/SFX/SCOPHIT3.SAD", &_rescueSound},
+ {"RESOURCE/GENERAL/SFX/SHELHIT1.SAD", &_collisionSound}
+ };
+ for (uint i = 0; i < ARRAYSIZE(effects); ++i) {
+ if (!file.open(Common::Path(effects[i].name)) || !loadMortimerSound(file, *effects[i].sound)) {
+ _loadError = Common::String::format("Missing or invalid %s", effects[i].name);
+ return false;
+ }
+ file.close();
+ }
+ _speechHandles.resize(_flight.sounds().size());
+ return true;
+}
+
+void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type) {
+ _mixer->stopHandle(handle);
+ // The engine owns the samples until all its handles have been stopped.
+ Audio::AudioStream *stream = Audio::makeRawStream(sound.samples.begin(), sound.samples.size(), sound.rate,
+ Audio::FLAG_UNSIGNED, DisposeAfterUse::NO);
+ _mixer->playStream(type, &handle, stream, -1, sound.volume, sound.balance);
+}
+
+void MortimerEngine::playSpeech() {
+ for (uint i = 0; i < _flight.sounds().size(); ++i)
+ if (_flight.sounds()[i].frame == _flight.frame())
+ playSound(_flight.sounds()[i], _speechHandles[i], Audio::Mixer::kSpeechSoundType);
+}
+
+void MortimerEngine::stopSounds() {
+ for (uint i = 0; i < _speechHandles.size(); ++i)
+ _mixer->stopHandle(_speechHandles[i]);
+ _mixer->stopHandle(_scopeHandle);
+ _mixer->stopHandle(_rescueHandle);
+ _mixer->stopHandle(_collisionHandle);
+}
+
+void MortimerEngine::restart() {
+ stopSounds();
+ _flight.reset();
+ _flight.advance();
+ _health = 100;
+ _lastDamageFrame = -int(_flight.frameRate());
+ _finished = false;
+ setMouse(Common::Point(320, 180));
+ _system->warpMouse(_mouse.x, _mouse.y);
+ _nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
+ playSpeech();
+}
+
+void MortimerEngine::setMouse(const Common::Point &position) {
+ _mouse.x = CLIP<int>(position.x, 0, 639);
+ _mouse.y = CLIP<int>(position.y, 0, 479);
+ // A bounded view into the overscanned movie supplies steering. Exact
+ // acceleration, collision damage and scope behaviour remain POC rules.
+ _view.x = CLIP<int>(32 + (_mouse.x - 320) / 8, 0, MortimerFlight::kWidth - 320);
+ _view.y = CLIP<int>(24 + (_mouse.y - 180) / 4, 0, MortimerFlight::kHeight - 240);
+}
+
+void MortimerEngine::pauseEngineIntern(bool pause) {
+ Engine::pauseEngineIntern(pause);
+ if (pause)
+ _pauseTime = _system->getMillis();
+ else
+ _nextFrameTime += _system->getMillis() - _pauseTime;
+}
+
+Common::Error MortimerEngine::run() {
+ if (!loadResources())
+ return Common::Error(Common::kReadingFailed, _loadError);
+ initGraphics(640, 480);
+ _screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
+ _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
+ syncSoundSettings();
+ const bool oldCursor = CursorMan.showMouse(false);
+ restart();
+ PauseToken pause;
+ bool exit = false;
+ while (!shouldQuit() && !exit) {
+ Common::Event event;
+ while (_eventMan->pollEvent(event)) {
+ if (event.type == Common::EVENT_MOUSEMOVE) {
+ if (!isPaused())
+ setMouse(event.mouse);
+ } else if (event.type == Common::EVENT_LBUTTONDOWN && !isPaused() && !_finished && _health > 0) {
+ setMouse(event.mouse);
+ if (_mouse.x >= 54 && _mouse.x < 567 && _mouse.y >= 37 && _mouse.y < 320) {
+ playSound(_scopeSound, _scopeHandle, Audio::Mixer::kSFXSoundType);
+ if (_flight.rescue(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2)))
+ playSound(_rescueSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ }
+ } else if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ case Common::KEYCODE_q:
+ exit = true;
+ break;
+ case Common::KEYCODE_SPACE:
+ if (pause.isActive())
+ pause.clear();
+ else
+ pause = pauseEngine();
+ break;
+ case Common::KEYCODE_r:
+ if (pause.isActive())
+ pause.clear();
+ restart();
+ break;
+ case Common::KEYCODE_F5:
+ openMainMenuDialog();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ if (!isPaused() && !_finished && _health > 0 && int32(_system->getMillis() - _nextFrameTime) >= 0) {
+ if (!_flight.advance()) {
+ _finished = true;
+ stopSounds();
+ } else {
+ playSpeech();
+ if (int(_flight.frame()) - _lastDamageFrame >= int(_flight.frameRate()) &&
+ _flight.hitsObstacle(Common::Point(_view.x + 160, _view.y + 120))) {
+ _health = MAX(0, _health - 10);
+ _lastDamageFrame = _flight.frame();
+ playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
+ }
+ }
+ _nextFrameTime += 1000 / _flight.frameRate();
+ // Avoid racing through queued frames following a slow backend call.
+ if (int32(_system->getMillis() - _nextFrameTime) > 250)
+ _nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
+ }
+ draw();
+ _system->delayMillis(10);
+ }
+ stopSounds();
+ CursorMan.showMouse(oldCursor);
+ return Common::kNoError;
+}
+
+void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y) {
+ Common::Rect clip(x, y, x + sprite.width, y + sprite.height);
+ clip.clip(Common::Rect(_screen.w, _screen.h));
+ if (clip.isEmpty())
+ return;
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ const byte *src = sprite.pixels.begin() + (row - y) * sprite.width + clip.left - x;
+ byte *dst = (byte *)_screen.getBasePtr(clip.left, row);
+ for (int col = clip.left; col < clip.right; ++col, ++src, ++dst)
+ if (*src)
+ *dst = *src;
+ }
+}
+
+void MortimerEngine::draw() {
+ for (int y = 0; y < 480; ++y) {
+ const byte *src = _flight.pixels() + (_view.y + y / 2) * MortimerFlight::kWidth + _view.x;
+ byte *dst = (byte *)_screen.getBasePtr(0, y);
+ for (int x = 0; x < 640; ++x)
+ dst[x] = src[x / 2];
+ }
+ drawSprite(_cockpit[0], 0, _cockpit[0].y);
+ drawSprite(_cockpit[1], 640 - _cockpit[1].width, _cockpit[1].y);
+ drawSprite(_cockpit[2], 0, 0);
+ drawSprite(_dashboard[35], 0, 320);
+ const int iconCenterY[] = {384, 379, 377, 376, 378, 380, 384};
+ for (uint species = 0; species < 7; ++species) {
+ if (_flight.rescuedSpecies() & (1 << species)) {
+ const MortimerSprite &icon = _icons[species * 2];
+ drawSprite(icon, 176 + species * 45 - icon.width / 2, iconCenterY[species] - icon.height / 2);
+ }
+ }
+ if (_mouse.y < 320)
+ drawSprite(_reticles[0], _mouse.x - _reticles[0].width / 2, _mouse.y - _reticles[0].height / 2);
+
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+ _screen.fillRect(Common::Rect(0, 449, 640, 480), 10);
+ font->drawString(&_screen, Common::String::format("Animals rescued: %u Shell: %d%% Flight: %u / %u",
+ _flight.rescuedCount(), _health, _flight.frame() + 1, _flight.frameCount()), 8, 451, 624, 20);
+ font->drawString(&_screen, "Mouse: steer/aim Click: rescue Space: pause R: restart Esc: exit", 8, 467, 624, 20);
+ if (_finished || !_health || isPaused()) {
+ _screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
+ const char *message = isPaused() ? "Paused - Space to continue" :
+ _finished ? "Flight complete - R to restart" : "Shell exhausted - R to restart";
+ font->drawString(&_screen, message, 90, 155, 460, 20, Graphics::kTextAlignCenter);
+ }
+ _system->copyRectToScreen(_screen.getPixels(), _screen.pitch, 0, 0, 640, 480);
+ _system->updateScreen();
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
new file mode 100644
index 00000000000..eed9d377868
--- /dev/null
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -0,0 +1,68 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_MORTIMER_H
+#define SCUMM_INSANE_MORTIMER_MORTIMER_H
+
+#include "engines/engine.h"
+#include "audio/mixer.h"
+#include "graphics/surface.h"
+#include "scumm/insane/mortimer/flight.h"
+
+namespace Scumm {
+
+// Registered by the SCUMM plugin, but deliberately independent of ScummEngine
+// and both Rebel Assault engines while the Mortimer flight POC is developed.
+class MortimerEngine : public Engine {
+public:
+ explicit MortimerEngine(OSystem *system);
+ ~MortimerEngine() override;
+ Common::Error run() override;
+ bool hasFeature(EngineFeature feature) const override;
+
+protected:
+ void pauseEngineIntern(bool pause) override;
+
+private:
+ bool loadResources();
+ void restart();
+ void setMouse(const Common::Point &position);
+ void draw();
+ void drawSprite(const MortimerSprite &sprite, int x, int y);
+ void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type);
+ void playSpeech();
+ void stopSounds();
+
+ MortimerFlight _flight;
+ Graphics::Surface _screen;
+ Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons;
+ MortimerSound _scopeSound, _rescueSound, _collisionSound;
+ Common::Array<Audio::SoundHandle> _speechHandles;
+ Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle;
+ Common::String _loadError;
+ Common::Point _mouse, _view;
+ uint32 _nextFrameTime, _pauseTime;
+ int _health, _lastDamageFrame;
+ bool _finished;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index d6bf12f63d6..5f474054257 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -48,6 +48,10 @@
#include "scumm/file.h"
#include "scumm/file_nes.h"
+#ifdef ENABLE_SCUMM_7_8
+#include "scumm/insane/mortimer/mortimer.h"
+#endif
+
#if defined(ENABLE_REBEL2_PSX) && !defined(ENABLE_SCUMM_7_8)
#error "SCUMM v7 & v8 games must be enabled for Rebel Assault II PlayStation. Specify --enable-engine=scumm-7-8,rebel2-psx"
#endif
@@ -467,6 +471,12 @@ Common::Error ScummMetaEngine::createInstance(OSystem *syst, Engine **engine,
// Finally, we have massaged the GameDescriptor to our satisfaction, and can
// instantiate the appropriate game engine. Hooray!
+#ifdef ENABLE_SCUMM_7_8
+ if (res.game.id == GID_MORTIMER) {
+ *engine = new MortimerEngine(syst);
+ return Common::kNoError;
+ }
+#endif
switch (res.game.version) {
case 0:
*engine = new ScummEngine_v0(syst, res);
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b0f7e3eb3aa..6c37ae4de2f 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -147,6 +147,8 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
+ insane/mortimer/flight.o \
+ insane/mortimer/mortimer.o \
insane/rebel/rebel_audio.o \
insane/rebel/rebel_gamepad.o \
insane/rebel/rebel_touch.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index f2a5530f9d0..5db6e273c88 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
/*
- This file was generated by the md5table tool on Sun Sep 27 19:37:35 2026
+ This file was generated by the md5table tool on Sun Sep 27 20:08:36 2026
DO NOT EDIT MANUALLY!
*/
@@ -196,6 +196,7 @@ static const MD5Table md5table[] = {
{ "330f631502e381a4e199a3f7cb483c20", "indy3", "EGA", "EGA", -1, Common::DE_DEU, Common::kPlatformAmiga },
{ "33e989f85da700e2014d00f345cab3d7", "puttrace", "HE 98.5", "", 32754, Common::FR_FRA, Common::kPlatformWindows },
{ "3433be9866ca4261b2d5d25374e3f243", "monkey", "VGA", "", 8347, Common::FR_FRA, Common::kPlatformAmiga },
+ { "3450a722d29c150a4bdac5ba9b5f3b46", "mortimer", "", "Demo (flight POC)", 290304, Common::EN_ANY, Common::kPlatformWindows },
{ "3486ede0f904789267d4bcc5537a46d4", "puttzoo", "", "Demo", 14337, Common::EN_ANY, Common::kPlatformMacintosh },
{ "35183fc18b1e04cdc9070078260b2c03", "zak", "V1", "", 1914, Common::EN_ANY, Common::kPlatformC64 },
{ "356fb5f680b68251333016175935d126", "BluesABCTime", "HE CUP", "Preview", 4133436, Common::UNK_LANG, Common::kPlatformUnknown },
Commit: 82d9ef19a4009c4525a06a9e80255f33d8b5af80
https://github.com/scummvm/scummvm/commit/82d9ef19a4009c4525a06a9e80255f33d8b5af80
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: improved flight progression code and removed POC naming
Changed paths:
devtools/scumm-md5.txt
engines/scumm/detection_tables.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/scumm-md5.h
diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 7a0e29c97f3..98fbe672899 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -450,7 +450,7 @@ ft Full Throttle
8ead2c01c48c2e3d15d6a89b86cd84ab 19697 he DOS - - - Hebrew fan translation
mortimer Mortimer and the Riddles of the Medallion
- 3450a722d29c150a4bdac5ba9b5f3b46 290304 en Windows - Demo (flight POC) Windows demo launcher; first Savanna flight only -
+ 3450a722d29c150a4bdac5ba9b5f3b46 290304 en Windows - Demo Windows demo launcher -
rebel1 Star Wars: Rebel Assault
0ca9c07185b4969764eaf3ecd0de06e9 111530 en DOS Demo v0.67 Demo v0.67 Non-interactive demo -
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index b6bd4f14739..d94ce23f1bb 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -251,7 +251,7 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
- {"mortimer", 0, "flight-poc", GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+ {"mortimer", 0, 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index f0cddef206c..cff31c40452 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -93,7 +93,7 @@ bool validRows(const byte *src, uint32 size, int height, bool remap) {
}
// Codec37's existing decoder has no input-length argument. Validate the two
-// encodings actually present in DEMO_S1 before passing any data to it.
+// encodings present in the demo flights before passing any data to it.
bool validBackground(const byte *src, uint32 size) {
if (size < 16 || src[1] != 1 || src[12] != 4)
return false;
@@ -143,7 +143,7 @@ bool validObject(const byte *data, uint32 size, bool sprite = false) {
if (codec == 37)
return width == MortimerFlight::kWidth && height == MortimerFlight::kHeight &&
READ_LE_UINT32(data + 2) == 0 && validBackground(data + 14, size - 14);
- if (codec != 3 && codec != 23)
+ if (codec != 1 && codec != 3 && codec != 23)
return false;
uint32 offset = 14;
if (codec == 23) {
@@ -156,6 +156,22 @@ bool validObject(const byte *data, uint32 size, bool sprite = false) {
return offset <= size && validRows(data + offset, size - offset, height, codec == 23);
}
+bool validPalette(const byte *data, uint32 size) {
+ return (size == 2308 && READ_BE_UINT32(data) == 2) ||
+ (size == 6 && READ_BE_UINT32(data) == 1);
+}
+
+bool validTransition(const byte *data, uint32 size) {
+ // IACT 8/46 contains a length-prefixed filename followed by CR/LF/EOF.
+ if (size < 22 || READ_LE_UINT16(data + 14) != size - 18 ||
+ data[size - 3] != '\r' || data[size - 2] != '\n' || data[size - 1] != 0x1a)
+ return false;
+ for (uint i = 18; i < size - 3; ++i)
+ if (data[i] < 32 || data[i] > 126)
+ return false;
+ return true;
+}
+
bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
if (!isContainer(data, MKTAG('S', 'A', 'U', 'D')))
return false;
@@ -229,7 +245,7 @@ bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound)
MortimerFlight::MortimerFlight() : _decoder(nullptr), _nextFrame(0), _frameRate(10), _rescuedCount(0), _rescuedSpecies(0) {
_background.resize(kWidth * kHeight);
_pixels.resize(kWidth * kHeight);
- memset(_palette, 0, sizeof(_palette));
+ memset(_initialPalette, 0, sizeof(_initialPalette));
reset();
}
@@ -271,7 +287,8 @@ bool MortimerFlight::load(Common::SeekableReadStream &stream) {
_frameRate = READ_LE_UINT32(src + 774);
if (!expectedFrames || _frameRate != 10)
return fail("Unsupported Mortimer flight timing");
- memcpy(_palette, src + 6, sizeof(_palette));
+ memcpy(_initialPalette, src + 6, sizeof(_initialPalette));
+ memcpy(_palette, _initialPalette, sizeof(_palette));
} else if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
if (!expectedFrames)
return fail("Mortimer flight has no header");
@@ -300,6 +317,13 @@ bool MortimerFlight::load(Common::SeekableReadStream &stream) {
if ((code >= 2 && code <= 4 && (sub.size < (code == 4 ? 18U : 12U) || READ_LE_UINT16(body + 8) >= 256)) ||
(code == 5 && sub.size < 28))
return fail("Invalid Mortimer flight action");
+ if (code == 2 && READ_LE_UINT16(body + 4) == 3 && READ_LE_UINT16(body + 10) >= 256)
+ return fail("Invalid Mortimer conditional object");
+ if (code == 8 && READ_LE_UINT16(body + 2) == 46 && !validTransition(body, sub.size))
+ return fail("Invalid Mortimer flight transition");
+ } else if (sub.tag == MKTAG('X', 'P', 'A', 'L')) {
+ if (!validPalette(body, sub.size))
+ return fail("Invalid Mortimer palette change");
} else if (sub.tag == MKTAG('P', 'S', 'A', 'D')) {
if (sub.size < 10)
return fail("Truncated Mortimer speech block");
@@ -312,6 +336,7 @@ bool MortimerFlight::load(Common::SeekableReadStream &stream) {
track.id = id;
track.nextBlock = 0;
track.sound.frame = _frames.size();
+ track.sound.music = (READ_LE_UINT16(body + 6) & 64) != 0;
track.sound.volume = MIN(255, body[8] * 2);
track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
tracks.push_back(track);
@@ -324,8 +349,9 @@ bool MortimerFlight::load(Common::SeekableReadStream &stream) {
memcpy(bytes.begin() + oldSize, body + 10, sub.size - 10);
}
}
- if (backgrounds != 1)
- return fail("Mortimer flight frame needs one background");
+ // The final Big Tree frame contains an opaque RLE image only.
+ if (backgrounds > 1 || (_frames.empty() && backgrounds != 1))
+ return fail("Invalid Mortimer flight background");
_frames.push_back({chunk.offset, chunk.size});
}
}
@@ -343,6 +369,11 @@ void MortimerFlight::reset() {
delete _decoder;
_decoder = new SmushDeltaBlocksDecoder(kWidth, kHeight);
_nextFrame = _rescuedCount = _rescuedSpecies = 0;
+ _nextFlight.clear();
+ _weapon = kRevitascope;
+ memcpy(_palette, _initialPalette, sizeof(_palette));
+ memset(_paletteDelta, 0, sizeof(_paletteDelta));
+ memset(_shiftedPalette, 0, sizeof(_shiftedPalette));
_targets.clear();
_objects.clear();
_obstacles.clear();
@@ -377,7 +408,7 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
target->remapToggle = toggle;
// Briefly show the rescued animal's silhouette before removing it
// from the scene. Flash duration is POC tuning.
- _toggles[toggle] = !target || !target->rescued || frame() - target->rescuedAt >= 5;
+ _toggles[toggle] = !target || !target->hit || frame() - target->rescuedAt >= 5;
return;
}
Target *target = findTarget(id);
@@ -387,14 +418,20 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
fresh.toggle = toggle;
fresh.remapToggle = 0xffff;
fresh.rescuedAt = 0;
- fresh.active = fresh.rescued = false;
+ fresh.active = fresh.hit = false;
+ // Salt belongs to the hornet whose ID is 100 lower. Destroying a
+ // hornet also disables its subsequent salt objects (SHELL 407c5f).
+ if (id >= 800 && id < 900) {
+ const Target *hornet = findTarget(id - 100);
+ fresh.hit = hornet && hornet->hit;
+ }
_targets.push_back(fresh);
target = &_targets.back();
}
if (!target)
return;
target->toggle = toggle;
- _toggles[toggle] = target->rescued;
+ _toggles[toggle] = target->hit;
if (code == 4) {
const int x = READ_LE_INT16(data + 10), y = READ_LE_INT16(data + 12);
const int w = READ_LE_INT16(data + 14), h = READ_LE_INT16(data + 16);
@@ -404,14 +441,37 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
} else if (code == 3) {
target->active = false;
}
+ } else if (code == 2 && mode == 3) {
+ _toggles[READ_LE_UINT16(data + 8)] = !_toggles[READ_LE_UINT16(data + 10)];
} else if (code == 5 && flags == 14 && size >= 28) {
Obstacle obstacle;
for (uint i = 0; i < 4; ++i)
obstacle.corners[i] = Common::Point(READ_LE_INT16(data + 12 + i * 4), READ_LE_INT16(data + 14 + i * 4));
_obstacles.push_back(obstacle);
+ } else if (code == 6 && (flags == 9 || flags == 10)) {
+ // SHELL 407c5f calls 40b844/40b7ce at the scripted weapon changes.
+ _weapon = flags == 9 ? kSlimeGun : kRevitascope;
+ } else if (code == 8 && flags == 46) {
+ _nextFlight = Common::String((const char *)data + 18, size - 21);
+ for (uint i = 0; i < _nextFlight.size(); ++i)
+ if (_nextFlight[i] == '\\')
+ _nextFlight.setChar('/', i);
+ }
+}
+
+void MortimerFlight::handlePalette(const byte *data, uint32 size) {
+ if (size == 2308) {
+ for (uint i = 0; i < 768; ++i) {
+ _shiftedPalette[i] = _palette[i] << 7;
+ _paletteDelta[i] = READ_LE_INT16(data + 4 + i * 2);
+ }
+ memcpy(_palette, data + 1540, sizeof(_palette));
+ } else {
+ for (uint i = 0; i < 768; ++i) {
+ _shiftedPalette[i] += _paletteDelta[i];
+ _palette[i] = CLIP<int32>(_shiftedPalette[i] >> 7, 0, 255);
+ }
}
- // Flight transitions (8/46), prefetches and shell/interface callbacks are
- // deliberately outside this single-segment POC.
}
bool MortimerFlight::advance() {
@@ -436,6 +496,8 @@ bool MortimerFlight::advance() {
}
if (chunk.tag == MKTAG('I', 'A', 'C', 'T')) {
handleAction(src, chunk.size);
+ } else if (chunk.tag == MKTAG('X', 'P', 'A', 'L')) {
+ handlePalette(src, chunk.size);
} else if (chunk.tag == MKTAG('F', 'O', 'B', 'J')) {
if (src[0] == 37) {
_decoder->decode(_background.begin(), src + 14);
@@ -483,34 +545,50 @@ void MortimerFlight::render() {
byte *dest = _pixels.begin() + row * kWidth + clip.left;
for (int col = 0; col < clip.width(); ++col) {
// Mortimer treats zero as transparent in literal runs as well.
- if (src[0] == 23 || line[col])
+ if (src[0] == 1 || src[0] == 23 || line[col])
dest[col] = line[col];
}
}
}
}
-bool MortimerFlight::rescue(const Common::Point &point) {
+MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
// Later objects are drawn on top of earlier ones.
for (int i = int(_targets.size()) - 1; i >= 0; --i) {
Target &target = _targets[i];
- if (!target.active || target.rescued || !target.rect.contains(point))
+ if (!target.active || target.hit || !target.rect.contains(point))
+ continue;
+ const uint species = target.id / 100;
+ if (!target.id || species > 9 || species == 8 ||
+ (species < 7 && _weapon != kRevitascope) || (species == 7 && _weapon != kSlimeGun))
continue;
- target.rescued = true;
+ target.hit = true;
target.rescuedAt = frame();
_toggles[target.toggle] = true;
if (target.remapToggle != 0xffff)
_toggles[target.remapToggle] = false;
- ++_rescuedCount;
- if (target.id / 100 < 7)
- _rescuedSpecies |= 1 << (target.id / 100);
+ if (species < 7) {
+ ++_rescuedCount;
+ _rescuedSpecies |= 1 << species;
+ } else if (species == 7) {
+ Target *salt = findTarget(target.id + 100);
+ if (salt) {
+ salt->hit = true;
+ _toggles[salt->toggle] = true;
+ }
+ }
render();
- return true;
+ return species == 9 ? kShellRestored : species == 7 ? kHornetHit : kAnimalRescued;
}
- return false;
+ return kMiss;
}
bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
+ for (uint i = 0; i < _targets.size(); ++i) {
+ const Target &target = _targets[i];
+ if (target.id >= 800 && target.id < 900 && target.active && !target.hit && target.rect.contains(point))
+ return true;
+ }
for (uint n = 0; n < _obstacles.size(); ++n) {
const Obstacle &obstacle = _obstacles[n];
bool inside = false;
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 23a25475ec2..5a9abb0e968 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -43,25 +43,28 @@ struct MortimerSound {
uint rate = 11025;
byte volume = 255;
int8 balance = 0;
+ bool music = false;
Common::Array<byte> samples;
};
bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites);
bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
-// The first Windows demo flight only. No ScummEngine, InsaneRebel or SmushPlayer
+// The Windows demo flights. No ScummEngine, InsaneRebel or SmushPlayer
// state is used here; only the existing, stateless RLE helpers and a private
// codec37 decoder are shared with the other SMUSH games.
class MortimerFlight {
public:
enum { kWidth = 384, kHeight = 288 };
+ enum Weapon { kRevitascope, kSlimeGun };
+ enum ShotResult { kMiss, kAnimalRescued, kHornetHit, kShellRestored };
MortimerFlight();
~MortimerFlight();
bool load(Common::SeekableReadStream &stream);
void reset();
bool advance();
- bool rescue(const Common::Point &point);
+ ShotResult shoot(const Common::Point &point);
bool hitsObstacle(const Common::Point &point) const;
const byte *pixels() const { return _pixels.begin(); }
@@ -71,6 +74,8 @@ public:
uint frameRate() const { return _frameRate; }
uint rescuedCount() const { return _rescuedCount; }
uint rescuedSpecies() const { return _rescuedSpecies; }
+ Weapon weapon() const { return _weapon; }
+ const Common::String &nextFlight() const { return _nextFlight; }
const Common::Array<MortimerSound> &sounds() const { return _sounds; }
const Common::String &errorMessage() const { return _error; }
@@ -86,7 +91,7 @@ private:
uint16 id, toggle, remapToggle;
uint rescuedAt;
Common::Rect rect;
- bool active, rescued;
+ bool active, hit;
};
struct Obstacle {
Common::Point corners[4];
@@ -94,6 +99,7 @@ private:
bool fail(const char *message);
void handleAction(const byte *data, uint32 size);
+ void handlePalette(const byte *data, uint32 size);
void render();
Target *findTarget(uint16 id);
@@ -104,9 +110,12 @@ private:
Common::Array<Obstacle> _obstacles;
Common::Array<MortimerSound> _sounds;
SmushDeltaBlocksDecoder *_decoder;
- Common::String _error;
- byte _palette[768], _remap[256];
+ Common::String _error, _nextFlight;
+ byte _palette[768], _initialPalette[768], _remap[256];
+ int16 _paletteDelta[768];
+ int32 _shiftedPalette[768];
bool _toggles[256];
+ Weapon _weapon;
uint _nextFrame, _frameRate, _rescuedCount, _rescuedSpecies;
};
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index eef05d64aef..3a6dd97fe9e 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -25,18 +25,26 @@
#include "common/events.h"
#include "common/file.h"
#include "common/system.h"
+#include "common/translation.h"
#include "common/util.h"
#include "engines/util.h"
#include "graphics/cursorman.h"
#include "graphics/font.h"
#include "graphics/fontman.h"
#include "graphics/paletteman.h"
+#include "gui/message.h"
namespace Scumm {
+static const char *const kDemoFlights[] = {
+ "RESOURCE/DEMO/DEMO_S1.SAN",
+ "RESOURCE/DEMO/DEMO_PL.SAN",
+ "RESOURCE/DEMO/DEMO_BT.SAN"
+};
+
MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
_mouse(320, 180), _view(32, 24), _nextFrameTime(0), _pauseTime(0),
- _health(100), _lastDamageFrame(-10), _finished(false) {
+ _segment(0), _completedRescues(0), _completedSpecies(0), _shellShocks(0), _finished(false) {
}
MortimerEngine::~MortimerEngine() {
@@ -49,17 +57,7 @@ bool MortimerEngine::hasFeature(EngineFeature feature) const {
}
bool MortimerEngine::loadResources() {
- const char *flightName = "RESOURCE/DEMO/DEMO_S1.SAN";
Common::File file;
- if (!file.open(Common::Path(flightName))) {
- _loadError = Common::String::format("Missing %s", flightName);
- return false;
- }
- if (!_flight.load(file)) {
- _loadError = _flight.errorMessage();
- return false;
- }
- file.close();
struct SpriteBank {
const char *name;
@@ -69,7 +67,7 @@ bool MortimerEngine::loadResources() {
const SpriteBank banks[] = {
{"RESOURCE/GENERAL/COCKPT_H.NUT", &_cockpit, 3},
{"RESOURCE/GENERAL/DASH_H.NUT", &_dashboard, 36},
- {"RESOURCE/GENERAL/RETICLEH.NUT", &_reticles, 1},
+ {"RESOURCE/GENERAL/RETICLEH.NUT", &_reticles, 2},
{"RESOURCE/SAVANNA/S_ICON_H.NUT", &_icons, 14}
};
for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
@@ -86,7 +84,9 @@ bool MortimerEngine::loadResources() {
const Effect effects[] = {
{"RESOURCE/GENERAL/SFX/REVITAL2.SAD", &_scopeSound},
{"RESOURCE/GENERAL/SFX/SCOPHIT3.SAD", &_rescueSound},
- {"RESOURCE/GENERAL/SFX/SHELHIT1.SAD", &_collisionSound}
+ {"RESOURCE/GENERAL/SFX/SHELHIT1.SAD", &_collisionSound},
+ {"RESOURCE/GENERAL/SFX/SLMGUN2.SAD", &_slimeSound},
+ {"RESOURCE/GENERAL/SFX/MUCKHIT1.SAD", &_slimeHitSound}
};
for (uint i = 0; i < ARRAYSIZE(effects); ++i) {
if (!file.open(Common::Path(effects[i].name)) || !loadMortimerSound(file, *effects[i].sound)) {
@@ -95,7 +95,24 @@ bool MortimerEngine::loadResources() {
}
file.close();
}
- _speechHandles.resize(_flight.sounds().size());
+ return loadFlight(0);
+}
+
+bool MortimerEngine::loadFlight(uint segment) {
+ // The mixer borrows the samples owned by the flight being replaced.
+ stopSounds();
+ _flightHandles.clear();
+ Common::File file;
+ if (!file.open(Common::Path(kDemoFlights[segment]))) {
+ _loadError = Common::String::format("Missing %s", kDemoFlights[segment]);
+ return false;
+ }
+ if (!_flight.load(file)) {
+ _loadError = Common::String::format("%s: %s", kDemoFlights[segment], _flight.errorMessage().c_str());
+ return false;
+ }
+ _segment = segment;
+ _flightHandles.resize(_flight.sounds().size());
return true;
}
@@ -107,31 +124,62 @@ void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &h
_mixer->playStream(type, &handle, stream, -1, sound.volume, sound.balance);
}
-void MortimerEngine::playSpeech() {
+void MortimerEngine::playFlightSounds() {
for (uint i = 0; i < _flight.sounds().size(); ++i)
if (_flight.sounds()[i].frame == _flight.frame())
- playSound(_flight.sounds()[i], _speechHandles[i], Audio::Mixer::kSpeechSoundType);
+ playSound(_flight.sounds()[i], _flightHandles[i],
+ _flight.sounds()[i].music ? Audio::Mixer::kMusicSoundType : Audio::Mixer::kSpeechSoundType);
}
void MortimerEngine::stopSounds() {
- for (uint i = 0; i < _speechHandles.size(); ++i)
- _mixer->stopHandle(_speechHandles[i]);
+ for (uint i = 0; i < _flightHandles.size(); ++i)
+ _mixer->stopHandle(_flightHandles[i]);
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
_mixer->stopHandle(_collisionHandle);
}
-void MortimerEngine::restart() {
+bool MortimerEngine::startFlight() {
stopSounds();
_flight.reset();
- _flight.advance();
- _health = 100;
- _lastDamageFrame = -int(_flight.frameRate());
+ if (!_flight.advance()) {
+ _loadError = _flight.errorMessage();
+ return false;
+ }
_finished = false;
setMouse(Common::Point(320, 180));
_system->warpMouse(_mouse.x, _mouse.y);
+ _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
_nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
- playSpeech();
+ playFlightSounds();
+ return true;
+}
+
+bool MortimerEngine::restart(bool all) {
+ _shellShocks = 0;
+ if (all) {
+ _completedRescues = _completedSpecies = 0;
+ if (!loadFlight(0))
+ return false;
+ }
+ return startFlight();
+}
+
+bool MortimerEngine::nextFlight(bool skip) {
+ stopSounds();
+ if (_segment + 1 == ARRAYSIZE(kDemoFlights)) {
+ _finished = true;
+ return true;
+ }
+ // Only follow the demo's two flight branches, never an interview or a
+ // full-game resource named by another SAN file.
+ if (!skip && !_flight.nextFlight().equalsIgnoreCase(kDemoFlights[_segment + 1])) {
+ _loadError = Common::String::format("Unexpected flight transition in %s", kDemoFlights[_segment]);
+ return false;
+ }
+ _completedRescues += _flight.rescuedCount();
+ _completedSpecies |= _flight.rescuedSpecies();
+ return loadFlight(_segment + 1) && startFlight();
}
void MortimerEngine::setMouse(const Common::Point &position) {
@@ -156,28 +204,37 @@ Common::Error MortimerEngine::run() {
return Common::Error(Common::kReadingFailed, _loadError);
initGraphics(640, 480);
_screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
- _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
syncSoundSettings();
const bool oldCursor = CursorMan.showMouse(false);
- restart();
PauseToken pause;
- bool exit = false;
+ bool exit = !restart();
while (!shouldQuit() && !exit) {
Common::Event event;
while (_eventMan->pollEvent(event)) {
if (event.type == Common::EVENT_MOUSEMOVE) {
if (!isPaused())
setMouse(event.mouse);
- } else if (event.type == Common::EVENT_LBUTTONDOWN && !isPaused() && !_finished && _health > 0) {
+ } else if (event.type == Common::EVENT_LBUTTONDOWN && !isPaused() && !_finished) {
setMouse(event.mouse);
if (_mouse.x >= 54 && _mouse.x < 567 && _mouse.y >= 37 && _mouse.y < 320) {
- playSound(_scopeSound, _scopeHandle, Audio::Mixer::kSFXSoundType);
- if (_flight.rescue(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2)))
+ playSound(_flight.weapon() == MortimerFlight::kSlimeGun ? _slimeSound : _scopeSound,
+ _scopeHandle, Audio::Mixer::kSFXSoundType);
+ const MortimerFlight::ShotResult result = _flight.shoot(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2));
+ if (result == MortimerFlight::kAnimalRescued || result == MortimerFlight::kShellRestored)
playSound(_rescueSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ else if (result == MortimerFlight::kHornetHit)
+ playSound(_slimeHitSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ if (result == MortimerFlight::kShellRestored)
+ _shellShocks = 0;
}
} else if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat) {
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
+ if (pause.isActive())
+ pause.clear();
+ else
+ exit = !nextFlight(true);
+ break;
case Common::KEYCODE_q:
exit = true;
break;
@@ -190,7 +247,7 @@ Common::Error MortimerEngine::run() {
case Common::KEYCODE_r:
if (pause.isActive())
pause.clear();
- restart();
+ exit = !restart();
break;
case Common::KEYCODE_F5:
openMainMenuDialog();
@@ -200,30 +257,43 @@ Common::Error MortimerEngine::run() {
}
}
}
- if (!isPaused() && !_finished && _health > 0 && int32(_system->getMillis() - _nextFrameTime) >= 0) {
+ if (exit)
+ break;
+ if (!isPaused() && !_finished && int32(_system->getMillis() - _nextFrameTime) >= 0) {
+ _nextFrameTime += 1000 / _flight.frameRate();
if (!_flight.advance()) {
- _finished = true;
- stopSounds();
+ if (!_flight.errorMessage().empty()) {
+ _loadError = _flight.errorMessage();
+ break;
+ }
+ if (!nextFlight())
+ break;
} else {
- playSpeech();
- if (int(_flight.frame()) - _lastDamageFrame >= int(_flight.frameRate()) &&
- _flight.hitsObstacle(Common::Point(_view.x + 160, _view.y + 120))) {
- _health = MAX(0, _health - 10);
- _lastDamageFrame = _flight.frame();
+ playFlightSounds();
+ _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
+ if (_flight.hitsObstacle(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2))) {
+ ++_shellShocks;
playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
+ // SHELL 40be36 resets the flight after eighteen shell shocks.
+ if (_shellShocks >= 18 && !restart())
+ break;
}
}
- _nextFrameTime += 1000 / _flight.frameRate();
// Avoid racing through queued frames following a slow backend call.
if (int32(_system->getMillis() - _nextFrameTime) > 250)
_nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
}
draw();
+ if (_finished) {
+ GUI::MessageDialog dialog(_("Play again?"), _("Restart"), _("Quit"));
+ if (dialog.runModal() != GUI::kMessageOK || !restart(true))
+ break;
+ }
_system->delayMillis(10);
}
stopSounds();
CursorMan.showMouse(oldCursor);
- return Common::kNoError;
+ return _loadError.empty() ? Common::kNoError : Common::Error(Common::kReadingFailed, _loadError);
}
void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y) {
@@ -253,24 +323,23 @@ void MortimerEngine::draw() {
drawSprite(_dashboard[35], 0, 320);
const int iconCenterY[] = {384, 379, 377, 376, 378, 380, 384};
for (uint species = 0; species < 7; ++species) {
- if (_flight.rescuedSpecies() & (1 << species)) {
+ if ((_completedSpecies | _flight.rescuedSpecies()) & (1 << species)) {
const MortimerSprite &icon = _icons[species * 2];
drawSprite(icon, 176 + species * 45 - icon.width / 2, iconCenterY[species] - icon.height / 2);
}
}
- if (_mouse.y < 320)
- drawSprite(_reticles[0], _mouse.x - _reticles[0].width / 2, _mouse.y - _reticles[0].height / 2);
+ if (_mouse.y < 320) {
+ const MortimerSprite &reticle = _reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1];
+ drawSprite(reticle, _mouse.x - reticle.width / 2, _mouse.y - reticle.height / 2);
+ }
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
- _screen.fillRect(Common::Rect(0, 449, 640, 480), 10);
- font->drawString(&_screen, Common::String::format("Animals rescued: %u Shell: %d%% Flight: %u / %u",
- _flight.rescuedCount(), _health, _flight.frame() + 1, _flight.frameCount()), 8, 451, 624, 20);
- font->drawString(&_screen, "Mouse: steer/aim Click: rescue Space: pause R: restart Esc: exit", 8, 467, 624, 20);
- if (_finished || !_health || isPaused()) {
+ _screen.fillRect(Common::Rect(0, 449, 640, 465), 10);
+ font->drawString(&_screen, Common::String::format("Animals rescued: %u Shell shocks: %u / 18 Flight: %u / 3",
+ _completedRescues + _flight.rescuedCount(), _shellShocks, _segment + 1), 8, 451, 624, 20);
+ if (isPaused()) {
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
- const char *message = isPaused() ? "Paused - Space to continue" :
- _finished ? "Flight complete - R to restart" : "Shell exhausted - R to restart";
- font->drawString(&_screen, message, 90, 155, 460, 20, Graphics::kTextAlignCenter);
+ font->drawString(&_screen, "Paused - Space to continue", 90, 155, 460, 20, Graphics::kTextAlignCenter);
}
_system->copyRectToScreen(_screen.getPixels(), _screen.pitch, 0, 0, 640, 480);
_system->updateScreen();
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index eed9d377868..62cd5144535 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -29,7 +29,7 @@
namespace Scumm {
// Registered by the SCUMM plugin, but deliberately independent of ScummEngine
-// and both Rebel Assault engines while the Mortimer flight POC is developed.
+// and both Rebel Assault engines.
class MortimerEngine : public Engine {
public:
explicit MortimerEngine(OSystem *system);
@@ -42,24 +42,27 @@ protected:
private:
bool loadResources();
- void restart();
+ bool loadFlight(uint segment);
+ bool startFlight();
+ bool nextFlight(bool skip = false);
+ bool restart(bool all = false);
void setMouse(const Common::Point &position);
void draw();
void drawSprite(const MortimerSprite &sprite, int x, int y);
void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type);
- void playSpeech();
+ void playFlightSounds();
void stopSounds();
MortimerFlight _flight;
Graphics::Surface _screen;
Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons;
- MortimerSound _scopeSound, _rescueSound, _collisionSound;
- Common::Array<Audio::SoundHandle> _speechHandles;
+ MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound;
+ Common::Array<Audio::SoundHandle> _flightHandles;
Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle;
Common::String _loadError;
Common::Point _mouse, _view;
uint32 _nextFrameTime, _pauseTime;
- int _health, _lastDamageFrame;
+ uint _segment, _completedRescues, _completedSpecies, _shellShocks;
bool _finished;
};
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 5db6e273c88..f5365db9848 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
/*
- This file was generated by the md5table tool on Sun Sep 27 20:08:36 2026
+ This file was generated by the md5table tool on Wed Sep 30 06:55:18 2026
DO NOT EDIT MANUALLY!
*/
@@ -196,7 +196,7 @@ static const MD5Table md5table[] = {
{ "330f631502e381a4e199a3f7cb483c20", "indy3", "EGA", "EGA", -1, Common::DE_DEU, Common::kPlatformAmiga },
{ "33e989f85da700e2014d00f345cab3d7", "puttrace", "HE 98.5", "", 32754, Common::FR_FRA, Common::kPlatformWindows },
{ "3433be9866ca4261b2d5d25374e3f243", "monkey", "VGA", "", 8347, Common::FR_FRA, Common::kPlatformAmiga },
- { "3450a722d29c150a4bdac5ba9b5f3b46", "mortimer", "", "Demo (flight POC)", 290304, Common::EN_ANY, Common::kPlatformWindows },
+ { "3450a722d29c150a4bdac5ba9b5f3b46", "mortimer", "", "Demo", 290304, Common::EN_ANY, Common::kPlatformWindows },
{ "3486ede0f904789267d4bcc5537a46d4", "puttzoo", "", "Demo", 14337, Common::EN_ANY, Common::kPlatformMacintosh },
{ "35183fc18b1e04cdc9070078260b2c03", "zak", "V1", "", 1914, Common::EN_ANY, Common::kPlatformC64 },
{ "356fb5f680b68251333016175935d126", "BluesABCTime", "HE CUP", "Preview", 4133436, Common::UNK_LANG, Common::kPlatformUnknown },
Commit: 322730bb230ae9567141bd684163aa6ed1ac13f1
https://github.com/scummvm/scummvm/commit/322730bb230ae9567141bd684163aa6ed1ac13f1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: implemented UI details like shooting, status bars, targets among others
Changed paths:
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index cff31c40452..bf4b728ad80 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -406,9 +406,9 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
Target *target = findTarget(id - 1000);
if (target)
target->remapToggle = toggle;
- // Briefly show the rescued animal's silhouette before removing it
- // from the scene. Flash duration is POC tuning.
- _toggles[toggle] = !target || !target->hit || frame() - target->rescuedAt >= 5;
+ // Keep the remapped silhouette for the duration of its NUT effect.
+ const uint duration = target && target->hitWeapon == kSlimeGun ? 7 : 11;
+ _toggles[toggle] = !target || !target->hit || frame() - target->rescuedAt >= duration;
return;
}
Target *target = findTarget(id);
@@ -418,6 +418,7 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
fresh.toggle = toggle;
fresh.remapToggle = 0xffff;
fresh.rescuedAt = 0;
+ fresh.hitWeapon = kRevitascope;
fresh.active = fresh.hit = false;
// Salt belongs to the hornet whose ID is 100 lower. Destroying a
// hornet also disables its subsequent salt objects (SHELL 407c5f).
@@ -564,6 +565,7 @@ MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
continue;
target.hit = true;
target.rescuedAt = frame();
+ target.hitWeapon = _weapon;
_toggles[target.toggle] = true;
if (target.remapToggle != 0xffff)
_toggles[target.remapToggle] = false;
@@ -584,21 +586,37 @@ MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
}
bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
+ // SHELL 40e390 uses a 50 x 50 footprint in movie coordinates.
+ const Common::Rect footprint(point.x - 25, point.y - 25, point.x + 25, point.y + 25);
for (uint i = 0; i < _targets.size(); ++i) {
const Target &target = _targets[i];
- if (target.id >= 800 && target.id < 900 && target.active && !target.hit && target.rect.contains(point))
+ if (target.id >= 800 && target.id < 900 && target.active && !target.hit && target.rect.intersects(footprint))
return true;
}
for (uint n = 0; n < _obstacles.size(); ++n) {
const Obstacle &obstacle = _obstacles[n];
- bool inside = false;
- for (uint i = 0, j = 3; i < 4; j = i++) {
- const Common::Point &a = obstacle.corners[i], &b = obstacle.corners[j];
- if ((a.y > point.y) != (b.y > point.y) &&
- point.x < a.x + int64(b.x - a.x) * (point.y - a.y) / (b.y - a.y))
- inside = !inside;
+ // Separating axes also handle vertical edges and degenerate quads
+ // without the divisions used by the original executable.
+ bool separated = false;
+ for (uint axis = 0; axis < 6 && !separated; ++axis) {
+ int dx = axis == 4 ? 1 : 0, dy = axis == 5 ? 1 : 0;
+ if (axis < 4) {
+ const Common::Point &a = obstacle.corners[axis], &b = obstacle.corners[(axis + 1) % 4];
+ dx = a.y - b.y;
+ dy = b.x - a.x;
+ }
+ int64 minimum = int64(dx) * obstacle.corners[0].x + int64(dy) * obstacle.corners[0].y;
+ int64 maximum = minimum;
+ for (uint i = 1; i < 4; ++i) {
+ const int64 projection = int64(dx) * obstacle.corners[i].x + int64(dy) * obstacle.corners[i].y;
+ minimum = MIN(minimum, projection);
+ maximum = MAX(maximum, projection);
+ }
+ const int64 center = int64(dx) * point.x + int64(dy) * point.y;
+ const int64 radius = 25 * (int64(ABS(dx)) + ABS(dy));
+ separated = center + radius < minimum || center - radius > maximum;
}
- if (inside)
+ if (!separated)
return true;
}
return false;
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 5a9abb0e968..fa6f9cd1770 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -58,6 +58,13 @@ public:
enum { kWidth = 384, kHeight = 288 };
enum Weapon { kRevitascope, kSlimeGun };
enum ShotResult { kMiss, kAnimalRescued, kHornetHit, kShellRestored };
+ struct Target {
+ uint16 id, toggle, remapToggle;
+ uint rescuedAt;
+ Common::Rect rect;
+ Weapon hitWeapon;
+ bool active, hit;
+ };
MortimerFlight();
~MortimerFlight();
@@ -77,6 +84,7 @@ public:
Weapon weapon() const { return _weapon; }
const Common::String &nextFlight() const { return _nextFlight; }
const Common::Array<MortimerSound> &sounds() const { return _sounds; }
+ const Common::Array<Target> &targets() const { return _targets; }
const Common::String &errorMessage() const { return _error; }
private:
@@ -87,12 +95,6 @@ private:
uint32 offset, size;
uint16 skipA, skipB;
};
- struct Target {
- uint16 id, toggle, remapToggle;
- uint rescuedAt;
- Common::Rect rect;
- bool active, hit;
- };
struct Obstacle {
Common::Point corners[4];
};
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 3a6dd97fe9e..dbfe3d2d1e6 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -22,6 +22,7 @@
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
+#include "backends/keymapper/action.h"
#include "common/events.h"
#include "common/file.h"
#include "common/system.h"
@@ -42,9 +43,68 @@ static const char *const kDemoFlights[] = {
"RESOURCE/DEMO/DEMO_BT.SAN"
};
+enum MortimerAction {
+ kActionFire, kActionPause, kActionSkip, kActionRestart, kActionQuit,
+ kActionMenu, kActionMouse, kActionJoystick
+};
+
+Common::KeymapArray MortimerEngine::initKeymaps() {
+ Common::Keymap *keymap = new Common::Keymap(Common::Keymap::kKeymapTypeGame, "mortimer", "Mortimer");
+ keymap->setPartialMatchAllowed(false);
+ struct Binding {
+ const char *id, *label;
+ MortimerAction action;
+ const char *inputs[4];
+ };
+ const Binding bindings[] = {
+ {"FIRE", _s("Fire"), kActionFire, {"MOUSE_LEFT", "MOUSE_RIGHT", "JOY_A", "JOY_B"}},
+ {"PAUSE", _s("Pause"), kActionPause, {"SPACE", "JOY_START", nullptr, nullptr}},
+ {"SKIP", _s("Skip"), kActionSkip, {"ESCAPE", "JOY_Y", nullptr, nullptr}},
+ {"RESTART", _s("Restart"), kActionRestart, {"r", "JOY_X", nullptr, nullptr}},
+ {"QUIT", _s("Quit"), kActionQuit, {"q", "A+q", nullptr, nullptr}},
+ {"MENU", _s("Game menu"), kActionMenu, {"F5", "JOY_LEFT_SHOULDER", nullptr, nullptr}},
+ {"MOUSE", _s("Mouse control"), kActionMouse, {"A+m", nullptr, nullptr, nullptr}},
+ {"JOYSTICK", _s("Joystick control"), kActionJoystick, {"A+j", nullptr, nullptr, nullptr}}
+ };
+ for (uint i = 0; i < ARRAYSIZE(bindings); ++i) {
+ Common::Action *action = new Common::Action(bindings[i].id, _(bindings[i].label));
+ action->setCustomEngineActionEvent(bindings[i].action);
+ for (uint j = 0; j < ARRAYSIZE(bindings[i].inputs); ++j)
+ if (bindings[i].inputs[j])
+ action->addDefaultInputMapping(bindings[i].inputs[j]);
+ if (bindings[i].action == kActionFire)
+ action->addDefaultInputMapping("RETURN");
+ keymap->addAction(action);
+ }
+ const char *const directions[] = {"LEFT", "RIGHT", "UP", "DOWN"};
+ const char *const labels[] = {_s("Left"), _s("Right"), _s("Up"), _s("Down")};
+ const char *const axes[] = {"JOY_LEFT_STICK_X-", "JOY_LEFT_STICK_X+", "JOY_LEFT_STICK_Y-", "JOY_LEFT_STICK_Y+"};
+ const char *const keypad[] = {"KP4", "KP6", "KP8", "KP2"};
+ for (uint i = 0; i < ARRAYSIZE(directions); ++i) {
+ Common::Action *action = new Common::Action(directions[i], _(labels[i]));
+ Common::Event event;
+ event.type = Common::EVENT_JOYAXIS_MOTION;
+ event.joystick.axis = i;
+ event.joystick.position = 0;
+ action->setEvent(event);
+ action->addDefaultInputMapping(directions[i]);
+ action->addDefaultInputMapping(keypad[i]);
+ action->addDefaultInputMapping(Common::String("JOY_") + directions[i]);
+ action->addDefaultInputMapping(axes[i]);
+ keymap->addAction(action);
+ }
+ Common::KeymapArray keymaps;
+ keymaps.push_back(keymap);
+ return keymaps;
+}
+
MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
- _mouse(320, 180), _view(32, 24), _nextFrameTime(0), _pauseTime(0),
- _segment(0), _completedRescues(0), _completedSpecies(0), _shellShocks(0), _finished(false) {
+ _viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
+ _nextFrameTime(0), _pauseTime(0), _segment(0), _completedRescues(0), _completedSpecies(0),
+ _shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
+ _pendingGlowStones(0), _finished(false), _controllerMode(false) {
+ memset(_steering, 0, sizeof(_steering));
+ memset(_iconFlash, 0, sizeof(_iconFlash));
}
MortimerEngine::~MortimerEngine() {
@@ -65,10 +125,17 @@ bool MortimerEngine::loadResources() {
uint minFrames;
};
const SpriteBank banks[] = {
- {"RESOURCE/GENERAL/COCKPT_H.NUT", &_cockpit, 3},
+ {"RESOURCE/GENERAL/COCKPT_H.NUT", &_cockpit, 14},
{"RESOURCE/GENERAL/DASH_H.NUT", &_dashboard, 36},
{"RESOURCE/GENERAL/RETICLEH.NUT", &_reticles, 2},
- {"RESOURCE/SAVANNA/S_ICON_H.NUT", &_icons, 14}
+ {"RESOURCE/SAVANNA/S_ICON_H.NUT", &_icons, 14},
+ {"RESOURCE/GENERAL/MHEAD_H.NUT", &_head, 4},
+ {"RESOURCE/GENERAL/REVITE.NUT", &_scopeHit, 11},
+ {"RESOURCE/GENERAL/SLIME_H.NUT", &_slimeHit, 7},
+ {"RESOURCE/GENERAL/LINE_BLU.NUT", &_scopeBeam, 1},
+ {"RESOURCE/GENERAL/SLMBALL.NUT", &_slimeBall, 1},
+ {"RESOURCE/GENERAL/NUMBER_H.NUT", &_numbers, 11},
+ {"RESOURCE/GENERAL/CORNER_H.NUT", &_corners, 2}
};
for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
if (!file.open(Common::Path(banks[i].name)) || !loadMortimerSprites(file, *banks[i].sprites) || banks[i].sprites->size() < banks[i].minFrames) {
@@ -86,7 +153,9 @@ bool MortimerEngine::loadResources() {
{"RESOURCE/GENERAL/SFX/SCOPHIT3.SAD", &_rescueSound},
{"RESOURCE/GENERAL/SFX/SHELHIT1.SAD", &_collisionSound},
{"RESOURCE/GENERAL/SFX/SLMGUN2.SAD", &_slimeSound},
- {"RESOURCE/GENERAL/SFX/MUCKHIT1.SAD", &_slimeHitSound}
+ {"RESOURCE/GENERAL/SFX/MUCKHIT1.SAD", &_slimeHitSound},
+ {"RESOURCE/GENERAL/SFX/GLOWIN1.SAD", &_glowSound},
+ {"RESOURCE/GENERAL/SFX/BURST1.SAD", &_balloonSound}
};
for (uint i = 0; i < ARRAYSIZE(effects); ++i) {
if (!file.open(Common::Path(effects[i].name)) || !loadMortimerSound(file, *effects[i].sound)) {
@@ -137,6 +206,7 @@ void MortimerEngine::stopSounds() {
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
_mixer->stopHandle(_collisionHandle);
+ _mixer->stopHandle(_glowHandle);
}
bool MortimerEngine::startFlight() {
@@ -147,7 +217,16 @@ bool MortimerEngine::startFlight() {
return false;
}
_finished = false;
- setMouse(Common::Point(320, 180));
+ _uiFrame = 0;
+ _shotFrame = 3;
+ _glowFrame = 10;
+ _glowStones = MIN(_completedRescues, 999U);
+ _pendingGlowStones = 0;
+ memset(_iconFlash, 0, sizeof(_iconFlash));
+ for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
+ _steeringHistory[i] = Common::Point();
+ setMouse(Common::Point(320, 240));
+ updateSteering();
_system->warpMouse(_mouse.x, _mouse.y);
_system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
_nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
@@ -185,10 +264,90 @@ bool MortimerEngine::nextFlight(bool skip) {
void MortimerEngine::setMouse(const Common::Point &position) {
_mouse.x = CLIP<int>(position.x, 0, 639);
_mouse.y = CLIP<int>(position.y, 0, 479);
- // A bounded view into the overscanned movie supplies steering. Exact
- // acceleration, collision damage and scope behaviour remain POC rules.
- _view.x = CLIP<int>(32 + (_mouse.x - 320) / 8, 0, MortimerFlight::kWidth - 320);
- _view.y = CLIP<int>(24 + (_mouse.y - 180) / 4, 0, MortimerFlight::kHeight - 240);
+}
+
+void MortimerEngine::updateSteering() {
+ int x = (_mouse.x - 320) / 2, y = 240 - _mouse.y;
+ if (_controllerMode) {
+ x = (_steering[1] - _steering[0]) * 128 / Common::JOYAXIS_MAX;
+ y = (_steering[2] - _steering[3]) * 128 / Common::JOYAXIS_MAX;
+ }
+ // SHELL 40db90 averages ten input samples before moving the viewport.
+ const int weights[] = {1, 1, 3, 3, 8, 8, 14, 16, 16, 14};
+ int averageX = 0, averageY = 0;
+ for (uint i = 0; i < ARRAYSIZE(weights); ++i) {
+ _steeringHistory[i] = i + 1 < ARRAYSIZE(weights) ? _steeringHistory[i + 1] : Common::Point(x, y);
+ averageX += _steeringHistory[i].x * weights[i];
+ averageY += _steeringHistory[i].y * weights[i];
+ }
+ averageX /= 84;
+ averageY /= 84;
+ // The movie is doubled, and its translation is aligned to four pixels.
+ _view.x = ((88 + CLIP(averageX * 88 / 128, -88, 88) + 3) & ~3) / 2;
+ _view.y = ((118 - CLIP(averageY * 118 / 128, -118, 118) + 3) & ~3) / 2;
+ if (ABS(x) < 3)
+ x = 0;
+ if (ABS(y) < 3)
+ y = 0;
+ _aim.x = 320 + CLIP(x * 296 / 109, -296, 295);
+ _aim.y = 190 - CLIP(y * 170 / 128, -169, 170);
+}
+
+Common::Point MortimerEngine::moviePoint(const Common::Point &point) const {
+ return Common::Point(_view.x + (point.x - _viewport.left) / 2, _view.y + (point.y - _viewport.top) / 2);
+}
+
+Common::Point MortimerEngine::screenPoint(const Common::Point &point) const {
+ return Common::Point(_viewport.left + (point.x - _view.x) * 2, _viewport.top + (point.y - _view.y) * 2);
+}
+
+void MortimerEngine::fire() {
+ if (_shotFrame >= 3) {
+ _shot = _aim;
+ _shotWeapon = _flight.weapon();
+ _shotFrame = 0;
+ playSound(_shotWeapon == MortimerFlight::kSlimeGun ? _slimeSound : _scopeSound,
+ _scopeHandle, Audio::Mixer::kSFXSoundType);
+ }
+ const MortimerFlight::ShotResult result = _flight.shoot(moviePoint(_aim));
+ if (result == MortimerFlight::kAnimalRescued) {
+ playSound(_rescueSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ ++_pendingGlowStones;
+ if (_glowFrame >= 10) {
+ _glowFrame = 0;
+ playSound(_glowSound, _glowHandle, Audio::Mixer::kSFXSoundType);
+ }
+ for (uint i = 0; i < _flight.targets().size(); ++i) {
+ const MortimerFlight::Target &target = _flight.targets()[i];
+ if (target.hit && target.rescuedAt == _flight.frame() && target.id / 100 < 7)
+ _iconFlash[target.id / 100] = 6;
+ }
+ } else if (result == MortimerFlight::kHornetHit) {
+ playSound(_slimeHitSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ } else if (result == MortimerFlight::kShellRestored) {
+ _shellShocks = 0;
+ playSound(_balloonSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
+ }
+}
+
+void MortimerEngine::updateInterface() {
+ ++_uiFrame;
+ if (_shotFrame < 3)
+ ++_shotFrame;
+ for (uint i = 0; i < ARRAYSIZE(_iconFlash); ++i)
+ if (_iconFlash[i])
+ --_iconFlash[i];
+ if (_glowFrame < 10 && ++_glowFrame == 10) {
+ _glowStones = MIN(_glowStones + _pendingGlowStones, 999U);
+ _pendingGlowStones = 0;
+ }
+}
+
+bool MortimerEngine::speechPlaying() const {
+ for (uint i = 0; i < _flightHandles.size(); ++i)
+ if (!_flight.sounds()[i].music && _mixer->isSoundHandleActive(_flightHandles[i]))
+ return true;
+ return false;
}
void MortimerEngine::pauseEngineIntern(bool pause) {
@@ -212,46 +371,50 @@ Common::Error MortimerEngine::run() {
Common::Event event;
while (_eventMan->pollEvent(event)) {
if (event.type == Common::EVENT_MOUSEMOVE) {
- if (!isPaused())
+ if (!isPaused()) {
setMouse(event.mouse);
- } else if (event.type == Common::EVENT_LBUTTONDOWN && !isPaused() && !_finished) {
- setMouse(event.mouse);
- if (_mouse.x >= 54 && _mouse.x < 567 && _mouse.y >= 37 && _mouse.y < 320) {
- playSound(_flight.weapon() == MortimerFlight::kSlimeGun ? _slimeSound : _scopeSound,
- _scopeHandle, Audio::Mixer::kSFXSoundType);
- const MortimerFlight::ShotResult result = _flight.shoot(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2));
- if (result == MortimerFlight::kAnimalRescued || result == MortimerFlight::kShellRestored)
- playSound(_rescueSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
- else if (result == MortimerFlight::kHornetHit)
- playSound(_slimeHitSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
- if (result == MortimerFlight::kShellRestored)
- _shellShocks = 0;
+ _controllerMode = false;
}
- } else if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat) {
- switch (event.kbd.keycode) {
- case Common::KEYCODE_ESCAPE:
+ } else if (event.type == Common::EVENT_JOYAXIS_MOTION && event.joystick.axis < ARRAYSIZE(_steering)) {
+ _steering[event.joystick.axis] = CLIP<int>(event.joystick.position, 0, Common::JOYAXIS_MAX);
+ if (event.joystick.position && !isPaused())
+ _controllerMode = true;
+ } else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
+ switch (event.customType) {
+ case kActionFire:
+ if (!isPaused() && !_finished)
+ fire();
+ break;
+ case kActionSkip:
if (pause.isActive())
pause.clear();
else
exit = !nextFlight(true);
break;
- case Common::KEYCODE_q:
+ case kActionQuit:
exit = true;
break;
- case Common::KEYCODE_SPACE:
+ case kActionPause:
if (pause.isActive())
pause.clear();
else
pause = pauseEngine();
break;
- case Common::KEYCODE_r:
+ case kActionRestart:
if (pause.isActive())
pause.clear();
exit = !restart();
break;
- case Common::KEYCODE_F5:
+ case kActionMenu:
openMainMenuDialog();
break;
+ case kActionMouse:
+ _controllerMode = false;
+ setMouse(_eventMan->getMousePos());
+ break;
+ case kActionJoystick:
+ _controllerMode = true;
+ break;
default:
break;
}
@@ -261,6 +424,8 @@ Common::Error MortimerEngine::run() {
break;
if (!isPaused() && !_finished && int32(_system->getMillis() - _nextFrameTime) >= 0) {
_nextFrameTime += 1000 / _flight.frameRate();
+ updateSteering();
+ updateInterface();
if (!_flight.advance()) {
if (!_flight.errorMessage().empty()) {
_loadError = _flight.errorMessage();
@@ -271,7 +436,7 @@ Common::Error MortimerEngine::run() {
} else {
playFlightSounds();
_system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
- if (_flight.hitsObstacle(Common::Point(_view.x + _mouse.x / 2, _view.y + _mouse.y / 2))) {
+ if (_flight.hitsObstacle(moviePoint(_aim))) {
++_shellShocks;
playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
// SHELL 40be36 resets the flight after eighteen shell shocks.
@@ -296,9 +461,19 @@ Common::Error MortimerEngine::run() {
return _loadError.empty() ? Common::kNoError : Common::Error(Common::kReadingFailed, _loadError);
}
-void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y) {
+void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y, bool centered, const Common::Rect *bounds) {
+ // NUT offsets are relative to the animation's origin, including when its
+ // frames have different bounding boxes (SHELL 402c83/4141bc).
+ if (centered) {
+ x -= (sprite.x + sprite.width) / 2;
+ y -= (sprite.y + sprite.height) / 2;
+ }
+ x += sprite.x;
+ y += sprite.y;
Common::Rect clip(x, y, x + sprite.width, y + sprite.height);
clip.clip(Common::Rect(_screen.w, _screen.h));
+ if (bounds)
+ clip.clip(*bounds);
if (clip.isEmpty())
return;
for (int row = clip.top; row < clip.bottom; ++row) {
@@ -310,36 +485,109 @@ void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y) {
}
}
-void MortimerEngine::draw() {
- for (int y = 0; y < 480; ++y) {
- const byte *src = _flight.pixels() + (_view.y + y / 2) * MortimerFlight::kWidth + _view.x;
- byte *dst = (byte *)_screen.getBasePtr(0, y);
- for (int x = 0; x < 640; ++x)
- dst[x] = src[x / 2];
+void MortimerEngine::drawTargets() {
+ for (uint i = 0; i < _flight.targets().size(); ++i) {
+ const MortimerFlight::Target &target = _flight.targets()[i];
+ if (!target.active || !target.id || target.id >= 800)
+ continue;
+ if (target.hit) {
+ const Common::Array<MortimerSprite> &effect = target.hitWeapon == MortimerFlight::kSlimeGun ? _slimeHit : _scopeHit;
+ const uint frame = _flight.frame() - target.rescuedAt;
+ if (frame < effect.size()) {
+ const Common::Point center = screenPoint(Common::Point((target.rect.left + target.rect.right) / 2,
+ (target.rect.top + target.rect.bottom) / 2));
+ drawSprite(effect[frame], center.x, center.y, true, &_viewport);
+ }
+ } else if ((target.id < 700 && _flight.weapon() == MortimerFlight::kRevitascope) ||
+ (target.id >= 700 && _flight.weapon() == MortimerFlight::kSlimeGun)) {
+ const Common::Point top = screenPoint(Common::Point(target.rect.left, target.rect.top));
+ const Common::Point bottom = screenPoint(Common::Point(target.rect.right, target.rect.bottom));
+ drawSprite(_corners[0], top.x, top.y, true, &_viewport);
+ drawSprite(_corners[1], bottom.x, bottom.y, true, &_viewport);
+ }
}
- drawSprite(_cockpit[0], 0, _cockpit[0].y);
- drawSprite(_cockpit[1], 640 - _cockpit[1].width, _cockpit[1].y);
- drawSprite(_cockpit[2], 0, 0);
- drawSprite(_dashboard[35], 0, 320);
- const int iconCenterY[] = {384, 379, 377, 376, 378, 380, 384};
- for (uint species = 0; species < 7; ++species) {
- if ((_completedSpecies | _flight.rescuedSpecies()) & (1 << species)) {
- const MortimerSprite &icon = _icons[species * 2];
- drawSprite(icon, 176 + species * 45 - icon.width / 2, iconCenterY[species] - icon.height / 2);
+}
+
+void MortimerEngine::drawProjectile() {
+ if (_shotFrame >= 3)
+ return;
+ const bool slime = _shotWeapon == MortimerFlight::kSlimeGun;
+ const MortimerSprite &texture = slime ? _slimeBall[0] : _scopeBeam[0];
+ const Common::Point origin(slime ? 599 : 40, 400);
+ // SHELL 403b60 projects a textured streak in three steps from the weapon
+ // towards the sight. Its far endpoint is 7/6 of the distance to the aim.
+ const int dx = (_shot.x - origin.x) * 7 / 6, dy = (_shot.y - origin.y) * 7 / 6;
+ const int distance = 32 * _shotFrame + 16;
+ const Common::Point tail(origin.x + dx - dx * 16 / distance, origin.y + dy - dy * 16 / distance);
+ const Common::Point tip(origin.x + dx - dx * 16 / (distance + 25), origin.y + dy - dy * 16 / (distance + 25));
+ const int lengthX = ABS(tip.x - tail.x), lengthY = ABS(tip.y - tail.y);
+ const bool horizontal = lengthX > lengthY;
+ const int length = MAX(lengthX, lengthY);
+ const int thickness = MIN(horizontal ? 8 : texture.height, length * texture.height / texture.width + 2);
+ for (int row = 0; row < thickness; ++row) {
+ const byte *src = texture.pixels.begin() + (row * texture.height / thickness) * texture.width;
+ for (int step = 0; step <= length; ++step) {
+ const int x = tail.x + (tip.x - tail.x) * step / MAX(length, 1) + (horizontal ? 0 : row - thickness / 2);
+ const int y = tail.y + (tip.y - tail.y) * step / MAX(length, 1) + (horizontal ? row - thickness / 2 : 0);
+ const byte color = src[step * texture.width / (length + 1)];
+ if (color && x >= 0 && x < _screen.w && y >= 0 && y < _screen.h)
+ *(byte *)_screen.getBasePtr(x, y) = color;
}
}
- if (_mouse.y < 320) {
- const MortimerSprite &reticle = _reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1];
- drawSprite(reticle, _mouse.x - reticle.width / 2, _mouse.y - reticle.height / 2);
+}
+
+void MortimerEngine::drawDashboard() {
+ // Origins and frame ranges are from SHELL's animation/position tables
+ // at 441240, 442d20 and 444958. drawSprite adds each frame's own offset.
+ drawSprite(_dashboard[35], 0, 0);
+ drawSprite(_dashboard[29 + _uiFrame % 6], 0, 439);
+ drawSprite(_dashboard[speechPlaying() ? 16 + _uiFrame % 3 : 16], 21, 312);
+ const uint gauge = _shellShocks < 16 ? 19 + _shellShocks / 2 : 27 + _uiFrame % 2;
+ drawSprite(_dashboard[gauge], 180, 442);
+ const Common::Point iconOrigins[] = {{158, 364}, {203, 358}, {248, 358}, {293, 354}, {339, 355}, {383, 360}, {428, 361}};
+ const Common::Point flashOrigins[] = {{154, 345}, {198, 338}, {243, 334}, {288, 332}, {333, 334}, {378, 338}, {422, 345}};
+ for (uint species = 0; species < 7; ++species) {
+ if (_iconFlash[species] & 1)
+ drawSprite(_dashboard[species], flashOrigins[species].x, flashOrigins[species].y);
+ const bool rescued = (_completedSpecies | _flight.rescuedSpecies()) & (1 << species);
+ drawSprite(_icons[species * 2 + (rescued ? 0 : 1)], iconOrigins[species].x, iconOrigins[species].y);
}
+ const uint thresholds[] = {1, 2, 3, 4, 7, 20, 30, 40, 50};
+ for (int i = ARRAYSIZE(thresholds) - 1; i >= 0; --i) {
+ if (_glowStones >= thresholds[i]) {
+ drawSprite(_dashboard[7 + i], 468, 368);
+ break;
+ }
+ }
+ drawSprite(_numbers[_glowStones < 100 ? 10 : _glowStones / 100], 504, 360);
+ drawSprite(_numbers[_glowStones < 10 ? 10 : (_glowStones / 10) % 10], 517, 361);
+ drawSprite(_numbers[_glowStones % 10], 529, 363);
+ if (_glowFrame < 10)
+ drawSprite(_cockpit[4 + _glowFrame], 392, 272);
+ if (_flight.weapon() == MortimerFlight::kSlimeGun)
+ drawSprite(_cockpit[3], 306, 465, true);
+}
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
- _screen.fillRect(Common::Rect(0, 449, 640, 465), 10);
- font->drawString(&_screen, Common::String::format("Animals rescued: %u Shell shocks: %u / 18 Flight: %u / 3",
- _completedRescues + _flight.rescuedCount(), _shellShocks, _segment + 1), 8, 451, 624, 20);
+void MortimerEngine::draw() {
+ _screen.fillRect(Common::Rect(640, 480), 10);
+ for (int y = _viewport.top; y < _viewport.bottom; ++y) {
+ const byte *src = _flight.pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerFlight::kWidth + _view.x;
+ byte *dst = (byte *)_screen.getBasePtr(_viewport.left, y);
+ for (int x = 0; x < _viewport.width(); ++x)
+ dst[x] = src[x / 2];
+ }
+ drawTargets();
+ drawSprite(_head[_uiFrame % 4], 270, 200);
+ drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
+ drawSprite(_cockpit[0], 0, 0);
+ drawSprite(_cockpit[1], 567, 0);
+ drawSprite(_cockpit[2], 0, 0);
+ drawDashboard();
+ drawProjectile();
if (isPaused()) {
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
- font->drawString(&_screen, "Paused - Space to continue", 90, 155, 460, 20, Graphics::kTextAlignCenter);
+ font->drawString(&_screen, _("Paused"), 90, 155, 460, 20, Graphics::kTextAlignCenter);
}
_system->copyRectToScreen(_screen.getPixels(), _screen.pitch, 0, 0, 640, 480);
_system->updateScreen();
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 62cd5144535..336b002243e 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -22,6 +22,7 @@
#define SCUMM_INSANE_MORTIMER_MORTIMER_H
#include "engines/engine.h"
+#include "backends/keymapper/keymap.h"
#include "audio/mixer.h"
#include "graphics/surface.h"
#include "scumm/insane/mortimer/flight.h"
@@ -36,6 +37,7 @@ public:
~MortimerEngine() override;
Common::Error run() override;
bool hasFeature(EngineFeature feature) const override;
+ static Common::KeymapArray initKeymaps();
protected:
void pauseEngineIntern(bool pause) override;
@@ -47,23 +49,39 @@ private:
bool nextFlight(bool skip = false);
bool restart(bool all = false);
void setMouse(const Common::Point &position);
+ void updateSteering();
+ void updateInterface();
+ void fire();
+ Common::Point moviePoint(const Common::Point &point) const;
+ Common::Point screenPoint(const Common::Point &point) const;
void draw();
- void drawSprite(const MortimerSprite &sprite, int x, int y);
+ void drawSprite(const MortimerSprite &sprite, int x, int y, bool centered = false, const Common::Rect *clip = nullptr);
+ void drawProjectile();
+ void drawTargets();
+ void drawDashboard();
+ bool speechPlaying() const;
void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type);
void playFlightSounds();
void stopSounds();
MortimerFlight _flight;
Graphics::Surface _screen;
- Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons;
- MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound;
+ Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons, _head;
+ Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
+ MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound, _glowSound, _balloonSound;
Common::Array<Audio::SoundHandle> _flightHandles;
- Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle;
+ Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle, _glowHandle;
Common::String _loadError;
- Common::Point _mouse, _view;
+ Common::Rect _viewport;
+ Common::Point _mouse, _view, _aim, _shot;
+ Common::Point _steeringHistory[10];
+ int _steering[4];
+ MortimerFlight::Weapon _shotWeapon;
uint32 _nextFrameTime, _pauseTime;
uint _segment, _completedRescues, _completedSpecies, _shellShocks;
- bool _finished;
+ uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
+ uint _iconFlash[7];
+ bool _finished, _controllerMode;
};
} // End of namespace Scumm
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 5f474054257..fb7f1e2b835 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1134,6 +1134,14 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
Common::String gameId = ConfMan.get("gameid", target);
Action *act;
+#ifdef ENABLE_SCUMM_7_8
+ if (gameId == "mortimer") {
+ for (uint i = 0; i < keymaps.size(); ++i)
+ delete keymaps[i];
+ return MortimerEngine::initKeymaps();
+ }
+#endif
+
if (gameId == "rebel1" || gameId == "rebel2") {
for (uint i = 0; i < keymaps.size(); ++i) {
if (keymaps[i]->getId() == "engine-default") {
Commit: af9e3bf7ee9afb611446f6102cfcdc70f67de93d
https://github.com/scummvm/scummvm/commit/af9e3bf7ee9afb611446f6102cfcdc70f67de93d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: implemented intro video decoding and playback
Changed paths:
A engines/scumm/insane/mortimer/movie.cpp
A engines/scumm/insane/mortimer/movie.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index bf4b728ad80..63971983352 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -19,369 +19,45 @@
*/
#include "scumm/insane/mortimer/flight.h"
-#include "scumm/smush/codec37.h"
-#include "scumm/smush/rebel/codec_ra2.h"
#include "common/endian.h"
-#include "common/stream.h"
#include "common/util.h"
namespace Scumm {
-namespace {
-
-struct Chunk {
- uint32 tag, offset, size;
-};
-
-bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk, bool padded = true) {
- if (pos > end || end - pos < 8)
- return false;
- chunk.tag = READ_BE_UINT32(data + pos);
- chunk.size = READ_BE_UINT32(data + pos + 4);
- chunk.offset = pos + 8;
- if (chunk.size > end - chunk.offset)
- return false;
- pos = chunk.offset + chunk.size;
- if (padded && (chunk.size & 1)) {
- if (pos == end)
- return false;
- ++pos;
- }
- return true;
-}
-
-bool readData(Common::SeekableReadStream &stream, Common::Array<byte> &data) {
- const int64 size = stream.size();
- if (size < 8 || size > 64 * 1024 * 1024 || !stream.seek(0))
- return false;
- data.resize(size);
- return stream.read(data.begin(), data.size()) == data.size();
-}
-
-bool isContainer(const Common::Array<byte> &data, uint32 tag) {
- return data.size() >= 8 && READ_BE_UINT32(data.begin()) == tag &&
- READ_BE_UINT32(data.begin() + 4) == data.size() - 8;
-}
-
-bool validRows(const byte *src, uint32 size, int height, bool remap) {
- uint32 pos = 0;
- // Remap masks can include additional rows below the FOBJ clipping height.
- for (int y = 0; y < height || (remap && pos + 1 < size); ++y) {
- if (size - pos < 2)
- return false;
- const uint len = READ_LE_UINT16(src + pos);
- pos += 2;
- if (len > size - pos)
- return false;
- const uint32 end = pos + len;
- if (remap) {
- // Skip/run pairs; a final skip without a run is also permitted.
- pos = end;
- } else {
- while (pos < end) {
- const byte code = src[pos++];
- const uint count = (code & 1) ? 1 : (code >> 1) + 1;
- if (count > end - pos)
- return false;
- pos += count;
- }
- }
- }
- // FOBJ includes the encoder's final byte of alignment in its payload size.
- return pos == size || (pos + 1 == size && src[pos] == 0);
-}
-
-// Codec37's existing decoder has no input-length argument. Validate the two
-// encodings present in the demo flights before passing any data to it.
-bool validBackground(const byte *src, uint32 size) {
- if (size < 16 || src[1] != 1 || src[12] != 4)
- return false;
- uint32 pos = 16;
- if (src[0] == 2) {
- if (READ_LE_UINT32(src + 4) != MortimerFlight::kWidth * MortimerFlight::kHeight)
- return false;
- uint remaining = MortimerFlight::kWidth * MortimerFlight::kHeight;
- while (remaining) {
- if (pos == size)
- return false;
- const byte code = src[pos++];
- const uint run = (code >> 1) + 1;
- const uint count = (code & 1) ? 1 : run;
- if (count > size - pos || run > remaining)
- return false;
- pos += count;
- remaining -= run;
- }
- } else if (src[0] == 3) {
- for (uint block = 0; block < MortimerFlight::kWidth * MortimerFlight::kHeight / 16; ++block) {
- if (pos == size)
- return false;
- const byte code = src[pos++];
- const uint count = code == 0xff ? 16 : code == 0xfe ? 4 : code == 0xfd ? 1 : 0;
- if (count > size - pos)
- return false;
- pos += count;
- }
- } else {
- return false;
- }
- return pos <= size;
-}
-
-bool validObject(const byte *data, uint32 size, bool sprite = false) {
- if (size < 14)
- return false;
- const int width = READ_LE_INT16(data + 6), height = READ_LE_INT16(data + 8);
- if (width <= 0 || height <= 0 || width > 640 || height > 480)
- return false;
- if (READ_LE_INT16(data + 2) + width > 32767 || READ_LE_INT16(data + 4) + height > 32767)
- return false;
- const byte codec = data[0];
- if (sprite)
- return codec == 1 && validRows(data + 14, size - 14, height, false);
- if (codec == 37)
- return width == MortimerFlight::kWidth && height == MortimerFlight::kHeight &&
- READ_LE_UINT32(data + 2) == 0 && validBackground(data + 14, size - 14);
- if (codec != 1 && codec != 3 && codec != 23)
- return false;
- uint32 offset = 14;
- if (codec == 23) {
- const uint16 mode = READ_LE_UINT16(data + 12);
- if (mode != 0x100 && mode != 0x101)
- return false;
- if (mode == 0x100)
- offset += 256;
- }
- return offset <= size && validRows(data + offset, size - offset, height, codec == 23);
-}
-
-bool validPalette(const byte *data, uint32 size) {
- return (size == 2308 && READ_BE_UINT32(data) == 2) ||
- (size == 6 && READ_BE_UINT32(data) == 1);
-}
-
-bool validTransition(const byte *data, uint32 size) {
- // IACT 8/46 contains a length-prefixed filename followed by CR/LF/EOF.
- if (size < 22 || READ_LE_UINT16(data + 14) != size - 18 ||
- data[size - 3] != '\r' || data[size - 2] != '\n' || data[size - 1] != 0x1a)
- return false;
- for (uint i = 18; i < size - 3; ++i)
- if (data[i] < 32 || data[i] > 126)
- return false;
- return true;
-}
-
-bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
- if (!isContainer(data, MKTAG('S', 'A', 'U', 'D')))
- return false;
- sound.samples.clear();
- uint32 pos = 8;
- while (pos < data.size()) {
- Chunk chunk;
- // SAUD chunks are not word-aligned, unlike the surrounding SMUSH file.
- if (!nextChunk(data.begin(), data.size(), pos, chunk, false))
- return false;
- const byte *src = data.begin() + chunk.offset;
- if (chunk.tag == MKTAG('S', 'D', 'A', 'T')) {
- if (!sound.samples.empty())
- return false;
- sound.samples.assign(src, src + chunk.size);
- } else if (chunk.tag == MKTAG('S', 'H', 'D', 'R') && chunk.size == 4) {
- sound.rate = READ_LE_UINT32(src);
- } else if (chunk.tag == MKTAG('S', 'T', 'R', 'K') && chunk.size >= 14 && src[0] == 6 && src[1] == 12) {
- sound.rate = READ_BE_UINT32(src + 10);
- }
- }
- return !sound.samples.empty() && sound.rate >= 4000 && sound.rate <= 48000;
-}
-
-} // End of anonymous namespace
-
-bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites) {
- Common::Array<byte> data;
- sprites.clear();
- if (!readData(stream, data) || !isContainer(data, MKTAG('A', 'N', 'I', 'M')))
- return false;
- uint32 pos = 8;
- while (pos < data.size()) {
- Chunk frame;
- if (!nextChunk(data.begin(), data.size(), pos, frame))
- return false;
- if (frame.tag != MKTAG('F', 'R', 'M', 'E'))
- continue;
- uint32 subpos = frame.offset;
- const uint before = sprites.size();
- while (subpos < frame.offset + frame.size) {
- Chunk chunk;
- if (!nextChunk(data.begin(), frame.offset + frame.size, subpos, chunk))
- return false;
- if (chunk.tag != MKTAG('F', 'O', 'B', 'J'))
- continue;
- const byte *src = data.begin() + chunk.offset;
- if (!validObject(src, chunk.size, true))
- return false;
- MortimerSprite sprite;
- sprite.x = READ_LE_INT16(src + 2);
- sprite.y = READ_LE_INT16(src + 4);
- sprite.width = READ_LE_INT16(src + 6);
- sprite.height = READ_LE_INT16(src + 8);
- sprite.pixels.resize(sprite.width * sprite.height);
- memset(sprite.pixels.begin(), 0, sprite.pixels.size());
- smushDecodeRLEOpaque(sprite.pixels.begin(), src + 14, 0, 0, sprite.width, sprite.height, sprite.width, chunk.size - 14);
- sprites.push_back(sprite);
- }
- if (sprites.size() != before + 1)
- return false;
- }
- return !sprites.empty();
-}
-
-bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound) {
- Common::Array<byte> data;
- return readData(stream, data) && decodeSound(data, sound);
-}
-
-MortimerFlight::MortimerFlight() : _decoder(nullptr), _nextFrame(0), _frameRate(10), _rescuedCount(0), _rescuedSpecies(0) {
- _background.resize(kWidth * kHeight);
- _pixels.resize(kWidth * kHeight);
- memset(_initialPalette, 0, sizeof(_initialPalette));
+MortimerFlight::MortimerFlight() : _initialInteractive(true), _interactive(true) {
reset();
}
-MortimerFlight::~MortimerFlight() {
- delete _decoder;
-}
-
-bool MortimerFlight::fail(const char *message) {
- _error = message;
- _frames.clear();
- return false;
-}
-
-bool MortimerFlight::load(Common::SeekableReadStream &stream) {
- _frames.clear();
- _sounds.clear();
- _error.clear();
- reset();
- if (!readData(stream, _data) || !isContainer(_data, MKTAG('A', 'N', 'I', 'M')))
- return fail("Invalid Mortimer flight ANIM container");
-
- struct Track {
- uint16 id, nextBlock;
- MortimerSound sound;
- Common::Array<byte> bytes;
- };
- Common::Array<Track> tracks;
- uint32 pos = 8;
- uint expectedFrames = 0;
- while (pos < _data.size()) {
- Chunk chunk;
- if (!nextChunk(_data.begin(), _data.size(), pos, chunk))
- return fail("Truncated Mortimer flight chunk");
- const byte *src = _data.begin() + chunk.offset;
- if (chunk.tag == MKTAG('A', 'H', 'D', 'R')) {
- if (expectedFrames || chunk.size < 778 || READ_LE_UINT16(src) != 2)
- return fail("Unsupported Mortimer flight header");
- expectedFrames = READ_LE_UINT16(src + 2);
- _frameRate = READ_LE_UINT32(src + 774);
- if (!expectedFrames || _frameRate != 10)
- return fail("Unsupported Mortimer flight timing");
- memcpy(_initialPalette, src + 6, sizeof(_initialPalette));
- memcpy(_palette, _initialPalette, sizeof(_palette));
- } else if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
- if (!expectedFrames)
- return fail("Mortimer flight has no header");
- uint32 subpos = chunk.offset;
- uint backgrounds = 0;
- while (subpos < chunk.offset + chunk.size) {
- Chunk sub;
- if (!nextChunk(_data.begin(), chunk.offset + chunk.size, subpos, sub))
- return fail("Truncated Mortimer flight frame");
- const byte *body = _data.begin() + sub.offset;
- if (sub.tag == MKTAG('F', 'O', 'B', 'J')) {
- if (!validObject(body, sub.size))
- return fail("Unsupported or truncated Mortimer flight image");
- if (body[0] == 37) {
- ++backgrounds;
- if (_frames.empty() && body[14] != 2)
- return fail("Mortimer flight does not start with a keyframe");
- }
- } else if (sub.tag == MKTAG('S', 'K', 'I', 'P')) {
- if (sub.size != 4 || READ_LE_UINT16(body) >= 256 || READ_LE_UINT16(body + 2) >= 256)
- return fail("Invalid Mortimer object toggle");
- } else if (sub.tag == MKTAG('I', 'A', 'C', 'T')) {
- if (sub.size < 8)
- return fail("Truncated Mortimer flight action");
- const uint16 code = READ_LE_UINT16(body);
- if ((code >= 2 && code <= 4 && (sub.size < (code == 4 ? 18U : 12U) || READ_LE_UINT16(body + 8) >= 256)) ||
- (code == 5 && sub.size < 28))
- return fail("Invalid Mortimer flight action");
- if (code == 2 && READ_LE_UINT16(body + 4) == 3 && READ_LE_UINT16(body + 10) >= 256)
- return fail("Invalid Mortimer conditional object");
- if (code == 8 && READ_LE_UINT16(body + 2) == 46 && !validTransition(body, sub.size))
- return fail("Invalid Mortimer flight transition");
- } else if (sub.tag == MKTAG('X', 'P', 'A', 'L')) {
- if (!validPalette(body, sub.size))
- return fail("Invalid Mortimer palette change");
- } else if (sub.tag == MKTAG('P', 'S', 'A', 'D')) {
- if (sub.size < 10)
- return fail("Truncated Mortimer speech block");
- const uint16 id = READ_LE_UINT16(body), block = READ_LE_UINT16(body + 2);
- uint index = 0;
- while (index < tracks.size() && tracks[index].id != id)
- ++index;
- if (index == tracks.size()) {
- Track track;
- track.id = id;
- track.nextBlock = 0;
- track.sound.frame = _frames.size();
- track.sound.music = (READ_LE_UINT16(body + 6) & 64) != 0;
- track.sound.volume = MIN(255, body[8] * 2);
- track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
- tracks.push_back(track);
- }
- if (tracks[index].nextBlock++ != block)
- return fail("Missing Mortimer speech block");
- Common::Array<byte> &bytes = tracks[index].bytes;
- const uint oldSize = bytes.size();
- bytes.resize(oldSize + sub.size - 10);
- memcpy(bytes.begin() + oldSize, body + 10, sub.size - 10);
- }
- }
- // The final Big Tree frame contains an opaque RLE image only.
- if (backgrounds > 1 || (_frames.empty() && backgrounds != 1))
- return fail("Invalid Mortimer flight background");
- _frames.push_back({chunk.offset, chunk.size});
- }
- }
- if (_frames.size() != expectedFrames || !expectedFrames)
- return fail("Incomplete Mortimer flight");
- for (uint i = 0; i < tracks.size(); ++i) {
- if (!decodeSound(tracks[i].bytes, tracks[i].sound))
- return fail("Unsupported or incomplete Mortimer speech");
- _sounds.push_back(tracks[i].sound);
- }
- return true;
+bool MortimerFlight::load(Common::SeekableReadStream &stream, bool interactive) {
+ _initialInteractive = interactive;
+ return MortimerMovie::load(stream);
}
void MortimerFlight::reset() {
- delete _decoder;
- _decoder = new SmushDeltaBlocksDecoder(kWidth, kHeight);
- _nextFrame = _rescuedCount = _rescuedSpecies = 0;
+ MortimerMovie::reset();
+ _rescuedCount = _rescuedSpecies = 0;
_nextFlight.clear();
_weapon = kRevitascope;
- memcpy(_palette, _initialPalette, sizeof(_palette));
- memset(_paletteDelta, 0, sizeof(_paletteDelta));
- memset(_shiftedPalette, 0, sizeof(_shiftedPalette));
+ _interactive = _initialInteractive;
_targets.clear();
- _objects.clear();
_obstacles.clear();
memset(_toggles, 0, sizeof(_toggles));
- memset(_background.begin(), 0, _background.size());
- memset(_pixels.begin(), 0, _pixels.size());
- for (uint i = 0; i < 256; ++i)
- _remap[i] = i;
+}
+
+bool MortimerFlight::advance() {
+ _obstacles.clear();
+ for (uint i = 0; i < _targets.size(); ++i)
+ _targets[i].active = false;
+ return MortimerMovie::advance();
+}
+
+int MortimerFlight::objectScale(byte codec) const {
+ return _interactive ? 1 : MortimerMovie::objectScale(codec);
+}
+
+bool MortimerFlight::skipObject(uint16 a, uint16 b) const {
+ return a != 0xffff && (b ? _toggles[a] != _toggles[b] : _toggles[a]);
}
MortimerFlight::Target *MortimerFlight::findTarget(uint16 id) {
@@ -392,6 +68,7 @@ MortimerFlight::Target *MortimerFlight::findTarget(uint16 id) {
}
void MortimerFlight::handleAction(const byte *data, uint32 size) {
+ MortimerMovie::handleAction(data, size);
const uint16 code = READ_LE_UINT16(data), flags = READ_LE_UINT16(data + 2);
const uint16 mode = READ_LE_UINT16(data + 4), id = READ_LE_UINT16(data + 6);
if (code >= 2 && code <= 4 && mode == 1) {
@@ -452,6 +129,8 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
} else if (code == 6 && (flags == 9 || flags == 10)) {
// SHELL 407c5f calls 40b844/40b7ce at the scripted weapon changes.
_weapon = flags == 9 ? kSlimeGun : kRevitascope;
+ } else if (code == 6 && (flags == 11 || flags == 25)) {
+ _interactive = flags == 11;
} else if (code == 8 && flags == 46) {
_nextFlight = Common::String((const char *)data + 18, size - 21);
for (uint i = 0; i < _nextFlight.size(); ++i)
@@ -460,99 +139,6 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
}
}
-void MortimerFlight::handlePalette(const byte *data, uint32 size) {
- if (size == 2308) {
- for (uint i = 0; i < 768; ++i) {
- _shiftedPalette[i] = _palette[i] << 7;
- _paletteDelta[i] = READ_LE_INT16(data + 4 + i * 2);
- }
- memcpy(_palette, data + 1540, sizeof(_palette));
- } else {
- for (uint i = 0; i < 768; ++i) {
- _shiftedPalette[i] += _paletteDelta[i];
- _palette[i] = CLIP<int32>(_shiftedPalette[i] >> 7, 0, 255);
- }
- }
-}
-
-bool MortimerFlight::advance() {
- if (_nextFrame >= _frames.size())
- return false;
- _objects.clear();
- _obstacles.clear();
- for (uint i = 0; i < _targets.size(); ++i)
- _targets[i].active = false;
- const Frame &frame = _frames[_nextFrame++];
- uint32 pos = frame.offset;
- uint16 skipA = 0xffff, skipB = 0;
- while (pos < frame.offset + frame.size) {
- Chunk chunk;
- if (!nextChunk(_data.begin(), frame.offset + frame.size, pos, chunk))
- return fail("Truncated Mortimer flight frame");
- const byte *src = _data.begin() + chunk.offset;
- if (chunk.tag == MKTAG('S', 'K', 'I', 'P')) {
- skipA = READ_LE_UINT16(src);
- skipB = READ_LE_UINT16(src + 2);
- continue;
- }
- if (chunk.tag == MKTAG('I', 'A', 'C', 'T')) {
- handleAction(src, chunk.size);
- } else if (chunk.tag == MKTAG('X', 'P', 'A', 'L')) {
- handlePalette(src, chunk.size);
- } else if (chunk.tag == MKTAG('F', 'O', 'B', 'J')) {
- if (src[0] == 37) {
- _decoder->decode(_background.begin(), src + 14);
- } else {
- if (src[0] == 23 && READ_LE_UINT16(src + 12) == 0x100)
- memcpy(_remap, src + 14, sizeof(_remap));
- _objects.push_back({chunk.offset, chunk.size, skipA, skipB});
- }
- }
- skipA = 0xffff;
- skipB = 0;
- }
- render();
- return true;
-}
-
-void MortimerFlight::render() {
- memcpy(_pixels.begin(), _background.begin(), _pixels.size());
- for (uint i = 0; i < _objects.size(); ++i) {
- const Object &object = _objects[i];
- if (object.skipA != 0xffff && (object.skipB ? _toggles[object.skipA] != _toggles[object.skipB] : _toggles[object.skipA]))
- continue;
- const byte *src = _data.begin() + object.offset;
- const int x = READ_LE_INT16(src + 2), y = READ_LE_INT16(src + 4);
- const int width = READ_LE_INT16(src + 6), height = READ_LE_INT16(src + 8);
- Common::Rect clip(x, y, x + width, y + height);
- clip.clip(Common::Rect(kWidth, kHeight));
- if (clip.isEmpty())
- continue;
- // Decode into an object-sized buffer so offscreen sprites cannot write
- // beyond the flight surface. Remap objects must start with its pixels.
- _scratch.resize(width * height);
- memset(_scratch.begin(), 0, _scratch.size());
- if (src[0] == 23) {
- for (int row = clip.top; row < clip.bottom; ++row)
- memcpy(_scratch.begin() + (row - y) * width + clip.left - x,
- _pixels.begin() + row * kWidth + clip.left, clip.width());
- const uint offset = READ_LE_UINT16(src + 12) == 0x100 ? 270 : 14;
- smushDecodeRA2SkipRemap(_scratch.begin(), src + offset, 0, 0, width, height, width, object.size - offset, _remap, 0);
- } else {
- smushDecodeRLEOpaque(_scratch.begin(), src + 14, 0, 0, width, height, width, object.size - 14);
- }
- for (int row = clip.top; row < clip.bottom; ++row) {
- const byte *line = _scratch.begin() + (row - y) * width + clip.left - x;
- byte *dest = _pixels.begin() + row * kWidth + clip.left;
- for (int col = 0; col < clip.width(); ++col) {
- // Mortimer treats zero as transparent in literal runs as well.
- if (src[0] == 1 || src[0] == 23 || line[col])
- dest[col] = line[col];
- }
- }
- }
-}
-
MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
// Later objects are drawn on top of earlier ones.
for (int i = int(_targets.size()) - 1; i >= 0; --i) {
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index fa6f9cd1770..b7320c21427 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -21,39 +21,11 @@
#ifndef SCUMM_INSANE_MORTIMER_FLIGHT_H
#define SCUMM_INSANE_MORTIMER_FLIGHT_H
-#include "common/array.h"
-#include "common/rect.h"
-#include "common/str.h"
-
-namespace Common {
-class SeekableReadStream;
-}
+#include "scumm/insane/mortimer/movie.h"
namespace Scumm {
-class SmushDeltaBlocksDecoder;
-
-struct MortimerSprite {
- int x, y, width, height;
- Common::Array<byte> pixels;
-};
-
-struct MortimerSound {
- uint frame = 0;
- uint rate = 11025;
- byte volume = 255;
- int8 balance = 0;
- bool music = false;
- Common::Array<byte> samples;
-};
-
-bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites);
-bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
-
-// The Windows demo flights. No ScummEngine, InsaneRebel or SmushPlayer
-// state is used here; only the existing, stateless RLE helpers and a private
-// codec37 decoder are shared with the other SMUSH games.
-class MortimerFlight {
+class MortimerFlight : public MortimerMovie {
public:
enum { kWidth = 384, kHeight = 288 };
enum Weapon { kRevitascope, kSlimeGun };
@@ -67,58 +39,36 @@ public:
};
MortimerFlight();
- ~MortimerFlight();
- bool load(Common::SeekableReadStream &stream);
- void reset();
- bool advance();
+ bool load(Common::SeekableReadStream &stream, bool interactive = true);
+ void reset() override;
+ bool advance() override;
ShotResult shoot(const Common::Point &point);
bool hitsObstacle(const Common::Point &point) const;
- const byte *pixels() const { return _pixels.begin(); }
- const byte *palette() const { return _palette; }
- uint frame() const { return _nextFrame ? _nextFrame - 1 : 0; }
- uint frameCount() const { return _frames.size(); }
- uint frameRate() const { return _frameRate; }
uint rescuedCount() const { return _rescuedCount; }
uint rescuedSpecies() const { return _rescuedSpecies; }
Weapon weapon() const { return _weapon; }
const Common::String &nextFlight() const { return _nextFlight; }
- const Common::Array<MortimerSound> &sounds() const { return _sounds; }
const Common::Array<Target> &targets() const { return _targets; }
- const Common::String &errorMessage() const { return _error; }
+ bool interactive() const { return _interactive; }
private:
- struct Frame {
- uint32 offset, size;
- };
- struct Object {
- uint32 offset, size;
- uint16 skipA, skipB;
- };
struct Obstacle {
Common::Point corners[4];
};
- bool fail(const char *message);
- void handleAction(const byte *data, uint32 size);
- void handlePalette(const byte *data, uint32 size);
- void render();
+ void handleAction(const byte *data, uint32 size) override;
+ bool skipObject(uint16 a, uint16 b) const override;
+ int objectScale(byte codec) const override;
Target *findTarget(uint16 id);
- Common::Array<byte> _data, _background, _pixels, _scratch;
- Common::Array<Frame> _frames;
- Common::Array<Object> _objects;
Common::Array<Target> _targets;
Common::Array<Obstacle> _obstacles;
- Common::Array<MortimerSound> _sounds;
- SmushDeltaBlocksDecoder *_decoder;
- Common::String _error, _nextFlight;
- byte _palette[768], _initialPalette[768], _remap[256];
- int16 _paletteDelta[768];
- int32 _shiftedPalette[768];
+ Common::String _nextFlight;
bool _toggles[256];
+ bool _initialInteractive, _interactive;
Weapon _weapon;
- uint _nextFrame, _frameRate, _rescuedCount, _rescuedSpecies;
+ uint _rescuedCount, _rescuedSpecies;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index dbfe3d2d1e6..37987aa99a2 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -37,8 +37,18 @@
namespace Scumm {
+// SHELL's demo scene list at 444220 plays all five introductions before
+// TITLE.SAN. The title continues directly into the first playable scene.
+static const char *const kDemoIntros[] = {
+ "RESOURCE/BEGIN/INTRO1_H.SAN",
+ "RESOURCE/BEGIN/INTRO2_H.SAN",
+ "RESOURCE/BEGIN/INTRO3_H.SAN",
+ "RESOURCE/BEGIN/INTRO4_H.SAN",
+ "RESOURCE/BEGIN/INTRO5_H.SAN"
+};
+
static const char *const kDemoFlights[] = {
- "RESOURCE/DEMO/DEMO_S1.SAN",
+ "RESOURCE/DEMO/TITLE.SAN",
"RESOURCE/DEMO/DEMO_PL.SAN",
"RESOURCE/DEMO/DEMO_BT.SAN"
};
@@ -100,7 +110,7 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
- _nextFrameTime(0), _pauseTime(0), _segment(0), _completedRescues(0), _completedSpecies(0),
+ _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _segment(0), _completedRescues(0), _completedSpecies(0),
_shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
_pendingGlowStones(0), _finished(false), _controllerMode(false) {
memset(_steering, 0, sizeof(_steering));
@@ -164,59 +174,100 @@ bool MortimerEngine::loadResources() {
}
file.close();
}
- return loadFlight(0);
+ return true;
+}
+
+MortimerMovie &MortimerEngine::currentMovie() {
+ return _introIndex >= 0 ? _intro : _flight;
+}
+
+const MortimerMovie &MortimerEngine::currentMovie() const {
+ return _introIndex >= 0 ? _intro : _flight;
+}
+
+bool MortimerEngine::loadIntro(uint index) {
+ stopSounds();
+ _movieHandles.clear();
+ Common::File file;
+ if (!file.open(Common::Path(kDemoIntros[index]))) {
+ _loadError = Common::String::format("Missing %s", kDemoIntros[index]);
+ return false;
+ }
+ // The fifth introduction starts with native 640 x 480 animation.
+ if (!_intro.load(file, index != 4)) {
+ _loadError = Common::String::format("%s: %s", kDemoIntros[index], _intro.errorMessage().c_str());
+ return false;
+ }
+ _introIndex = index;
+ _movieHandles.resize(_intro.sounds().size());
+ return true;
}
bool MortimerEngine::loadFlight(uint segment) {
- // The mixer borrows the samples owned by the flight being replaced.
+ // The mixer borrows the samples owned by the movie being replaced.
stopSounds();
- _flightHandles.clear();
+ _movieHandles.clear();
Common::File file;
if (!file.open(Common::Path(kDemoFlights[segment]))) {
_loadError = Common::String::format("Missing %s", kDemoFlights[segment]);
return false;
}
- if (!_flight.load(file)) {
+ if (!_flight.load(file, segment != 0)) {
_loadError = Common::String::format("%s: %s", kDemoFlights[segment], _flight.errorMessage().c_str());
return false;
}
_segment = segment;
- _flightHandles.resize(_flight.sounds().size());
+ _introIndex = -1;
+ _movieHandles.resize(_flight.sounds().size());
return true;
}
-void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type) {
+void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset) {
_mixer->stopHandle(handle);
// The engine owns the samples until all its handles have been stopped.
- Audio::AudioStream *stream = Audio::makeRawStream(sound.samples.begin(), sound.samples.size(), sound.rate,
+ Audio::AudioStream *stream = Audio::makeRawStream(sound.samples.begin() + offset, sound.samples.size() - offset, sound.rate,
Audio::FLAG_UNSIGNED, DisposeAfterUse::NO);
_mixer->playStream(type, &handle, stream, -1, sound.volume, sound.balance);
}
-void MortimerEngine::playFlightSounds() {
- for (uint i = 0; i < _flight.sounds().size(); ++i)
- if (_flight.sounds()[i].frame == _flight.frame())
- playSound(_flight.sounds()[i], _flightHandles[i],
- _flight.sounds()[i].music ? Audio::Mixer::kMusicSoundType : Audio::Mixer::kSpeechSoundType);
+void MortimerEngine::playMovieSounds(bool seek) {
+ const MortimerMovie &movie = currentMovie();
+ for (uint i = 0; i < movie.sounds().size(); ++i) {
+ const MortimerSound &sound = movie.sounds()[i];
+ if (sound.frame > movie.frame() || (!seek && sound.frame != movie.frame()))
+ continue;
+ const uint64 offset = uint64(movie.frame() - sound.frame) * sound.rate / movie.frameRate();
+ if (offset < sound.samples.size())
+ playSound(sound, _movieHandles[i], sound.music ? Audio::Mixer::kMusicSoundType :
+ (sound.speech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType), offset);
+ }
}
void MortimerEngine::stopSounds() {
- for (uint i = 0; i < _flightHandles.size(); ++i)
- _mixer->stopHandle(_flightHandles[i]);
+ for (uint i = 0; i < _movieHandles.size(); ++i)
+ _mixer->stopHandle(_movieHandles[i]);
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
_mixer->stopHandle(_collisionHandle);
_mixer->stopHandle(_glowHandle);
}
-bool MortimerEngine::startFlight() {
+bool MortimerEngine::startMovie() {
stopSounds();
- _flight.reset();
- if (!_flight.advance()) {
- _loadError = _flight.errorMessage();
+ MortimerMovie &movie = currentMovie();
+ movie.reset();
+ if (!movie.advance()) {
+ _loadError = movie.errorMessage();
return false;
}
_finished = false;
+ _system->getPaletteManager()->setPalette(movie.palette(), 0, 256);
+ _nextFrameTime = _system->getMillis() + 1000 / movie.frameRate();
+ playMovieSounds();
+ return true;
+}
+
+bool MortimerEngine::startFlight() {
_uiFrame = 0;
_shotFrame = 3;
_glowFrame = 10;
@@ -228,13 +279,12 @@ bool MortimerEngine::startFlight() {
setMouse(Common::Point(320, 240));
updateSteering();
_system->warpMouse(_mouse.x, _mouse.y);
- _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
- _nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
- playFlightSounds();
- return true;
+ return startMovie();
}
bool MortimerEngine::restart(bool all) {
+ if (_introIndex >= 0 && !all)
+ return startMovie();
_shellShocks = 0;
if (all) {
_completedRescues = _completedSpecies = 0;
@@ -244,8 +294,27 @@ bool MortimerEngine::restart(bool all) {
return startFlight();
}
-bool MortimerEngine::nextFlight(bool skip) {
+bool MortimerEngine::nextScene(bool skip) {
stopSounds();
+ if (_introIndex >= 0) {
+ if (uint(_introIndex + 1) < ARRAYSIZE(kDemoIntros))
+ return loadIntro(_introIndex + 1) && startMovie();
+ return loadFlight(0) && startFlight();
+ }
+ if (skip && !_flight.interactive()) {
+ // Keep TITLE's music at the matching position when skipping its
+ // opening; the same track continues after the cockpit appears.
+ while (!_flight.interactive()) {
+ if (!_flight.advance()) {
+ _loadError = _flight.errorMessage().empty() ? "Mortimer title has no gameplay transition" : _flight.errorMessage();
+ return false;
+ }
+ }
+ _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
+ _nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
+ playMovieSounds(true);
+ return true;
+ }
if (_segment + 1 == ARRAYSIZE(kDemoFlights)) {
_finished = true;
return true;
@@ -344,8 +413,8 @@ void MortimerEngine::updateInterface() {
}
bool MortimerEngine::speechPlaying() const {
- for (uint i = 0; i < _flightHandles.size(); ++i)
- if (!_flight.sounds()[i].music && _mixer->isSoundHandleActive(_flightHandles[i]))
+ for (uint i = 0; i < _movieHandles.size(); ++i)
+ if (currentMovie().sounds()[i].speech && _mixer->isSoundHandleActive(_movieHandles[i]))
return true;
return false;
}
@@ -366,7 +435,7 @@ Common::Error MortimerEngine::run() {
syncSoundSettings();
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
- bool exit = !restart();
+ bool exit = !(loadIntro(0) && startMovie());
while (!shouldQuit() && !exit) {
Common::Event event;
while (_eventMan->pollEvent(event)) {
@@ -382,14 +451,14 @@ Common::Error MortimerEngine::run() {
} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
switch (event.customType) {
case kActionFire:
- if (!isPaused() && !_finished)
+ if (!isPaused() && !_finished && interactive())
fire();
break;
case kActionSkip:
if (pause.isActive())
pause.clear();
else
- exit = !nextFlight(true);
+ exit = !nextScene(true);
break;
case kActionQuit:
exit = true;
@@ -423,20 +492,23 @@ Common::Error MortimerEngine::run() {
if (exit)
break;
if (!isPaused() && !_finished && int32(_system->getMillis() - _nextFrameTime) >= 0) {
- _nextFrameTime += 1000 / _flight.frameRate();
- updateSteering();
- updateInterface();
- if (!_flight.advance()) {
- if (!_flight.errorMessage().empty()) {
- _loadError = _flight.errorMessage();
+ MortimerMovie &movie = currentMovie();
+ _nextFrameTime += 1000 / movie.frameRate();
+ if (interactive()) {
+ updateSteering();
+ updateInterface();
+ }
+ if (!movie.advance()) {
+ if (!movie.errorMessage().empty()) {
+ _loadError = movie.errorMessage();
break;
}
- if (!nextFlight())
+ if (!nextScene())
break;
} else {
- playFlightSounds();
- _system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
- if (_flight.hitsObstacle(moviePoint(_aim))) {
+ playMovieSounds();
+ _system->getPaletteManager()->setPalette(movie.palette(), 0, 256);
+ if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
++_shellShocks;
playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
// SHELL 40be36 resets the flight after eighteen shell shocks.
@@ -446,7 +518,7 @@ Common::Error MortimerEngine::run() {
}
// Avoid racing through queued frames following a slow backend call.
if (int32(_system->getMillis() - _nextFrameTime) > 250)
- _nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
+ _nextFrameTime = _system->getMillis() + 1000 / currentMovie().frameRate();
}
draw();
if (_finished) {
@@ -569,21 +641,25 @@ void MortimerEngine::drawDashboard() {
}
void MortimerEngine::draw() {
- _screen.fillRect(Common::Rect(640, 480), 10);
- for (int y = _viewport.top; y < _viewport.bottom; ++y) {
- const byte *src = _flight.pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerFlight::kWidth + _view.x;
- byte *dst = (byte *)_screen.getBasePtr(_viewport.left, y);
- for (int x = 0; x < _viewport.width(); ++x)
- dst[x] = src[x / 2];
+ if (interactive()) {
+ _screen.fillRect(Common::Rect(640, 480), 10);
+ for (int y = _viewport.top; y < _viewport.bottom; ++y) {
+ const byte *src = _flight.pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerMovie::kScreenWidth + _view.x;
+ byte *dst = (byte *)_screen.getBasePtr(_viewport.left, y);
+ for (int x = 0; x < _viewport.width(); ++x)
+ dst[x] = src[x / 2];
+ }
+ drawTargets();
+ drawSprite(_head[_uiFrame % 4], 270, 200);
+ drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
+ drawSprite(_cockpit[0], 0, 0);
+ drawSprite(_cockpit[1], 567, 0);
+ drawSprite(_cockpit[2], 0, 0);
+ drawDashboard();
+ drawProjectile();
+ } else {
+ _screen.copyRectToSurface(currentMovie().pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
}
- drawTargets();
- drawSprite(_head[_uiFrame % 4], 270, 200);
- drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
- drawSprite(_cockpit[0], 0, 0);
- drawSprite(_cockpit[1], 567, 0);
- drawSprite(_cockpit[2], 0, 0);
- drawDashboard();
- drawProjectile();
if (isPaused()) {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 336b002243e..dbdcfacaa0c 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -44,10 +44,15 @@ protected:
private:
bool loadResources();
+ bool loadIntro(uint index);
bool loadFlight(uint segment);
+ bool startMovie();
bool startFlight();
- bool nextFlight(bool skip = false);
+ bool nextScene(bool skip = false);
bool restart(bool all = false);
+ MortimerMovie ¤tMovie();
+ const MortimerMovie ¤tMovie() const;
+ bool interactive() const { return _introIndex < 0 && _flight.interactive(); }
void setMouse(const Common::Point &position);
void updateSteering();
void updateInterface();
@@ -60,16 +65,17 @@ private:
void drawTargets();
void drawDashboard();
bool speechPlaying() const;
- void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type);
- void playFlightSounds();
+ void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset = 0);
+ void playMovieSounds(bool seek = false);
void stopSounds();
+ MortimerMovie _intro;
MortimerFlight _flight;
Graphics::Surface _screen;
Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons, _head;
Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound, _glowSound, _balloonSound;
- Common::Array<Audio::SoundHandle> _flightHandles;
+ Common::Array<Audio::SoundHandle> _movieHandles;
Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle, _glowHandle;
Common::String _loadError;
Common::Rect _viewport;
@@ -78,6 +84,7 @@ private:
int _steering[4];
MortimerFlight::Weapon _shotWeapon;
uint32 _nextFrameTime, _pauseTime;
+ int _introIndex;
uint _segment, _completedRescues, _completedSpecies, _shellShocks;
uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
uint _iconFlash[7];
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
new file mode 100644
index 00000000000..d7f6777a151
--- /dev/null
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -0,0 +1,557 @@
+/* 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 "scumm/insane/mortimer/movie.h"
+#include "scumm/smush/codec37.h"
+#include "scumm/smush/rebel/codec_ra2.h"
+
+#include "common/endian.h"
+#include "common/stream.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+namespace {
+
+struct Chunk {
+ uint32 tag, offset, size;
+};
+
+bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk, bool padded = true) {
+ if (pos > end || end - pos < 8)
+ return false;
+ chunk.tag = READ_BE_UINT32(data + pos);
+ chunk.size = READ_BE_UINT32(data + pos + 4);
+ chunk.offset = pos + 8;
+ if (chunk.size > end - chunk.offset)
+ return false;
+ pos = chunk.offset + chunk.size;
+ if (padded && (chunk.size & 1)) {
+ if (pos == end)
+ return false;
+ ++pos;
+ }
+ return true;
+}
+
+bool readData(Common::SeekableReadStream &stream, Common::Array<byte> &data) {
+ const int64 size = stream.size();
+ if (size < 8 || size > 64 * 1024 * 1024 || !stream.seek(0))
+ return false;
+ data.resize(size);
+ return stream.read(data.begin(), data.size()) == data.size();
+}
+
+bool isContainer(const Common::Array<byte> &data, uint32 tag) {
+ return data.size() >= 8 && READ_BE_UINT32(data.begin()) == tag &&
+ READ_BE_UINT32(data.begin() + 4) == data.size() - 8;
+}
+
+bool validRows(const byte *src, uint32 size, int height, bool remap) {
+ uint32 pos = 0;
+ // Remap masks can include additional rows below the FOBJ clipping height.
+ for (int y = 0; y < height || (remap && pos + 1 < size); ++y) {
+ if (size - pos < 2)
+ return false;
+ const uint len = READ_LE_UINT16(src + pos);
+ pos += 2;
+ if (len > size - pos)
+ return false;
+ const uint32 end = pos + len;
+ if (remap) {
+ // Skip/run pairs; a final skip without a run is also permitted.
+ pos = end;
+ } else {
+ while (pos < end) {
+ const byte code = src[pos++];
+ const uint count = (code & 1) ? 1 : (code >> 1) + 1;
+ if (count > end - pos)
+ return false;
+ pos += count;
+ }
+ }
+ }
+ // FOBJ includes the encoder's final byte of alignment in its payload size.
+ return pos == size || (pos + 1 == size && src[pos] == 0);
+}
+
+// Codec37's existing decoder has no input-length argument. Validate the two
+// encodings present in the demo movies before passing any data to it.
+bool validBackground(const byte *src, uint32 size, int width, int height) {
+ if (size < 16 || src[1] != 1 || (src[12] != 0 && src[12] != 4))
+ return false;
+ uint32 pos = 16;
+ if (src[0] == 2) {
+ if (READ_LE_UINT32(src + 4) != uint(width * height))
+ return false;
+ uint remaining = uint(width * height);
+ while (remaining) {
+ if (pos == size)
+ return false;
+ const byte code = src[pos++];
+ const uint run = (code >> 1) + 1;
+ const uint count = (code & 1) ? 1 : run;
+ if (count > size - pos || run > remaining)
+ return false;
+ pos += count;
+ remaining -= run;
+ }
+ } else if (src[0] == 3) {
+ for (uint block = 0; block < uint(width * height) / 16; ++block) {
+ if (pos == size)
+ return false;
+ const byte code = src[pos++];
+ const uint count = code == 0xff ? 16 : (src[12] & 4) && code == 0xfe ? 4 : (src[12] & 4) && code == 0xfd ? 1 : 0;
+ if (count > size - pos)
+ return false;
+ pos += count;
+ }
+ } else {
+ return false;
+ }
+ return pos <= size;
+}
+
+bool validObject(const byte *data, uint32 size, bool sprite = false) {
+ if (size < 14)
+ return false;
+ const int width = READ_LE_INT16(data + 6), height = READ_LE_INT16(data + 8);
+ if (width <= 0 || height <= 0 || width > 640 || height > 480)
+ return false;
+ if (READ_LE_INT16(data + 2) + width > 32767 || READ_LE_INT16(data + 4) + height > 32767)
+ return false;
+ const byte codec = data[0];
+ if (sprite)
+ return codec == 1 && validRows(data + 14, size - 14, height, false);
+ if (codec == 37)
+ return !(width & 3) && !(height & 3) && validBackground(data + 14, size - 14, width, height);
+ if (codec != 1 && codec != 3 && codec != 23)
+ return false;
+ uint32 offset = 14;
+ if (codec == 23) {
+ const uint16 mode = READ_LE_UINT16(data + 12);
+ if (mode != 0x100 && mode != 0x101)
+ return false;
+ if (mode == 0x100)
+ offset += 256;
+ }
+ return offset <= size && validRows(data + offset, size - offset, height, codec == 23);
+}
+
+bool validPalette(const byte *data, uint32 size) {
+ return (size == 2308 && READ_BE_UINT32(data) == 2) ||
+ (size == 6 && READ_BE_UINT32(data) == 1);
+}
+
+bool validTransition(const byte *data, uint32 size) {
+ // IACT 8/46 contains a length-prefixed filename followed by CR/LF/EOF.
+ if (size < 22 || READ_LE_UINT16(data + 14) != size - 18 ||
+ data[size - 3] != '\r' || data[size - 2] != '\n' || data[size - 1] != 0x1a)
+ return false;
+ for (uint i = 18; i < size - 3; ++i)
+ if (data[i] < 32 || data[i] > 126)
+ return false;
+ return true;
+}
+
+bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
+ if (!isContainer(data, MKTAG('S', 'A', 'U', 'D')))
+ return false;
+ sound.samples.clear();
+ uint32 pos = 8;
+ while (pos < data.size()) {
+ Chunk chunk;
+ // SAUD chunks are not word-aligned, unlike the surrounding SMUSH file.
+ if (!nextChunk(data.begin(), data.size(), pos, chunk, false))
+ return false;
+ const byte *src = data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('S', 'D', 'A', 'T')) {
+ if (!sound.samples.empty())
+ return false;
+ sound.samples.assign(src, src + chunk.size);
+ } else if (chunk.tag == MKTAG('S', 'H', 'D', 'R') && chunk.size == 4) {
+ sound.rate = READ_LE_UINT32(src);
+ } else if (chunk.tag == MKTAG('S', 'T', 'R', 'K') && chunk.size >= 14 && src[0] == 6 && src[1] == 12) {
+ sound.rate = READ_BE_UINT32(src + 10);
+ }
+ }
+ return !sound.samples.empty() && sound.rate >= 4000 && sound.rate <= 48000;
+}
+
+} // End of anonymous namespace
+
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites) {
+ Common::Array<byte> data;
+ sprites.clear();
+ if (!readData(stream, data) || !isContainer(data, MKTAG('A', 'N', 'I', 'M')))
+ return false;
+ uint32 pos = 8;
+ while (pos < data.size()) {
+ Chunk frame;
+ if (!nextChunk(data.begin(), data.size(), pos, frame))
+ return false;
+ if (frame.tag != MKTAG('F', 'R', 'M', 'E'))
+ continue;
+ uint32 subpos = frame.offset;
+ const uint before = sprites.size();
+ while (subpos < frame.offset + frame.size) {
+ Chunk chunk;
+ if (!nextChunk(data.begin(), frame.offset + frame.size, subpos, chunk))
+ return false;
+ if (chunk.tag != MKTAG('F', 'O', 'B', 'J'))
+ continue;
+ const byte *src = data.begin() + chunk.offset;
+ if (!validObject(src, chunk.size, true))
+ return false;
+ MortimerSprite sprite;
+ sprite.x = READ_LE_INT16(src + 2);
+ sprite.y = READ_LE_INT16(src + 4);
+ sprite.width = READ_LE_INT16(src + 6);
+ sprite.height = READ_LE_INT16(src + 8);
+ sprite.pixels.resize(sprite.width * sprite.height);
+ memset(sprite.pixels.begin(), 0, sprite.pixels.size());
+ smushDecodeRLEOpaque(sprite.pixels.begin(), src + 14, 0, 0, sprite.width, sprite.height, sprite.width, chunk.size - 14);
+ sprites.push_back(sprite);
+ }
+ if (sprites.size() != before + 1)
+ return false;
+ }
+ return !sprites.empty();
+}
+
+bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound) {
+ Common::Array<byte> data;
+ return readData(stream, data) && decodeSound(data, sound);
+}
+
+MortimerMovie::MortimerMovie() : _decoder(nullptr), _decoderWidth(0), _decoderHeight(0),
+ _doubled(true), _initialDoubled(true), _storeNext(false), _nextFrame(0), _frameRate(10) {
+ _pixels.resize(kScreenWidth * kScreenHeight);
+ _previous.resize(_pixels.size());
+ memset(_initialPalette, 0, sizeof(_initialPalette));
+ reset();
+}
+
+MortimerMovie::~MortimerMovie() {
+ delete _decoder;
+}
+
+bool MortimerMovie::fail(const char *message) {
+ _error = message;
+ _frames.clear();
+ return false;
+}
+
+bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
+ _initialDoubled = doubled;
+ _frames.clear();
+ _sounds.clear();
+ _error.clear();
+ reset();
+ if (!readData(stream, _data) || !isContainer(_data, MKTAG('A', 'N', 'I', 'M')))
+ return fail("Invalid Mortimer movie ANIM container");
+
+ struct Track {
+ uint16 id, flags, nextBlock;
+ MortimerSound sound;
+ Common::Array<byte> bytes;
+ };
+ Common::Array<Track> tracks;
+ uint32 pos = 8;
+ uint expectedFrames = 0;
+ int decoderWidth = 0, decoderHeight = 0;
+ while (pos < _data.size()) {
+ Chunk chunk;
+ if (!nextChunk(_data.begin(), _data.size(), pos, chunk))
+ return fail("Truncated Mortimer movie chunk");
+ const byte *src = _data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('A', 'H', 'D', 'R')) {
+ if (expectedFrames || chunk.size < 778 || READ_LE_UINT16(src) != 2)
+ return fail("Unsupported Mortimer movie header");
+ expectedFrames = READ_LE_UINT16(src + 2);
+ _frameRate = READ_LE_UINT32(src + 774);
+ if (!expectedFrames || _frameRate != 10)
+ return fail("Unsupported Mortimer movie timing");
+ memcpy(_initialPalette, src + 6, sizeof(_initialPalette));
+ memcpy(_palette, _initialPalette, sizeof(_palette));
+ } else if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
+ if (!expectedFrames)
+ return fail("Mortimer movie has no header");
+ uint32 subpos = chunk.offset;
+ uint backgrounds = 0;
+ while (subpos < chunk.offset + chunk.size) {
+ Chunk sub;
+ if (!nextChunk(_data.begin(), chunk.offset + chunk.size, subpos, sub))
+ return fail("Truncated Mortimer movie frame");
+ const byte *body = _data.begin() + sub.offset;
+ if (sub.tag == MKTAG('F', 'O', 'B', 'J')) {
+ if (!validObject(body, sub.size))
+ return fail("Unsupported or truncated Mortimer movie image");
+ if (body[0] == 37) {
+ ++backgrounds;
+ const int width = READ_LE_UINT16(body + 6), height = READ_LE_UINT16(body + 8);
+ if ((width != decoderWidth || height != decoderHeight) && body[14] != 2)
+ return fail("Mortimer movie changes dimensions without a keyframe");
+ decoderWidth = width;
+ decoderHeight = height;
+ }
+ } else if (sub.tag == MKTAG('S', 'K', 'I', 'P')) {
+ if (sub.size != 4 || READ_LE_UINT16(body) >= 256 || READ_LE_UINT16(body + 2) >= 256)
+ return fail("Invalid Mortimer object toggle");
+ } else if (sub.tag == MKTAG('I', 'A', 'C', 'T')) {
+ if (sub.size < 8)
+ return fail("Truncated Mortimer movie action");
+ const uint16 code = READ_LE_UINT16(body);
+ if ((code >= 2 && code <= 4 && (sub.size < (code == 4 ? 18U : 12U) || READ_LE_UINT16(body + 8) >= 256)) ||
+ (code == 5 && sub.size < 28))
+ return fail("Invalid Mortimer movie action");
+ if (code == 2 && READ_LE_UINT16(body + 4) == 3 && READ_LE_UINT16(body + 10) >= 256)
+ return fail("Invalid Mortimer conditional object");
+ if (code == 8 && READ_LE_UINT16(body + 2) == 46 && !validTransition(body, sub.size))
+ return fail("Invalid Mortimer movie transition");
+ } else if (sub.tag == MKTAG('X', 'P', 'A', 'L')) {
+ if (!validPalette(body, sub.size))
+ return fail("Invalid Mortimer palette change");
+ } else if (sub.tag == MKTAG('N', 'P', 'A', 'L')) {
+ if (sub.size != 768)
+ return fail("Invalid Mortimer palette");
+ } else if (sub.tag == MKTAG('S', 'T', 'O', 'R') || sub.tag == MKTAG('F', 'T', 'C', 'H')) {
+ if (sub.size != (sub.tag == MKTAG('S', 'T', 'O', 'R') ? 4U : 6U))
+ return fail("Invalid Mortimer background command");
+ } else if (sub.tag == MKTAG('P', 'S', 'A', 'D')) {
+ if (sub.size < 10)
+ return fail("Truncated Mortimer audio block");
+ const uint16 id = READ_LE_UINT16(body), block = READ_LE_UINT16(body + 2);
+ const uint16 flags = READ_LE_UINT16(body + 6) & 0xc0;
+ uint index = 0;
+ while (index < tracks.size() && (tracks[index].id != id || tracks[index].flags != flags))
+ ++index;
+ if (index == tracks.size()) {
+ Track track;
+ track.id = id;
+ track.flags = flags;
+ track.nextBlock = 0;
+ track.sound.frame = _frames.size();
+ track.sound.music = (flags & 64) != 0;
+ track.sound.speech = (flags & 128) != 0;
+ track.sound.volume = MIN(255, body[8] * 2);
+ track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
+ tracks.push_back(track);
+ }
+ if (tracks[index].nextBlock++ != block)
+ return fail("Missing Mortimer audio block");
+ Common::Array<byte> &bytes = tracks[index].bytes;
+ const uint oldSize = bytes.size();
+ bytes.resize(oldSize + sub.size - 10);
+ memcpy(bytes.begin() + oldSize, body + 10, sub.size - 10);
+ }
+ }
+ if (backgrounds > 1)
+ return fail("Invalid Mortimer movie background");
+ _frames.push_back({chunk.offset, chunk.size});
+ }
+ }
+ if (_frames.size() != expectedFrames || !expectedFrames)
+ return fail("Incomplete Mortimer movie");
+ for (uint i = 0; i < tracks.size(); ++i) {
+ if (!decodeSound(tracks[i].bytes, tracks[i].sound))
+ return fail("Unsupported or incomplete Mortimer audio");
+ _sounds.push_back(tracks[i].sound);
+ }
+ return true;
+}
+
+void MortimerMovie::reset() {
+ delete _decoder;
+ _decoder = nullptr;
+ _decoderWidth = _decoderHeight = 0;
+ _nextFrame = 0;
+ _doubled = _initialDoubled;
+ _storeNext = false;
+ _storedObject = Object();
+ _objects.clear();
+ memcpy(_palette, _initialPalette, sizeof(_palette));
+ memset(_paletteDelta, 0, sizeof(_paletteDelta));
+ memset(_shiftedPalette, 0, sizeof(_shiftedPalette));
+ memset(_pixels.begin(), 0, _pixels.size());
+ memset(_previous.begin(), 0, _previous.size());
+ for (uint i = 0; i < 256; ++i)
+ _remap[i] = i;
+}
+
+void MortimerMovie::handleAction(const byte *data, uint32 size) {
+ if (READ_LE_UINT16(data) == 6) {
+ const uint16 flags = READ_LE_UINT16(data + 2);
+ if (flags == 7 || flags == 38)
+ _doubled = flags == 38;
+ }
+}
+
+int MortimerMovie::objectScale(byte codec) const {
+ // SHELL 401392/402430 double codec37 and codec3 images.
+ // Codec1 artwork uses native screen coordinates.
+ return _doubled && codec != 1 ? 2 : 1;
+}
+
+void MortimerMovie::handlePalette(const byte *data, uint32 size) {
+ if (size == 2308) {
+ for (uint i = 0; i < 768; ++i) {
+ _shiftedPalette[i] = _palette[i] << 7;
+ _paletteDelta[i] = READ_LE_INT16(data + 4 + i * 2);
+ }
+ memcpy(_palette, data + 1540, sizeof(_palette));
+ } else {
+ for (uint i = 0; i < 768; ++i) {
+ _shiftedPalette[i] += _paletteDelta[i];
+ _palette[i] = CLIP<int32>(_shiftedPalette[i] >> 7, 0, 255);
+ }
+ }
+}
+
+bool MortimerMovie::decodeObject(Object &object) {
+ const byte *src = _data.begin() + object.offset;
+ object.codec = src[0];
+ object.x = READ_LE_INT16(src + 2);
+ object.y = READ_LE_INT16(src + 4);
+ object.width = READ_LE_UINT16(src + 6);
+ object.height = READ_LE_UINT16(src + 8);
+ object.scale = objectScale(object.codec);
+ if (object.codec == 23) {
+ if (READ_LE_UINT16(src + 12) == 0x100)
+ memcpy(_remap, src + 14, sizeof(_remap));
+ memcpy(object.remap, _remap, sizeof(_remap));
+ return true;
+ }
+ object.pixels.resize(object.width * object.height);
+ if (object.codec == 37) {
+ if (_decoderWidth != object.width || _decoderHeight != object.height) {
+ if (src[14] != 2)
+ return fail("Mortimer movie changes dimensions without a keyframe");
+ delete _decoder;
+ _decoderWidth = object.width;
+ _decoderHeight = object.height;
+ _decoder = new SmushDeltaBlocksDecoder(_decoderWidth, _decoderHeight);
+ }
+ _decoder->decode(object.pixels.begin(), src + 14);
+ } else {
+ memset(object.pixels.begin(), 0, object.pixels.size());
+ smushDecodeRLEOpaque(object.pixels.begin(), src + 14, 0, 0,
+ object.width, object.height, object.width, object.size - 14);
+ }
+ return true;
+}
+
+bool MortimerMovie::advance() {
+ if (_nextFrame >= _frames.size())
+ return false;
+ memcpy(_previous.begin(), _pixels.begin(), _pixels.size());
+ _objects.clear();
+ const Frame ¤t = _frames[_nextFrame++];
+ uint32 pos = current.offset;
+ uint16 skipA = 0xffff, skipB = 0;
+ while (pos < current.offset + current.size) {
+ Chunk chunk;
+ if (!nextChunk(_data.begin(), current.offset + current.size, pos, chunk))
+ return fail("Truncated Mortimer movie frame");
+ const byte *src = _data.begin() + chunk.offset;
+ if (chunk.tag == MKTAG('S', 'K', 'I', 'P')) {
+ skipA = READ_LE_UINT16(src);
+ skipB = READ_LE_UINT16(src + 2);
+ continue;
+ }
+ if (chunk.tag == MKTAG('I', 'A', 'C', 'T')) {
+ handleAction(src, chunk.size);
+ } else if (chunk.tag == MKTAG('X', 'P', 'A', 'L')) {
+ handlePalette(src, chunk.size);
+ } else if (chunk.tag == MKTAG('N', 'P', 'A', 'L')) {
+ memcpy(_palette, src, sizeof(_palette));
+ } else if (chunk.tag == MKTAG('S', 'T', 'O', 'R')) {
+ _storeNext = true;
+ } else if (chunk.tag == MKTAG('F', 'T', 'C', 'H')) {
+ if (!_storedObject.size)
+ return fail("Mortimer movie fetches an unstored background");
+ Object object = _storedObject;
+ object.x += READ_LE_INT16(src + 2);
+ object.y += READ_LE_INT16(src + 4);
+ object.scale = objectScale(object.codec);
+ object.skipA = skipA;
+ object.skipB = skipB;
+ _objects.push_back(object);
+ } else if (chunk.tag == MKTAG('F', 'O', 'B', 'J')) {
+ Object object;
+ object.offset = chunk.offset;
+ object.size = chunk.size;
+ object.skipA = skipA;
+ object.skipB = skipB;
+ if (!decodeObject(object))
+ return false;
+ if (_storeNext) {
+ _storedObject = object;
+ _storeNext = false;
+ }
+ _objects.push_back(object);
+ }
+ skipA = 0xffff;
+ skipB = 0;
+ }
+ render();
+ return true;
+}
+
+void MortimerMovie::render() {
+ memcpy(_pixels.begin(), _previous.begin(), _pixels.size());
+ for (uint i = 0; i < _objects.size(); ++i) {
+ const Object &object = _objects[i];
+ if (skipObject(object.skipA, object.skipB))
+ continue;
+ const int x = object.x * object.scale, y = object.y * object.scale;
+ Common::Rect clip(CLIP<int>(x, 0, kScreenWidth), CLIP<int>(y, 0, kScreenHeight),
+ CLIP<int>(x + object.width * object.scale, 0, kScreenWidth),
+ CLIP<int>(y + object.height * object.scale, 0, kScreenHeight));
+ if (clip.isEmpty())
+ continue;
+ const byte *pixels = object.pixels.begin();
+ if (object.codec == 23) {
+ _scratch.resize(object.width * object.height);
+ memset(_scratch.begin(), 0, _scratch.size());
+ for (int row = clip.top; row < clip.bottom; ++row)
+ for (int col = clip.left; col < clip.right; ++col)
+ _scratch[(row - y) / object.scale * object.width + (col - x) / object.scale] = _pixels[row * kScreenWidth + col];
+ const byte *src = _data.begin() + object.offset;
+ const uint offset = READ_LE_UINT16(src + 12) == 0x100 ? 270 : 14;
+ smushDecodeRA2SkipRemap(_scratch.begin(), src + offset, 0, 0, object.width,
+ object.height, object.width, object.size - offset, object.remap, 0);
+ pixels = _scratch.begin();
+ }
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ const byte *line = pixels + (row - y) / object.scale * object.width;
+ byte *dest = _pixels.begin() + row * kScreenWidth;
+ for (int col = clip.left; col < clip.right; ++col) {
+ const byte color = line[(col - x) / object.scale];
+ // SHELL 410160/401b20 preserve the destination for every
+ // zero pixel in both RLE codecs, including literal runs.
+ if ((object.codec != 1 && object.codec != 3) || color)
+ dest[col] = color;
+ }
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
new file mode 100644
index 00000000000..080b3d13f22
--- /dev/null
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -0,0 +1,113 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_MOVIE_H
+#define SCUMM_INSANE_MORTIMER_MOVIE_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Scumm {
+
+class SmushDeltaBlocksDecoder;
+
+struct MortimerSprite {
+ int x, y, width, height;
+ Common::Array<byte> pixels;
+};
+
+struct MortimerSound {
+ uint frame = 0;
+ uint rate = 11025;
+ byte volume = 255;
+ int8 balance = 0;
+ bool music = false;
+ bool speech = true;
+ Common::Array<byte> samples;
+};
+
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites);
+bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
+
+// Mortimer's SMUSH movie reader. Gameplay supplies the IACT and SKIP rules.
+class MortimerMovie {
+public:
+ enum { kScreenWidth = 640, kScreenHeight = 480 };
+
+ MortimerMovie();
+ virtual ~MortimerMovie();
+ bool load(Common::SeekableReadStream &stream, bool doubled = true);
+ virtual void reset();
+ virtual bool advance();
+
+ const byte *pixels() const { return _pixels.begin(); }
+ const byte *palette() const { return _palette; }
+ uint frame() const { return _nextFrame ? _nextFrame - 1 : 0; }
+ uint frameCount() const { return _frames.size(); }
+ uint frameRate() const { return _frameRate; }
+ const Common::Array<MortimerSound> &sounds() const { return _sounds; }
+ const Common::String &errorMessage() const { return _error; }
+
+protected:
+ virtual void handleAction(const byte *data, uint32 size);
+ virtual bool skipObject(uint16 a, uint16 b) const { return false; }
+ virtual int objectScale(byte codec) const;
+ void render();
+
+private:
+ struct Frame {
+ uint32 offset, size;
+ };
+ struct Object {
+ uint32 offset = 0, size = 0;
+ uint16 skipA = 0xffff, skipB = 0;
+ int x = 0, y = 0, width = 0, height = 0, scale = 1;
+ byte codec = 0;
+ byte remap[256] = {};
+ Common::Array<byte> pixels;
+ };
+
+ bool fail(const char *message);
+ void handlePalette(const byte *data, uint32 size);
+ bool decodeObject(Object &object);
+
+ Common::Array<byte> _data, _pixels, _previous, _scratch;
+ Common::Array<Frame> _frames;
+ Common::Array<Object> _objects;
+ Common::Array<MortimerSound> _sounds;
+ Object _storedObject;
+ SmushDeltaBlocksDecoder *_decoder;
+ Common::String _error;
+ byte _palette[768], _initialPalette[768], _remap[256];
+ int16 _paletteDelta[768];
+ int32 _shiftedPalette[768];
+ int _decoderWidth, _decoderHeight;
+ bool _doubled, _initialDoubled, _storeNext;
+ uint _nextFrame, _frameRate;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 6c37ae4de2f..cd60315f8ab 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -148,6 +148,7 @@ MODULE_OBJS += \
insane/insane_scenes.o \
insane/insane_iact.o \
insane/mortimer/flight.o \
+ insane/mortimer/movie.o \
insane/mortimer/mortimer.o \
insane/rebel/rebel_audio.o \
insane/rebel/rebel_gamepad.o \
Commit: bbb9d83c865c12d27b87b4cb1c9910704f0868ce
https://github.com/scummvm/scummvm/commit/bbb9d83c865c12d27b87b4cb1c9910704f0868ce
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: implemented basic interview handling
Changed paths:
A engines/scumm/insane/mortimer/interview.cpp
A engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
new file mode 100644
index 00000000000..4b107a6e0d7
--- /dev/null
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -0,0 +1,492 @@
+/* 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 "scumm/insane/mortimer/interview.h"
+
+#include "common/file.h"
+#include "common/tokenizer.h"
+#include "common/util.h"
+#include "graphics/surface.h"
+
+namespace Scumm {
+
+namespace {
+
+// SHELL 444030: the order also matches the rescued-animal flags and icons.
+const char *const kAnimals[] = {"Rhinoceros", "Baboon", "Cheetah", "Elephant", "Giraffe", "Lion", "Oxpecker"};
+const char *const kMovieNames[] = {"RHIN", "BABO", "CHEE", "ELEP", "GIRA", "LION", "OXPE"};
+const char *const kRiddleNames[] = {"RHI", "BAB", "CHE", "ELE", "GIR", "LIO", "OXP"};
+
+struct Control {
+ int id, left, top, right, bottom;
+ const char *help;
+};
+
+// SHELL 441bb0. The large text and picture regions repeat the last question.
+const Control kControls[] = {
+ {62, 9, 0, 376, 172, "HTEXTSCR"},
+ {46, 126, 187, 366, 238, "HINTVIEW"},
+ {47, 126, 239, 366, 284, "HINTVIEW"},
+ {48, 126, 285, 360, 330, "HINTVIEW"},
+ {63, 375, 65, 624, 326, "HIMAGWIN"},
+ {64, 14, 330, 140, 476, "HSPEAKER"},
+ {65, 486, 355, 587, 465, "HGLOWSTN"},
+ {67, 182, 441, 442, 459, "HSHELSHK"},
+ {66, 263, 465, 350, 479, "HSLMSCOP"},
+ {50, 396, 6, 463, 64, "HBUTTON1"},
+ {51, 493, 6, 616, 60, "HDONEBUT"},
+ {52, 10, 242, 124, 329, "HPLAYRID"},
+ {53, 8, 174, 125, 241, "HSOLVRID"},
+ {55, 147, 343, 197, 407, "HPICTURE"},
+ {56, 198, 336, 242, 407, "HPICTURE"},
+ {57, 243, 331, 286, 407, "HPICTURE"},
+ {58, 287, 330, 332, 407, "HPICTURE"},
+ {59, 333, 331, 377, 407, "HPICTURE"},
+ {60, 378, 335, 423, 407, "HPICTURE"},
+ {61, 428, 341, 469, 407, "HPICTURE"}
+};
+
+const Common::Point kQuestionOrigins[] = {{122, 191}, {120, 234}, {124, 278}};
+
+void blit(Graphics::Surface &screen, const MortimerSprite &sprite, int x, int y) {
+ x += sprite.x;
+ y += sprite.y;
+ Common::Rect clip(x, y, x + sprite.width, y + sprite.height);
+ clip.clip(Common::Rect(screen.w, screen.h));
+ if (clip.isEmpty())
+ return;
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ const byte *src = sprite.pixels.begin() + (row - y) * sprite.width;
+ byte *dst = (byte *)screen.getBasePtr(0, row);
+ for (int col = clip.left; col < clip.right; ++col)
+ if (src[col - x])
+ dst[col] = src[col - x];
+ }
+}
+
+} // End of anonymous namespace
+
+bool MortimerFont::load(const char *path) {
+ Common::File file;
+ if (!file.open(Common::Path(path)) || !loadMortimerSprites(file, _glyphs) || _glyphs.size() <= 126)
+ return false;
+ _width = _height = 0;
+ for (uint i = 0; i < _glyphs.size(); ++i) {
+ _width = MAX(_width, _glyphs[i].width);
+ _height = MAX(_height, _glyphs[i].height);
+ }
+ return true;
+}
+
+int MortimerFont::getCharWidth(uint32 chr) const {
+ return chr < _glyphs.size() ? _glyphs[chr].width : 0;
+}
+
+void MortimerFont::drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const {
+ if (chr < _glyphs.size())
+ blit(*surface, _glyphs[chr], x, y);
+}
+
+MortimerInterview::MortimerInterview() : _random("mortimer"), _stage(kIdle), _effect(kNoEffect),
+ _answer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0),
+ _species(0), _stones(0), _guesses(3), _frame(0), _switchFrame(6), _payment(0),
+ _moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false) {
+ memset(_palette, 0, sizeof(_palette));
+ memset(_gatePalette, 0, sizeof(_gatePalette));
+}
+
+bool MortimerInterview::loadText(const char *path, bool riddles) {
+ Common::File file;
+ if (!file.open(Common::Path(path)))
+ return false;
+ int animal = -1, answer = -1;
+ while (!file.eos() && !file.err()) {
+ Common::String line = file.readLine();
+ line.trim();
+ if (line.empty())
+ continue;
+ if (line[0] == 0x1a)
+ break;
+ if (line[0] == '#') {
+ const Common::String heading = line.substr(1);
+ if (heading.hasPrefix("Answer") && heading.size() == 7) {
+ answer = heading[6] - '1';
+ } else if (heading == "Easy" || heading == "Medium" || heading == "Hard") {
+ answer = heading == "Easy" ? 0 : -1;
+ } else {
+ animal = answer = -1;
+ for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i)
+ if (heading.equalsIgnoreCase(kAnimals[i]))
+ animal = i;
+ }
+ } else if (animal >= 0 && answer >= 0 && answer < 3) {
+ Common::String &text = riddles ? _riddles[animal] : _dialogue[animal][answer];
+ if (!text.empty())
+ text += '\n';
+ text += line;
+ }
+ }
+ for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i) {
+ if (riddles && _riddles[i].empty())
+ return false;
+ for (uint j = 0; !riddles && j < 3; ++j)
+ if (_dialogue[i][j].empty())
+ return false;
+ }
+ return !file.err();
+}
+
+bool MortimerInterview::loadResources() {
+ struct Bank {
+ const char *name;
+ Common::Array<MortimerSprite> *sprites;
+ uint count;
+ byte *palette;
+ };
+ const Bank banks[] = {
+ {"RESOURCE/GENERAL/INTERV_H.NUT", &_panel, 21, nullptr},
+ {"RESOURCE/SAVANNA/S_ANIM_H.NUT", &_portraits, 8, _palette},
+ {"RESOURCE/SAVANNA/S_GATE_H.NUT", &_gate, 1, _gatePalette},
+ {"RESOURCE/GENERAL/CURS_I_H.NUT", &_cursor, 4, nullptr}
+ };
+ Common::File file;
+ for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
+ if (!file.open(Common::Path(banks[i].name)) ||
+ !loadMortimerSprites(file, *banks[i].sprites, banks[i].palette) || banks[i].sprites->size() < banks[i].count) {
+ _error = Common::String::format("Missing or invalid %s", banks[i].name);
+ return false;
+ }
+ file.close();
+ }
+ if (!_font.load("RESOURCE/GENERAL/MORTFNTH.NUT") || !_captionFont.load("RESOURCE/GENERAL/SUBTFONT.NUT")) {
+ _error = "Missing or invalid Mortimer font";
+ return false;
+ }
+ if (!loadText("RESOURCE/DATA/DIALOG_F.DAT", false) || !loadText("RESOURCE/DATA/RIDDLE_F.DAT", true)) {
+ _error = "Missing or invalid Mortimer dialogue or riddle text";
+ return false;
+ }
+ if (!file.open("RESOURCE/DATA/SAV_SONG.TRS")) {
+ _error = "Missing Mortimer song captions";
+ return false;
+ }
+ uint id = 0;
+ while (!file.eos() && !file.err()) {
+ Common::String line = file.readLine();
+ line.trim();
+ if (line.hasPrefix("#define")) {
+ Common::StringTokenizer tokens(line);
+ tokens.nextToken();
+ tokens.nextToken();
+ id = atoi(tokens.nextToken().c_str());
+ } else if (line.hasPrefix("//") && id < ARRAYSIZE(_captions)) {
+ _captions[id] = line.substr(2);
+ }
+ }
+ return !file.err();
+}
+
+void MortimerInterview::resetRiddle() {
+ _answer = -1;
+}
+
+void MortimerInterview::start(uint species, uint stones, int scene) {
+ _species = species;
+ _stones = stones;
+ _guesses = 3;
+ _frame = _payment = 0;
+ _switchFrame = 6;
+ _selected = -1;
+ _question = _textScroll = 0;
+ _lastAction = 46;
+ _riddleView = _solving = _help = false;
+ _effect = kNoEffect;
+ if (_answer < 0) {
+ // SHELL 40cd65 restricts the demo to baboon, elephant and oxpecker.
+ const int answers[] = {1, 3, 6};
+ _answer = answers[_random.getRandomNumber(2)];
+ }
+ for (int i = 0; i < 7 && _selected < 0; ++i)
+ if (_species & (1 << i))
+ _selected = i;
+ switch (scene) {
+ case 4: enter(kIdle); break;
+ case 8: enter(kPopup); break;
+ case 18: enter(kSong); break;
+ case 25: enter(kEnding); break;
+ default: enter(kGate); break;
+ }
+}
+
+Common::String MortimerInterview::animalMovie(const char *pattern, int animal) const {
+ return Common::String::format(pattern, kRiddleNames[animal]);
+}
+
+void MortimerInterview::enter(Stage stage) {
+ _stage = stage;
+ _movieName.clear();
+ switch (stage) {
+ case kGate: _movieName = "RESOURCE/SAVANNA/SGATE.SAN"; break;
+ case kSong: _movieName = animalMovie("RESOURCE/SAVANNA/S%sSNG0.SAN", _answer); break;
+ case kPopup: _movieName = "RESOURCE/SAVANNA/S_POPUPH.SAN"; break;
+ case kAnswer:
+ _movieName = Common::String::format("RESOURCE/SAVANNA/S%s%d_H.SAN", kMovieNames[_selected], _question + 1);
+ break;
+ case kRiddle: _movieName = animalMovie("RESOURCE/SAVANNA/S%sRIH0.SAN", _answer); break;
+ case kSolve: _movieName = "RESOURCE/SAVANNA/S_SOLVEH.SAN"; break;
+ case kNoStones: _movieName = "RESOURCE/GENERAL/SGMOR13.SAN"; break;
+ case kGuess: _movieName = animalMovie("RESOURCE/SAVANNA/ANS_%sH.SAN", _guess); break;
+ case kCorrect: _movieName = animalMovie("RESOURCE/SAVANNA/ANS%sH0.SAN", _answer); break;
+ case kWrong: _movieName = "RESOURCE/SAVANNA/S_WRONGH.SAN"; break;
+ case kOpenGate: _movieName = "RESOURCE/SAVANNA/SGYESH.SAN"; break;
+ case kEnding: _movieName = "RESOURCE/ENDING/DEMOENDH.SAN"; break;
+ default: break;
+ }
+ _moviePending = !_movieName.empty();
+}
+
+bool MortimerInterview::fullScreen() const {
+ return _stage == kGate || _stage == kSong || _stage == kPopup || _stage == kOpenGate || _stage == kEnding || _stage == kFinished;
+}
+
+bool MortimerInterview::movieDoubled() const {
+ return _stage == kGate || _stage == kSong || _stage == kOpenGate;
+}
+
+const byte *MortimerInterview::palette(const MortimerMovie &movie) const {
+ if (playingMovie() && _stage != kHelp && _stage != kNoStones)
+ return movie.palette();
+ return _riddleView ? _gatePalette : _palette;
+}
+
+int MortimerInterview::hitControl(const Common::Point &point) const {
+ for (uint i = 0; i < ARRAYSIZE(kControls); ++i) {
+ const Control &c = kControls[i];
+ if (point.x >= c.left && point.x <= c.right && point.y >= c.top && point.y <= c.bottom) {
+ if (!_help && c.id >= 55 && c.id <= 61 && !(_species & (1 << (c.id - 55))))
+ return -1;
+ if (!_help && c.id >= 46 && c.id <= 48 && (_riddleView || _selected < 0))
+ return -1;
+ return c.id;
+ }
+ }
+ return -1;
+}
+
+void MortimerInterview::selectAnimal(int animal) {
+ if (_selected != animal || _riddleView) {
+ _switchFrame = 0;
+ _effect = kSwitchEffect;
+ }
+ _selected = animal;
+ _riddleView = false;
+ _question = _textScroll = 0;
+ _lastAction = 46;
+ enter(kIdle);
+}
+
+void MortimerInterview::click(const Common::Point &point) {
+ if (fullScreen() || _stage == kPay || _stage == kGuess || _stage == kCorrect || _stage == kWrong || _stage == kSolve || _stage == kNoStones)
+ return;
+ int action = hitControl(point);
+ if (action < 0)
+ return;
+ _effect = kButtonEffect;
+ if (action < 55 || action > 61)
+ _solving = false;
+ if (action == 50) {
+ _help = !_help;
+ enter(kIdle);
+ if (_help && !_helpExplained) {
+ _helpExplained = true;
+ enter(kHelp);
+ _movieName = "RESOURCE/GENERAL/HELP/HBUTTON1.SAN";
+ _moviePending = true;
+ }
+ return;
+ }
+ if (_help) {
+ for (uint i = 0; i < ARRAYSIZE(kControls); ++i)
+ if (kControls[i].id == action) {
+ enter(kHelp);
+ _movieName = Common::String::format("RESOURCE/GENERAL/HELP/%s.SAN", kControls[i].help);
+ _moviePending = true;
+ _help = false;
+ return;
+ }
+ }
+ if (action == 51) {
+ enter(kFlyAway);
+ return;
+ }
+ if (_selected < 0)
+ return;
+ if (action == 62 || action == 63)
+ action = _lastAction;
+ if (action >= 55 && action <= 61) {
+ if (_solving) {
+ _guess = action - 55;
+ _solving = false;
+ _payment = 5;
+ --_guesses;
+ enter(kPay);
+ } else {
+ selectAnimal(action - 55);
+ }
+ } else if (action >= 46 && action <= 48 && !_riddleView) {
+ _solving = false;
+ _question = action - 46;
+ _textScroll = 0;
+ _lastAction = action;
+ enter(kAnswer);
+ } else if (action == 52 || action == 53) {
+ _solving = false;
+ _riddleView = true;
+ _textScroll = 0;
+ _lastAction = 52;
+ enter(action == 52 ? kRiddle : kSolve);
+ }
+}
+
+void MortimerInterview::scroll(int direction) {
+ _textScroll = MAX(0, _textScroll + direction);
+}
+
+MortimerInterview::Effect MortimerInterview::takeEffect() {
+ Effect result = _effect;
+ _effect = kNoEffect;
+ return result;
+}
+
+void MortimerInterview::update() {
+ ++_frame;
+ if (_switchFrame < 6)
+ ++_switchFrame;
+ if (_stage == kPay && (_frame % 3) == 0) {
+ --_stones;
+ --_payment;
+ _effect = kPayoutEffect;
+ if (!_payment)
+ enter(kGuess);
+ }
+}
+
+void MortimerInterview::movieFinished() {
+ switch (_stage) {
+ case kGate: enter(kSong); break;
+ case kSong: enter(kPopup); break;
+ case kSolve:
+ if (_stones < 5) {
+ enter(kNoStones);
+ } else {
+ _solving = true;
+ enter(kIdle);
+ }
+ break;
+ case kGuess: enter(_guess == _answer ? kCorrect : kWrong); break;
+ case kCorrect: enter(kOpenGate); break;
+ case kWrong:
+ if (!_guesses)
+ enter(kFlyAway);
+ else
+ enter(kIdle);
+ break;
+ case kOpenGate: enter(kEnding); break;
+ case kEnding: enter(kFinished); break;
+ default: enter(kIdle); break;
+ }
+}
+
+void MortimerInterview::skip() {
+ if (playingMovie())
+ movieFinished();
+ else if (_stage != kPay)
+ enter(kFlyAway);
+}
+
+void MortimerInterview::drawText(Graphics::Surface &screen, const Common::String &text) const {
+ Common::Array<Common::String> lines;
+ _font.wordWrapText(text, 330, lines, 0, Graphics::kWordWrapOnExplicitNewLines);
+ const int height = _font.getFontHeight();
+ const int visible = MIN<int>(lines.size(), 140 / height);
+ const int first = MIN<int>(_textScroll, lines.size() - visible);
+ const int top = 96 - visible * height / 2;
+ for (int i = 0; i < visible; ++i)
+ _font.drawString(&screen, lines[first + i], 29, top + i * height, 330, 0, Graphics::kTextAlignCenter);
+}
+
+void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &movie) const {
+ if (fullScreen()) {
+ screen.copyRectToSurface(movie.pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
+ for (uint i = 0; i < movie.captions().size(); ++i) {
+ const MortimerMovie::Caption &caption = movie.captions()[i];
+ if (caption.id < ARRAYSIZE(_captions))
+ _captionFont.drawString(&screen, _captions[caption.id], 0, caption.y, MIN<int>(640, caption.x * 2), 0, Graphics::kTextAlignCenter);
+ }
+ return;
+ }
+ screen.fillRect(Common::Rect(640, 480), 10);
+ blit(screen, _riddleView ? _gate[0] : _portraits[_selected < 0 ? 7 : _selected], 354, 69);
+ if (playingMovie() && _stage != kHelp && _stage != kNoStones) {
+ for (int y = 0; y < 320 && y + 69 < screen.h; ++y) {
+ const byte *src = movie.pixels() + y * MortimerMovie::kScreenWidth;
+ byte *dst = (byte *)screen.getBasePtr(354, y + 69);
+ for (int x = 0; x < 640 - 354; ++x)
+ if (src[x])
+ dst[x] = src[x];
+ }
+ }
+ if (_switchFrame < 6)
+ blit(screen, _panel[10 + _switchFrame], 354, 69);
+ blit(screen, _panel[0], 0, 0);
+ if (_riddleView || _selected < 0) {
+ blit(screen, _panel[7], 123, 175);
+ blit(screen, _panel[1], (_frame / 4) % 2 ? 102 : 30, (_frame / 4) % 2 ? 282 : 286);
+ } else if (_selected >= 0) {
+ blit(screen, _panel[4 + _question], kQuestionOrigins[_question].x, kQuestionOrigins[_question].y);
+ }
+ if (_help)
+ blit(screen, _panel[9], 400, 6);
+ if (_solving)
+ blit(screen, _panel[2], 11, 174);
+ if (_selected >= 0)
+ drawText(screen, _riddleView ? _riddles[_answer] : _dialogue[_selected][_question]);
+}
+
+void MortimerInterview::drawCursor(Graphics::Surface &screen, const Common::Point &point) const {
+ if (!fullScreen()) {
+ if (_stage == kPay)
+ blit(screen, _panel[19 + _frame % 2], 508, 451);
+ const int cursor = (_help ? 2 : 0) + (hitControl(point) < 0 ? 1 : 0);
+ blit(screen, _cursor[cursor], point.x, point.y);
+ }
+}
+
+int MortimerInterview::highlightedAnimal() const {
+ if (_solving) {
+ int animal = (_frame / 2) % 7;
+ for (int i = 0; i < 7; ++i, animal = (animal + 1) % 7)
+ if (_species & (1 << animal))
+ return animal;
+ }
+ return _riddleView ? -1 : _selected;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
new file mode 100644
index 00000000000..404e8c57fc7
--- /dev/null
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -0,0 +1,110 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_INTERVIEW_H
+#define SCUMM_INSANE_MORTIMER_INTERVIEW_H
+
+#include "common/random.h"
+#include "graphics/font.h"
+#include "scumm/insane/mortimer/movie.h"
+
+namespace Graphics {
+struct Surface;
+}
+
+namespace Scumm {
+
+class MortimerFont : public Graphics::Font {
+public:
+ bool load(const char *path);
+ int getFontHeight() const override { return _height; }
+ int getMaxCharWidth() const override { return _width; }
+ int getCharWidth(uint32 chr) const override;
+ void drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const override;
+
+private:
+ Common::Array<MortimerSprite> _glyphs;
+ int _height = 0, _width = 0;
+};
+
+// The native interview controller in SHELL 40a884, including its riddle
+// callbacks. Movie requests are consumed by the engine after stopping audio.
+class MortimerInterview {
+public:
+ enum Effect { kNoEffect, kButtonEffect, kPayoutEffect, kSwitchEffect };
+
+ MortimerInterview();
+ bool loadResources();
+ void resetRiddle();
+ void start(uint species, uint stones, int scene = 10);
+ void click(const Common::Point &point);
+ void scroll(int direction);
+ void update();
+ void movieFinished();
+ void movieLoaded() { _moviePending = false; }
+ void skip();
+ void draw(Graphics::Surface &screen, const MortimerMovie &movie) const;
+ void drawCursor(Graphics::Surface &screen, const Common::Point &point) const;
+
+ bool needsMovie() const { return _moviePending; }
+ bool playingMovie() const { return !_movieName.empty(); }
+ bool fullScreen() const;
+ bool showDashboard() const { return !fullScreen() || _stage == kPopup; }
+ bool movieDoubled() const;
+ bool wantsFlight() const { return _stage == kFlyAway; }
+ bool finished() const { return _stage == kFinished; }
+ const Common::String &movieName() const { return _movieName; }
+ const Common::String &errorMessage() const { return _error; }
+ const byte *palette(const MortimerMovie &movie) const;
+ uint stones() const { return _stones; }
+ uint species() const { return _species; }
+ int highlightedAnimal() const;
+ Effect takeEffect();
+
+private:
+ enum Stage {
+ kGate, kSong, kPopup, kIdle, kAnswer, kRiddle, kSolve,
+ kNoStones, kPay, kGuess, kCorrect, kWrong, kOpenGate,
+ kEnding, kHelp, kFlyAway, kFinished
+ };
+
+ void enter(Stage stage);
+ void selectAnimal(int animal);
+ int hitControl(const Common::Point &point) const;
+ bool loadText(const char *path, bool riddles);
+ void drawText(Graphics::Surface &screen, const Common::String &text) const;
+ Common::String animalMovie(const char *pattern, int animal) const;
+
+ Common::RandomSource _random;
+ Common::Array<MortimerSprite> _panel, _portraits, _gate, _cursor;
+ MortimerFont _font, _captionFont;
+ Common::String _dialogue[7][3], _riddles[7], _captions[16];
+ Common::String _movieName, _error;
+ byte _palette[768], _gatePalette[768];
+ Stage _stage;
+ Effect _effect;
+ int _answer, _selected, _question, _guess, _lastAction, _textScroll;
+ uint _species, _stones, _guesses, _frame, _switchFrame, _payment;
+ bool _moviePending, _riddleView, _solving, _help, _helpExplained;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 37987aa99a2..5d98d35f8f5 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -23,6 +23,7 @@
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
#include "backends/keymapper/action.h"
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/file.h"
#include "common/system.h"
@@ -110,9 +111,9 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
- _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _segment(0), _completedRescues(0), _completedSpecies(0),
+ _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _segment(0), _carriedStones(0), _completedSpecies(0),
_shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
- _pendingGlowStones(0), _finished(false), _controllerMode(false) {
+ _pendingGlowStones(0), _finished(false), _controllerMode(false), _interviewActive(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
}
@@ -165,7 +166,9 @@ bool MortimerEngine::loadResources() {
{"RESOURCE/GENERAL/SFX/SLMGUN2.SAD", &_slimeSound},
{"RESOURCE/GENERAL/SFX/MUCKHIT1.SAD", &_slimeHitSound},
{"RESOURCE/GENERAL/SFX/GLOWIN1.SAD", &_glowSound},
- {"RESOURCE/GENERAL/SFX/BURST1.SAD", &_balloonSound}
+ {"RESOURCE/GENERAL/SFX/BURST1.SAD", &_balloonSound},
+ {"RESOURCE/GENERAL/SFX/BUTTON7.SAD", &_buttonSound},
+ {"RESOURCE/GENERAL/SFX/PAYOUT.SAD", &_payoutSound}
};
for (uint i = 0; i < ARRAYSIZE(effects); ++i) {
if (!file.open(Common::Path(effects[i].name)) || !loadMortimerSound(file, *effects[i].sound)) {
@@ -174,15 +177,19 @@ bool MortimerEngine::loadResources() {
}
file.close();
}
+ if (!_interview.loadResources()) {
+ _loadError = _interview.errorMessage();
+ return false;
+ }
return true;
}
MortimerMovie &MortimerEngine::currentMovie() {
- return _introIndex >= 0 ? _intro : _flight;
+ return _interviewActive || _introIndex >= 0 ? _intro : _flight;
}
const MortimerMovie &MortimerEngine::currentMovie() const {
- return _introIndex >= 0 ? _intro : _flight;
+ return _interviewActive || _introIndex >= 0 ? _intro : _flight;
}
bool MortimerEngine::loadIntro(uint index) {
@@ -252,10 +259,12 @@ void MortimerEngine::stopSounds() {
_mixer->stopHandle(_glowHandle);
}
-bool MortimerEngine::startMovie() {
+bool MortimerEngine::startMovie(bool preserveScreen) {
stopSounds();
MortimerMovie &movie = currentMovie();
movie.reset();
+ if (preserveScreen)
+ movie.setBackground((const byte *)_screen.getPixels());
if (!movie.advance()) {
_loadError = movie.errorMessage();
return false;
@@ -271,7 +280,7 @@ bool MortimerEngine::startFlight() {
_uiFrame = 0;
_shotFrame = 3;
_glowFrame = 10;
- _glowStones = MIN(_completedRescues, 999U);
+ _glowStones = MIN(_carriedStones, 999U);
_pendingGlowStones = 0;
memset(_iconFlash, 0, sizeof(_iconFlash));
for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
@@ -283,11 +292,15 @@ bool MortimerEngine::startFlight() {
}
bool MortimerEngine::restart(bool all) {
+ if (_interviewActive)
+ all = true;
if (_introIndex >= 0 && !all)
return startMovie();
_shellShocks = 0;
if (all) {
- _completedRescues = _completedSpecies = 0;
+ _interviewActive = false;
+ _interview.resetRiddle();
+ _carriedStones = _completedSpecies = 0;
if (!loadFlight(0))
return false;
}
@@ -296,6 +309,10 @@ bool MortimerEngine::restart(bool all) {
bool MortimerEngine::nextScene(bool skip) {
stopSounds();
+ if (_interviewActive) {
+ _interview.skip();
+ return processInterview();
+ }
if (_introIndex >= 0) {
if (uint(_introIndex + 1) < ARRAYSIZE(kDemoIntros))
return loadIntro(_introIndex + 1) && startMovie();
@@ -316,20 +333,106 @@ bool MortimerEngine::nextScene(bool skip) {
return true;
}
if (_segment + 1 == ARRAYSIZE(kDemoFlights)) {
- _finished = true;
- return true;
+ _carriedStones += _flight.rescuedCount();
+ _completedSpecies |= _flight.rescuedSpecies();
+ _glowStones = MIN(_carriedStones, 999U);
+ _pendingGlowStones = 0;
+ _glowFrame = 10;
+ _interviewActive = true;
+ _interview.start(_completedSpecies, _glowStones);
+ return processInterview();
}
- // Only follow the demo's two flight branches, never an interview or a
- // full-game resource named by another SAN file.
+ // The demo's flight movies must follow their expected SAN branches.
if (!skip && !_flight.nextFlight().equalsIgnoreCase(kDemoFlights[_segment + 1])) {
_loadError = Common::String::format("Unexpected flight transition in %s", kDemoFlights[_segment]);
return false;
}
- _completedRescues += _flight.rescuedCount();
+ _carriedStones += _flight.rescuedCount();
_completedSpecies |= _flight.rescuedSpecies();
return loadFlight(_segment + 1) && startFlight();
}
+bool MortimerEngine::processInterview() {
+ if (_interview.wantsFlight()) {
+ _carriedStones = _interview.stones();
+ _completedSpecies = _interview.species();
+ _interviewActive = false;
+ _shellShocks = 0;
+ return loadFlight(0) && startFlight();
+ }
+ if (_interview.finished()) {
+ stopSounds();
+ _finished = true;
+ return true;
+ }
+ if (_interview.needsMovie()) {
+ stopSounds();
+ _movieHandles.clear();
+ Common::File file;
+ if (!file.open(Common::Path(_interview.movieName()))) {
+ _loadError = Common::String::format("Missing %s", _interview.movieName().c_str());
+ return false;
+ }
+ if (!_intro.load(file, _interview.movieDoubled())) {
+ _loadError = Common::String::format("%s: %s", _interview.movieName().c_str(), _intro.errorMessage().c_str());
+ return false;
+ }
+ _movieHandles.resize(_intro.sounds().size());
+ _interview.movieLoaded();
+ if (!startMovie(_interview.fullScreen()))
+ return false;
+ }
+ const MortimerInterview::Effect effect = _interview.takeEffect();
+ if (effect != MortimerInterview::kNoEffect)
+ playSound(effect == MortimerInterview::kPayoutEffect ? _payoutSound :
+ (effect == MortimerInterview::kSwitchEffect ? _balloonSound : _buttonSound), _scopeHandle, Audio::Mixer::kSFXSoundType);
+ _glowStones = _interview.stones();
+ _system->getPaletteManager()->setPalette(_interview.palette(_intro), 0, 256);
+ return true;
+}
+
+bool MortimerEngine::updateScene() {
+ if (_interviewActive) {
+ if (_controllerMode) {
+ setMouse(Common::Point(_mouse.x + (_steering[1] - _steering[0]) * 18 / Common::JOYAXIS_MAX,
+ _mouse.y + (_steering[3] - _steering[2]) * 18 / Common::JOYAXIS_MAX));
+ }
+ updateInterface();
+ _interview.update();
+ const bool changed = _interview.needsMovie();
+ if (!processInterview())
+ return false;
+ if (changed || !_interview.playingMovie())
+ return true;
+ } else if (interactive()) {
+ updateSteering();
+ updateInterface();
+ }
+ MortimerMovie &movie = currentMovie();
+ if (!movie.advance()) {
+ if (!movie.errorMessage().empty()) {
+ _loadError = movie.errorMessage();
+ return false;
+ }
+ if (_interviewActive) {
+ stopSounds();
+ _interview.movieFinished();
+ return processInterview();
+ }
+ return nextScene();
+ }
+ playMovieSounds();
+ _system->getPaletteManager()->setPalette(_interviewActive ? _interview.palette(movie) : movie.palette(), 0, 256);
+ if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
+ ++_shellShocks;
+ playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
+ // SHELL 40be36 resets the flight after eighteen shell shocks.
+ if (_shellShocks >= 18)
+ return restart();
+ }
+ return true;
+}
+
void MortimerEngine::setMouse(const Common::Point &position) {
_mouse.x = CLIP<int>(position.x, 0, 639);
_mouse.y = CLIP<int>(position.y, 0, 479);
@@ -432,10 +535,24 @@ Common::Error MortimerEngine::run() {
return Common::Error(Common::kReadingFailed, _loadError);
initGraphics(640, 480);
_screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
+ _screen.fillRect(Common::Rect(640, 480), 10);
syncSoundSettings();
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
- bool exit = !(loadIntro(0) && startMovie());
+ bool exit;
+ // Use the original scene IDs for ScummVM's developer startup shortcut,
+ // with all animals rescued and enough glowstones to review the riddles.
+ const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : 0;
+ if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
+ stopSounds();
+ _introIndex = -1;
+ _interviewActive = true;
+ _interview.start(0x7f, 30, boot);
+ exit = !processInterview();
+ _nextFrameTime = _system->getMillis() + 1000 / currentMovie().frameRate();
+ } else {
+ exit = !(loadIntro(0) && startMovie());
+ }
while (!shouldQuit() && !exit) {
Common::Event event;
while (_eventMan->pollEvent(event)) {
@@ -448,11 +565,23 @@ Common::Error MortimerEngine::run() {
_steering[event.joystick.axis] = CLIP<int>(event.joystick.position, 0, Common::JOYAXIS_MAX);
if (event.joystick.position && !isPaused())
_controllerMode = true;
+ } else if (_interviewActive && !isPaused() &&
+ (event.type == Common::EVENT_WHEELUP || event.type == Common::EVENT_WHEELDOWN)) {
+ _interview.scroll(event.type == Common::EVENT_WHEELUP ? -1 : 1);
} else if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
switch (event.customType) {
case kActionFire:
- if (!isPaused() && !_finished && interactive())
- fire();
+ if (!isPaused() && !_finished) {
+ if (_interviewActive) {
+ const bool playing = _interview.playingMovie();
+ _interview.click(_mouse);
+ if (playing && !_interview.playingMovie())
+ stopSounds();
+ exit = !processInterview();
+ } else if (interactive()) {
+ fire();
+ }
+ }
break;
case kActionSkip:
if (pause.isActive())
@@ -492,30 +621,9 @@ Common::Error MortimerEngine::run() {
if (exit)
break;
if (!isPaused() && !_finished && int32(_system->getMillis() - _nextFrameTime) >= 0) {
- MortimerMovie &movie = currentMovie();
- _nextFrameTime += 1000 / movie.frameRate();
- if (interactive()) {
- updateSteering();
- updateInterface();
- }
- if (!movie.advance()) {
- if (!movie.errorMessage().empty()) {
- _loadError = movie.errorMessage();
- break;
- }
- if (!nextScene())
- break;
- } else {
- playMovieSounds();
- _system->getPaletteManager()->setPalette(movie.palette(), 0, 256);
- if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
- ++_shellShocks;
- playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
- // SHELL 40be36 resets the flight after eighteen shell shocks.
- if (_shellShocks >= 18 && !restart())
- break;
- }
- }
+ _nextFrameTime += 1000 / currentMovie().frameRate();
+ if (!updateScene())
+ break;
// Avoid racing through queued frames following a slow backend call.
if (int32(_system->getMillis() - _nextFrameTime) > 250)
_nextFrameTime = _system->getMillis() + 1000 / currentMovie().frameRate();
@@ -619,9 +727,10 @@ void MortimerEngine::drawDashboard() {
const Common::Point iconOrigins[] = {{158, 364}, {203, 358}, {248, 358}, {293, 354}, {339, 355}, {383, 360}, {428, 361}};
const Common::Point flashOrigins[] = {{154, 345}, {198, 338}, {243, 334}, {288, 332}, {333, 334}, {378, 338}, {422, 345}};
for (uint species = 0; species < 7; ++species) {
- if (_iconFlash[species] & 1)
+ if ((_iconFlash[species] & 1) || (_interviewActive && int(species) == _interview.highlightedAnimal()))
drawSprite(_dashboard[species], flashOrigins[species].x, flashOrigins[species].y);
- const bool rescued = (_completedSpecies | _flight.rescuedSpecies()) & (1 << species);
+ const uint available = _interviewActive ? _interview.species() : (_completedSpecies | _flight.rescuedSpecies());
+ const bool rescued = available & (1 << species);
drawSprite(_icons[species * 2 + (rescued ? 0 : 1)], iconOrigins[species].x, iconOrigins[species].y);
}
const uint thresholds[] = {1, 2, 3, 4, 7, 20, 30, 40, 50};
@@ -641,7 +750,12 @@ void MortimerEngine::drawDashboard() {
}
void MortimerEngine::draw() {
- if (interactive()) {
+ if (_interviewActive) {
+ _interview.draw(_screen, _intro);
+ if (_interview.showDashboard())
+ drawDashboard();
+ _interview.drawCursor(_screen, _mouse);
+ } else if (interactive()) {
_screen.fillRect(Common::Rect(640, 480), 10);
for (int y = _viewport.top; y < _viewport.bottom; ++y) {
const byte *src = _flight.pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerMovie::kScreenWidth + _view.x;
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index dbdcfacaa0c..c6b5ac10433 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -26,6 +26,7 @@
#include "audio/mixer.h"
#include "graphics/surface.h"
#include "scumm/insane/mortimer/flight.h"
+#include "scumm/insane/mortimer/interview.h"
namespace Scumm {
@@ -46,13 +47,15 @@ private:
bool loadResources();
bool loadIntro(uint index);
bool loadFlight(uint segment);
- bool startMovie();
+ bool startMovie(bool preserveScreen = false);
bool startFlight();
+ bool processInterview();
+ bool updateScene();
bool nextScene(bool skip = false);
bool restart(bool all = false);
MortimerMovie ¤tMovie();
const MortimerMovie ¤tMovie() const;
- bool interactive() const { return _introIndex < 0 && _flight.interactive(); }
+ bool interactive() const { return !_interviewActive && _introIndex < 0 && _flight.interactive(); }
void setMouse(const Common::Point &position);
void updateSteering();
void updateInterface();
@@ -71,10 +74,12 @@ private:
MortimerMovie _intro;
MortimerFlight _flight;
+ MortimerInterview _interview;
Graphics::Surface _screen;
Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons, _head;
Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound, _glowSound, _balloonSound;
+ MortimerSound _buttonSound, _payoutSound;
Common::Array<Audio::SoundHandle> _movieHandles;
Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle, _glowHandle;
Common::String _loadError;
@@ -85,10 +90,10 @@ private:
MortimerFlight::Weapon _shotWeapon;
uint32 _nextFrameTime, _pauseTime;
int _introIndex;
- uint _segment, _completedRescues, _completedSpecies, _shellShocks;
+ uint _segment, _carriedStones, _completedSpecies, _shellShocks;
uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
uint _iconFlash[7];
- bool _finished, _controllerMode;
+ bool _finished, _controllerMode, _interviewActive;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index d7f6777a151..6d7dc2da1c6 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -92,10 +92,10 @@ bool validRows(const byte *src, uint32 size, int height, bool remap) {
return pos == size || (pos + 1 == size && src[pos] == 0);
}
-// Codec37's existing decoder has no input-length argument. Validate the two
+// Codec37's existing decoder has no input-length argument. Validate the
// encodings present in the demo movies before passing any data to it.
bool validBackground(const byte *src, uint32 size, int width, int height) {
- if (size < 16 || src[1] != 1 || (src[12] != 0 && src[12] != 4))
+ if (size < 16 || src[1] != 1 || (src[12] & ~6))
return false;
uint32 pos = 16;
if (src[0] == 2) {
@@ -113,11 +113,21 @@ bool validBackground(const byte *src, uint32 size, int width, int height) {
pos += count;
remaining -= run;
}
- } else if (src[0] == 3) {
- for (uint block = 0; block < uint(width * height) / 16; ++block) {
+ } else if (src[0] == 3 || src[0] == 4) {
+ const uint blocks = uint(width * height) / 16;
+ for (uint block = 0; block < blocks; ++block) {
if (pos == size)
return false;
const byte code = src[pos++];
+ if (src[0] == 4 && code == 0) {
+ if (pos == size)
+ return false;
+ const uint run = src[pos++] + 1;
+ if (run > blocks - block)
+ return false;
+ block += run - 1;
+ continue;
+ }
const uint count = code == 0xff ? 16 : (src[12] & 4) && code == 0xfe ? 4 : (src[12] & 4) && code == 0xfd ? 1 : 0;
if (count > size - pos)
return false;
@@ -197,7 +207,7 @@ bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
} // End of anonymous namespace
-bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites) {
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites, byte *palette) {
Common::Array<byte> data;
sprites.clear();
if (!readData(stream, data) || !isContainer(data, MKTAG('A', 'N', 'I', 'M')))
@@ -207,6 +217,11 @@ bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<Morti
Chunk frame;
if (!nextChunk(data.begin(), data.size(), pos, frame))
return false;
+ if (palette && frame.tag == MKTAG('A', 'H', 'D', 'R')) {
+ if (frame.size < 774)
+ return false;
+ memcpy(palette, data.begin() + frame.offset + 6, 768);
+ }
if (frame.tag != MKTAG('F', 'R', 'M', 'E'))
continue;
uint32 subpos = frame.offset;
@@ -332,6 +347,9 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
} else if (sub.tag == MKTAG('N', 'P', 'A', 'L')) {
if (sub.size != 768)
return fail("Invalid Mortimer palette");
+ } else if (sub.tag == MKTAG('T', 'R', 'E', 'S')) {
+ if (sub.size != 18)
+ return fail("Invalid Mortimer caption");
} else if (sub.tag == MKTAG('S', 'T', 'O', 'R') || sub.tag == MKTAG('F', 'T', 'C', 'H')) {
if (sub.size != (sub.tag == MKTAG('S', 'T', 'O', 'R') ? 4U : 6U))
return fail("Invalid Mortimer background command");
@@ -387,6 +405,8 @@ void MortimerMovie::reset() {
_storeNext = false;
_storedObject = Object();
_objects.clear();
+ _captions.clear();
+ _background.clear();
memcpy(_palette, _initialPalette, sizeof(_palette));
memset(_paletteDelta, 0, sizeof(_paletteDelta));
memset(_shiftedPalette, 0, sizeof(_shiftedPalette));
@@ -396,6 +416,11 @@ void MortimerMovie::reset() {
_remap[i] = i;
}
+void MortimerMovie::setBackground(const byte *pixels) {
+ _background.resize(_pixels.size());
+ memcpy(_background.begin(), pixels, _background.size());
+}
+
void MortimerMovie::handleAction(const byte *data, uint32 size) {
if (READ_LE_UINT16(data) == 6) {
const uint16 flags = READ_LE_UINT16(data + 2);
@@ -433,6 +458,8 @@ bool MortimerMovie::decodeObject(Object &object) {
object.width = READ_LE_UINT16(src + 6);
object.height = READ_LE_UINT16(src + 8);
object.scale = objectScale(object.codec);
+ object.transparent = object.codec == 1 || object.codec == 3 ||
+ (object.codec == 37 && object.scale == 1 && (src[26] & 2));
if (object.codec == 23) {
if (READ_LE_UINT16(src + 12) == 0x100)
memcpy(_remap, src + 14, sizeof(_remap));
@@ -463,6 +490,7 @@ bool MortimerMovie::advance() {
return false;
memcpy(_previous.begin(), _pixels.begin(), _pixels.size());
_objects.clear();
+ _captions.clear();
const Frame ¤t = _frames[_nextFrame++];
uint32 pos = current.offset;
uint16 skipA = 0xffff, skipB = 0;
@@ -482,6 +510,8 @@ bool MortimerMovie::advance() {
handlePalette(src, chunk.size);
} else if (chunk.tag == MKTAG('N', 'P', 'A', 'L')) {
memcpy(_palette, src, sizeof(_palette));
+ } else if (chunk.tag == MKTAG('T', 'R', 'E', 'S')) {
+ _captions.push_back({READ_LE_INT16(src), READ_LE_INT16(src + 2), READ_LE_UINT16(src + 16)});
} else if (chunk.tag == MKTAG('S', 'T', 'O', 'R')) {
_storeNext = true;
} else if (chunk.tag == MKTAG('F', 'T', 'C', 'H')) {
@@ -491,6 +521,8 @@ bool MortimerMovie::advance() {
object.x += READ_LE_INT16(src + 2);
object.y += READ_LE_INT16(src + 4);
object.scale = objectScale(object.codec);
+ if (object.codec == 37)
+ object.transparent = object.scale == 1 && (_data[object.offset + 26] & 2);
object.skipA = skipA;
object.skipB = skipB;
_objects.push_back(object);
@@ -516,7 +548,11 @@ bool MortimerMovie::advance() {
}
void MortimerMovie::render() {
- memcpy(_pixels.begin(), _previous.begin(), _pixels.size());
+ // Transparent cutscenes compose over the scene beneath the movie, not
+ // over their own previous overlays (notably the rising interview panel).
+ // Frames containing only palette or audio commands retain the last image.
+ const byte *base = !_background.empty() && !_objects.empty() ? _background.begin() : _previous.begin();
+ memcpy(_pixels.begin(), base, _pixels.size());
for (uint i = 0; i < _objects.size(); ++i) {
const Object &object = _objects[i];
if (skipObject(object.skipA, object.skipB))
@@ -547,7 +583,7 @@ void MortimerMovie::render() {
const byte color = line[(col - x) / object.scale];
// SHELL 410160/401b20 preserve the destination for every
// zero pixel in both RLE codecs, including literal runs.
- if ((object.codec != 1 && object.codec != 3) || color)
+ if (!object.transparent || color)
dest[col] = color;
}
}
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index 080b3d13f22..49e4197c746 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -48,19 +48,24 @@ struct MortimerSound {
Common::Array<byte> samples;
};
-bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites);
+bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites, byte *palette = nullptr);
bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
// Mortimer's SMUSH movie reader. Gameplay supplies the IACT and SKIP rules.
class MortimerMovie {
public:
enum { kScreenWidth = 640, kScreenHeight = 480 };
+ struct Caption {
+ int16 x, y;
+ uint16 id;
+ };
MortimerMovie();
virtual ~MortimerMovie();
bool load(Common::SeekableReadStream &stream, bool doubled = true);
virtual void reset();
virtual bool advance();
+ void setBackground(const byte *pixels);
const byte *pixels() const { return _pixels.begin(); }
const byte *palette() const { return _palette; }
@@ -68,6 +73,7 @@ public:
uint frameCount() const { return _frames.size(); }
uint frameRate() const { return _frameRate; }
const Common::Array<MortimerSound> &sounds() const { return _sounds; }
+ const Common::Array<Caption> &captions() const { return _captions; }
const Common::String &errorMessage() const { return _error; }
protected:
@@ -85,6 +91,7 @@ private:
uint16 skipA = 0xffff, skipB = 0;
int x = 0, y = 0, width = 0, height = 0, scale = 1;
byte codec = 0;
+ bool transparent = false;
byte remap[256] = {};
Common::Array<byte> pixels;
};
@@ -93,10 +100,11 @@ private:
void handlePalette(const byte *data, uint32 size);
bool decodeObject(Object &object);
- Common::Array<byte> _data, _pixels, _previous, _scratch;
+ Common::Array<byte> _data, _pixels, _previous, _scratch, _background;
Common::Array<Frame> _frames;
Common::Array<Object> _objects;
Common::Array<MortimerSound> _sounds;
+ Common::Array<Caption> _captions;
Object _storedObject;
SmushDeltaBlocksDecoder *_decoder;
Common::String _error;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index cd60315f8ab..a480e3aa651 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -148,6 +148,7 @@ MODULE_OBJS += \
insane/insane_scenes.o \
insane/insane_iact.o \
insane/mortimer/flight.o \
+ insane/mortimer/interview.o \
insane/mortimer/movie.o \
insane/mortimer/mortimer.o \
insane/rebel/rebel_audio.o \
Commit: 1c2a61c56e18fc351c5d6a2a3899a5e69847ee0b
https://github.com/scummvm/scummvm/commit/1c2a61c56e18fc351c5d6a2a3899a5e69847ee0b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: improved video playback to avoid slowdowns
Changed paths:
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index 63971983352..b6d7c9c9582 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -45,11 +45,11 @@ void MortimerFlight::reset() {
memset(_toggles, 0, sizeof(_toggles));
}
-bool MortimerFlight::advance() {
+bool MortimerFlight::advance(bool renderFrame) {
_obstacles.clear();
for (uint i = 0; i < _targets.size(); ++i)
_targets[i].active = false;
- return MortimerMovie::advance();
+ return MortimerMovie::advance(renderFrame);
}
int MortimerFlight::objectScale(byte codec) const {
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index b7320c21427..39724a98d8c 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -41,7 +41,7 @@ public:
MortimerFlight();
bool load(Common::SeekableReadStream &stream, bool interactive = true);
void reset() override;
- bool advance() override;
+ bool advance(bool renderFrame = true) override;
ShotResult shoot(const Common::Point &point);
bool hitsObstacle(const Common::Point &point) const;
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 5d98d35f8f5..14bc609650e 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -320,13 +320,19 @@ bool MortimerEngine::nextScene(bool skip) {
}
if (skip && !_flight.interactive()) {
// Keep TITLE's music at the matching position when skipping its
- // opening; the same track continues after the cockpit appears.
+ // opening, without compositing images that will never be displayed.
while (!_flight.interactive()) {
- if (!_flight.advance()) {
+ if (!_flight.advance(false)) {
_loadError = _flight.errorMessage().empty() ? "Mortimer title has no gameplay transition" : _flight.errorMessage();
return false;
}
}
+ // The frame following the mode switch is a complete keyframe of
+ // the playable scene, so it does not need the skipped title image.
+ if (!_flight.advance()) {
+ _loadError = "Mortimer title has no gameplay frame";
+ return false;
+ }
_system->getPaletteManager()->setPalette(_flight.palette(), 0, 256);
_nextFrameTime = _system->getMillis() + 1000 / _flight.frameRate();
playMovieSounds(true);
@@ -540,9 +546,9 @@ Common::Error MortimerEngine::run() {
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
bool exit;
- // Use the original scene IDs for ScummVM's developer startup shortcut,
- // with all animals rescued and enough glowstones to review the riddles.
- const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : 0;
+ // Use the original scene IDs for ScummVM's developer startup shortcut.
+ // Interview entry points supply rescued animals and glowstones.
+ const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : -1;
if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
stopSounds();
_introIndex = -1;
@@ -550,8 +556,10 @@ Common::Error MortimerEngine::run() {
_interview.start(0x7f, 30, boot);
exit = !processInterview();
_nextFrameTime = _system->getMillis() + 1000 / currentMovie().frameRate();
+ } else if (boot == 0) {
+ exit = !(loadFlight(0) && startFlight());
} else {
- exit = !(loadIntro(0) && startMovie());
+ exit = !(loadIntro(boot >= 19 && boot <= 23 ? boot - 19 : 0) && startMovie());
}
while (!shouldQuit() && !exit) {
Common::Event event;
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 6d7dc2da1c6..d0f418ec0bc 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -24,6 +24,7 @@
#include "common/endian.h"
#include "common/stream.h"
+#include "common/system.h"
#include "common/util.h"
namespace Scumm {
@@ -275,6 +276,7 @@ bool MortimerMovie::fail(const char *message) {
}
bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
+ const uint32 started = g_system->getMillis();
_initialDoubled = doubled;
_frames.clear();
_sounds.clear();
@@ -285,8 +287,9 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
struct Track {
uint16 id, flags, nextBlock;
+ uint32 size = 0;
MortimerSound sound;
- Common::Array<byte> bytes;
+ Common::Array<Chunk> blocks;
};
Common::Array<Track> tracks;
uint32 pos = 8;
@@ -375,10 +378,8 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
}
if (tracks[index].nextBlock++ != block)
return fail("Missing Mortimer audio block");
- Common::Array<byte> &bytes = tracks[index].bytes;
- const uint oldSize = bytes.size();
- bytes.resize(oldSize + sub.size - 10);
- memcpy(bytes.begin() + oldSize, body + 10, sub.size - 10);
+ tracks[index].blocks.push_back(sub);
+ tracks[index].size += sub.size - 10;
}
}
if (backgrounds > 1)
@@ -389,10 +390,21 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
if (_frames.size() != expectedFrames || !expectedFrames)
return fail("Incomplete Mortimer movie");
for (uint i = 0; i < tracks.size(); ++i) {
- if (!decodeSound(tracks[i].bytes, tracks[i].sound))
+ // Allocate each track once. Growing an exact-sized byte array for
+ // every PSAD block repeatedly copied the entire accumulated audio.
+ Track &track = tracks[i];
+ Common::Array<byte> bytes(track.size);
+ uint32 offset = 0;
+ for (uint j = 0; j < track.blocks.size(); ++j) {
+ const Chunk &block = track.blocks[j];
+ memcpy(bytes.begin() + offset, _data.begin() + block.offset + 10, block.size - 10);
+ offset += block.size - 10;
+ }
+ if (!decodeSound(bytes, track.sound))
return fail("Unsupported or incomplete Mortimer audio");
- _sounds.push_back(tracks[i].sound);
+ _sounds.push_back(Common::move(track.sound));
}
+ debug(1, "Mortimer: loaded %u movie frames in %u ms", _frames.size(), g_system->getMillis() - started);
return true;
}
@@ -437,11 +449,14 @@ int MortimerMovie::objectScale(byte codec) const {
void MortimerMovie::handlePalette(const byte *data, uint32 size) {
if (size == 2308) {
+ // SHELL 414a14 installs XPAL's base palette before initializing the
+ // fade accumulators. Using the preceding scene's colors corrupts
+ // subsequent delta frames, even though this first frame looks right.
+ memcpy(_palette, data + 1540, sizeof(_palette));
for (uint i = 0; i < 768; ++i) {
_shiftedPalette[i] = _palette[i] << 7;
_paletteDelta[i] = READ_LE_INT16(data + 4 + i * 2);
}
- memcpy(_palette, data + 1540, sizeof(_palette));
} else {
for (uint i = 0; i < 768; ++i) {
_shiftedPalette[i] += _paletteDelta[i];
@@ -485,10 +500,11 @@ bool MortimerMovie::decodeObject(Object &object) {
return true;
}
-bool MortimerMovie::advance() {
+bool MortimerMovie::advance(bool renderFrame) {
if (_nextFrame >= _frames.size())
return false;
- memcpy(_previous.begin(), _pixels.begin(), _pixels.size());
+ if (renderFrame)
+ memcpy(_previous.begin(), _pixels.begin(), _pixels.size());
_objects.clear();
_captions.clear();
const Frame ¤t = _frames[_nextFrame++];
@@ -525,7 +541,7 @@ bool MortimerMovie::advance() {
object.transparent = object.scale == 1 && (_data[object.offset + 26] & 2);
object.skipA = skipA;
object.skipB = skipB;
- _objects.push_back(object);
+ _objects.push_back(Common::move(object));
} else if (chunk.tag == MKTAG('F', 'O', 'B', 'J')) {
Object object;
object.offset = chunk.offset;
@@ -538,12 +554,13 @@ bool MortimerMovie::advance() {
_storedObject = object;
_storeNext = false;
}
- _objects.push_back(object);
+ _objects.push_back(Common::move(object));
}
skipA = 0xffff;
skipB = 0;
}
- render();
+ if (renderFrame)
+ render();
return true;
}
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index 49e4197c746..bdab3b107a7 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -64,7 +64,8 @@ public:
virtual ~MortimerMovie();
bool load(Common::SeekableReadStream &stream, bool doubled = true);
virtual void reset();
- virtual bool advance();
+ // Suppress compositing while seeking to a frame that replaces the image.
+ virtual bool advance(bool renderFrame = true);
void setBackground(const byte *pixels);
const byte *pixels() const { return _pixels.begin(); }
Commit: c866c4ade3524ff9e11985425a6f21f324cd9d13
https://github.com/scummvm/scummvm/commit/c866c4ade3524ff9e11985425a6f21f324cd9d13
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:42+02:00
Commit Message:
SCUMM: MORTIMER: added full game detection
Changed paths:
devtools/scumm-md5.txt
engines/scumm/detection_tables.h
engines/scumm/scumm-md5.h
diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 98fbe672899..ec92e15f7a9 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -450,7 +450,8 @@ ft Full Throttle
8ead2c01c48c2e3d15d6a89b86cd84ab 19697 he DOS - - - Hebrew fan translation
mortimer Mortimer and the Riddles of the Medallion
- 3450a722d29c150a4bdac5ba9b5f3b46 290304 en Windows - Demo Windows demo launcher -
+ b6d609c97e1d089e27ef5eadd6bb3fc1 450048 en Windows - - Windows retail launcher -
+ 3450a722d29c150a4bdac5ba9b5f3b46 290304 en Windows Demo Demo Windows demo launcher -
rebel1 Star Wars: Rebel Assault
0ca9c07185b4969764eaf3ecd0de06e9 111530 en DOS Demo v0.67 Demo v0.67 Non-interactive demo -
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index d94ce23f1bb..e1f2a720abc 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -251,7 +251,8 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
- {"mortimer", 0, 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+ {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+ {"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index f5365db9848..6d8900a1659 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
/*
- This file was generated by the md5table tool on Wed Sep 30 06:55:18 2026
+ This file was generated by the md5table tool on Wed Sep 30 14:18:16 2026
DO NOT EDIT MANUALLY!
*/
@@ -196,7 +196,7 @@ static const MD5Table md5table[] = {
{ "330f631502e381a4e199a3f7cb483c20", "indy3", "EGA", "EGA", -1, Common::DE_DEU, Common::kPlatformAmiga },
{ "33e989f85da700e2014d00f345cab3d7", "puttrace", "HE 98.5", "", 32754, Common::FR_FRA, Common::kPlatformWindows },
{ "3433be9866ca4261b2d5d25374e3f243", "monkey", "VGA", "", 8347, Common::FR_FRA, Common::kPlatformAmiga },
- { "3450a722d29c150a4bdac5ba9b5f3b46", "mortimer", "", "Demo", 290304, Common::EN_ANY, Common::kPlatformWindows },
+ { "3450a722d29c150a4bdac5ba9b5f3b46", "mortimer", "Demo", "Demo", 290304, Common::EN_ANY, Common::kPlatformWindows },
{ "3486ede0f904789267d4bcc5537a46d4", "puttzoo", "", "Demo", 14337, Common::EN_ANY, Common::kPlatformMacintosh },
{ "35183fc18b1e04cdc9070078260b2c03", "zak", "V1", "", 1914, Common::EN_ANY, Common::kPlatformC64 },
{ "356fb5f680b68251333016175935d126", "BluesABCTime", "HE CUP", "Preview", 4133436, Common::UNK_LANG, Common::kPlatformUnknown },
@@ -615,6 +615,7 @@ static const MD5Table md5table[] = {
{ "b628506f7def772e40de0aa5440fb8e1", "activity", "HE 70", "", 9410, Common::EN_ANY, Common::kPlatformWindows },
{ "b6a7f83e77e33879f669d4cc5f28403c", "freddi2", "HE 80", "", 65305, Common::JA_JPN, Common::kPlatformWindows },
{ "b6b286246b05c147e3f8b15505b1648c", "freddi", "HE 73", "Demo", 9800, Common::JA_JPN, Common::kPlatformWindows },
+ { "b6d609c97e1d089e27ef5eadd6bb3fc1", "mortimer", "", "", 450048, Common::EN_ANY, Common::kPlatformWindows },
{ "b7479cd302cd82f1cd2b5a12186d1401", "atlantis", "", "Floppy", 12030, Common::RU_RUS, Common::kPlatformDOS },
{ "b7d37d6b786b5a22deea3b038eca96ca", "maniac", "NES", "", 2082, Common::ES_ESP, Common::kPlatformNES },
{ "b83cfea649a94907062e176e41279a58", "football", "", "Steam", -1, Common::EN_ANY, Common::kPlatformWindows },
Commit: 8b1b2cc97c65ce457c643e219c19a7754076db53
https://github.com/scummvm/scummvm/commit/8b1b2cc97c65ce457c643e219c19a7754076db53
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: full game intro implemented
Changed paths:
A engines/scumm/insane/mortimer/text.cpp
A engines/scumm/insane/mortimer/text.h
engines/scumm/detection_tables.h
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
engines/scumm/metaengine.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index e1f2a720abc..d3ebd48c32b 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -251,7 +251,7 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
- {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+ {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO3(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT)},
{"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 4b107a6e0d7..8baa56f2073 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -83,27 +83,6 @@ void blit(Graphics::Surface &screen, const MortimerSprite &sprite, int x, int y)
} // End of anonymous namespace
-bool MortimerFont::load(const char *path) {
- Common::File file;
- if (!file.open(Common::Path(path)) || !loadMortimerSprites(file, _glyphs) || _glyphs.size() <= 126)
- return false;
- _width = _height = 0;
- for (uint i = 0; i < _glyphs.size(); ++i) {
- _width = MAX(_width, _glyphs[i].width);
- _height = MAX(_height, _glyphs[i].height);
- }
- return true;
-}
-
-int MortimerFont::getCharWidth(uint32 chr) const {
- return chr < _glyphs.size() ? _glyphs[chr].width : 0;
-}
-
-void MortimerFont::drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const {
- if (chr < _glyphs.size())
- blit(*surface, _glyphs[chr], x, y);
-}
-
MortimerInterview::MortimerInterview() : _random("mortimer"), _stage(kIdle), _effect(kNoEffect),
_answer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0),
_species(0), _stones(0), _guesses(3), _frame(0), _switchFrame(6), _payment(0),
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index 404e8c57fc7..4487c948ba9 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -22,8 +22,7 @@
#define SCUMM_INSANE_MORTIMER_INTERVIEW_H
#include "common/random.h"
-#include "graphics/font.h"
-#include "scumm/insane/mortimer/movie.h"
+#include "scumm/insane/mortimer/text.h"
namespace Graphics {
struct Surface;
@@ -31,19 +30,6 @@ struct Surface;
namespace Scumm {
-class MortimerFont : public Graphics::Font {
-public:
- bool load(const char *path);
- int getFontHeight() const override { return _height; }
- int getMaxCharWidth() const override { return _width; }
- int getCharWidth(uint32 chr) const override;
- void drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const override;
-
-private:
- Common::Array<MortimerSprite> _glyphs;
- int _height = 0, _width = 0;
-};
-
// The native interview controller in SHELL 40a884, including its riddle
// callbacks. Movie requests are consumed by the engine after stopping audio.
class MortimerInterview {
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 14bc609650e..f620dbdd974 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -48,6 +48,16 @@ static const char *const kDemoIntros[] = {
"RESOURCE/BEGIN/INTRO5_H.SAN"
};
+const char *const kRetailIntros[] = {
+ "RESOURCE/BEGIN/GOLDGUY.SAN",
+ "RESOURCE/BEGIN/INTRO1_H.SAN",
+ "RESOURCE/BEGIN/INTRO2_H.SAN",
+ "RESOURCE/BEGIN/INTRO3_H.SAN",
+ "RESOURCE/BEGIN/INTRO4_H.SAN",
+ "RESOURCE/BEGIN/INTRO5HB.SAN",
+ "RESOURCE/BEGIN/TITLE.SAN"
+};
+
static const char *const kDemoFlights[] = {
"RESOURCE/DEMO/TITLE.SAN",
"RESOURCE/DEMO/DEMO_PL.SAN",
@@ -109,7 +119,7 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
return keymaps;
}
-MortimerEngine::MortimerEngine(OSystem *system) : Engine(system),
+MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
_nextFrameTime(0), _pauseTime(0), _introIndex(-1), _segment(0), _carriedStones(0), _completedSpecies(0),
_shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
@@ -185,24 +195,33 @@ bool MortimerEngine::loadResources() {
}
MortimerMovie &MortimerEngine::currentMovie() {
- return _interviewActive || _introIndex >= 0 ? _intro : _flight;
+ if (_interviewActive || _introIndex >= 0)
+ return _intro;
+ return _flight;
}
const MortimerMovie &MortimerEngine::currentMovie() const {
- return _interviewActive || _introIndex >= 0 ? _intro : _flight;
+ if (_interviewActive || _introIndex >= 0)
+ return _intro;
+ return _flight;
+}
+
+uint MortimerEngine::introCount() const {
+ return _isDemo ? ARRAYSIZE(kDemoIntros) : ARRAYSIZE(kRetailIntros);
}
bool MortimerEngine::loadIntro(uint index) {
stopSounds();
_movieHandles.clear();
+ const char *name = _isDemo ? kDemoIntros[index] : kRetailIntros[index];
Common::File file;
- if (!file.open(Common::Path(kDemoIntros[index]))) {
- _loadError = Common::String::format("Missing %s", kDemoIntros[index]);
+ if (!file.open(Common::Path(name))) {
+ _loadError = Common::String::format("Missing %s", name);
return false;
}
// The fifth introduction starts with native 640 x 480 animation.
- if (!_intro.load(file, index != 4)) {
- _loadError = Common::String::format("%s: %s", kDemoIntros[index], _intro.errorMessage().c_str());
+ if (!_intro.load(file, index != (_isDemo ? 4U : 5U))) {
+ _loadError = Common::String::format("%s: %s", name, _intro.errorMessage().c_str());
return false;
}
_introIndex = index;
@@ -292,6 +311,8 @@ bool MortimerEngine::startFlight() {
}
bool MortimerEngine::restart(bool all) {
+ if (!_isDemo)
+ return all ? loadIntro(0) && startMovie() : startMovie();
if (_interviewActive)
all = true;
if (_introIndex >= 0 && !all)
@@ -314,8 +335,11 @@ bool MortimerEngine::nextScene(bool skip) {
return processInterview();
}
if (_introIndex >= 0) {
- if (uint(_introIndex + 1) < ARRAYSIZE(kDemoIntros))
+ if (uint(_introIndex + 1) < introCount())
return loadIntro(_introIndex + 1) && startMovie();
+ // Retail gameplay follows the title once its campaign is implemented.
+ if (!_isDemo)
+ return false;
return loadFlight(0) && startFlight();
}
if (skip && !_flight.interactive()) {
@@ -537,8 +561,10 @@ void MortimerEngine::pauseEngineIntern(bool pause) {
}
Common::Error MortimerEngine::run() {
- if (!loadResources())
+ if (_isDemo && !loadResources())
return Common::Error(Common::kReadingFailed, _loadError);
+ if (!_isDemo && !_subtitles.load("RESOURCE/DATA/BEGIN.TRS", "RESOURCE/GENERAL/ESTRO20.NUT", "RESOURCE/GENERAL/MORTFNTH.NUT"))
+ return Common::Error(Common::kReadingFailed, "Missing or invalid Mortimer subtitle resources");
initGraphics(640, 480);
_screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
_screen.fillRect(Common::Rect(640, 480), 10);
@@ -549,7 +575,9 @@ Common::Error MortimerEngine::run() {
// Use the original scene IDs for ScummVM's developer startup shortcut.
// Interview entry points supply rescued animals and glowstones.
const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : -1;
- if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
+ if (!_isDemo) {
+ exit = !(loadIntro(boot >= 24 && boot <= 30 ? boot - 24 : 0) && startMovie());
+ } else if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
stopSounds();
_introIndex = -1;
_interviewActive = true;
@@ -781,6 +809,8 @@ void MortimerEngine::draw() {
drawProjectile();
} else {
_screen.copyRectToSurface(currentMovie().pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
+ if (!_isDemo)
+ _subtitles.draw(_screen, currentMovie().captions(), ConfMan.getBool("subtitles"));
}
if (isPaused()) {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index c6b5ac10433..3fbaa77f00c 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -34,7 +34,7 @@ namespace Scumm {
// and both Rebel Assault engines.
class MortimerEngine : public Engine {
public:
- explicit MortimerEngine(OSystem *system);
+ MortimerEngine(OSystem *system, bool isDemo);
~MortimerEngine() override;
Common::Error run() override;
bool hasFeature(EngineFeature feature) const override;
@@ -45,6 +45,7 @@ protected:
private:
bool loadResources();
+ uint introCount() const;
bool loadIntro(uint index);
bool loadFlight(uint segment);
bool startMovie(bool preserveScreen = false);
@@ -72,9 +73,11 @@ private:
void playMovieSounds(bool seek = false);
void stopSounds();
- MortimerMovie _intro;
+ const bool _isDemo;
+ MortimerCutscene _intro;
MortimerFlight _flight;
MortimerInterview _interview;
+ MortimerSubtitles _subtitles;
Graphics::Surface _screen;
Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons, _head;
Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index d0f418ec0bc..850686f1bbd 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -150,7 +150,7 @@ bool validObject(const byte *data, uint32 size, bool sprite = false) {
return false;
const byte codec = data[0];
if (sprite)
- return codec == 1 && validRows(data + 14, size - 14, height, false);
+ return codec == 44 || (codec == 1 && validRows(data + 14, size - 14, height, false));
if (codec == 37)
return !(width & 3) && !(height & 3) && validBackground(data + 14, size - 14, width, height);
if (codec != 1 && codec != 3 && codec != 23)
@@ -208,6 +208,43 @@ bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
} // End of anonymous namespace
+bool decodeMortimerGlyph(const byte *src, uint32 size, MortimerSprite &sprite) {
+ sprite.mask.resize(sprite.pixels.size());
+ memset(sprite.mask.begin(), 0, sprite.mask.size());
+ uint32 pos = 0;
+ for (int y = 0; y < sprite.height; ++y) {
+ if (size - pos < 2)
+ return false;
+ const uint length = READ_LE_UINT16(src + pos);
+ pos += 2;
+ if (length > size - pos)
+ return false;
+ const uint32 end = pos + length;
+ uint x = 0;
+ while (x < uint(sprite.width)) {
+ if (end - pos < 2)
+ return false;
+ x += READ_LE_UINT16(src + pos);
+ pos += 2;
+ if (x >= uint(sprite.width))
+ break;
+ if (end - pos < 2)
+ return false;
+ const uint count = MIN<uint>(READ_LE_UINT16(src + pos) + 1, sprite.width - x);
+ pos += 2;
+ if (count > end - pos)
+ return false;
+ const uint offset = y * sprite.width + x;
+ memcpy(sprite.pixels.begin() + offset, src + pos, count);
+ memset(sprite.mask.begin() + offset, 1, count);
+ pos += count;
+ x += count;
+ }
+ pos = end;
+ }
+ return true;
+}
+
bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites, byte *palette) {
Common::Array<byte> data;
sprites.clear();
@@ -243,7 +280,12 @@ bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<Morti
sprite.height = READ_LE_INT16(src + 8);
sprite.pixels.resize(sprite.width * sprite.height);
memset(sprite.pixels.begin(), 0, sprite.pixels.size());
- smushDecodeRLEOpaque(sprite.pixels.begin(), src + 14, 0, 0, sprite.width, sprite.height, sprite.width, chunk.size - 14);
+ if (src[0] == 44) {
+ if (!decodeMortimerGlyph(src + 14, chunk.size - 14, sprite))
+ return false;
+ } else {
+ smushDecodeRLEOpaque(sprite.pixels.begin(), src + 14, 0, 0, sprite.width, sprite.height, sprite.width, chunk.size - 14);
+ }
sprites.push_back(sprite);
}
if (sprites.size() != before + 1)
@@ -361,10 +403,19 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
return fail("Truncated Mortimer audio block");
const uint16 id = READ_LE_UINT16(body), block = READ_LE_UINT16(body + 2);
const uint16 flags = READ_LE_UINT16(body + 6) & 0xc0;
- uint index = 0;
- while (index < tracks.size() && (tracks[index].id != id || tracks[index].flags != flags))
- ++index;
- if (index == tracks.size()) {
+ int index = -1;
+ // Block zero starts a new sound, even when its ID is reused.
+ if (block) {
+ for (int i = int(tracks.size()) - 1; i >= 0; --i) {
+ if (tracks[i].id == id && tracks[i].flags == flags) {
+ index = i;
+ break;
+ }
+ }
+ if (index < 0)
+ return fail("Missing Mortimer audio block");
+ }
+ if (index < 0) {
Track track;
track.id = id;
track.flags = flags;
@@ -374,6 +425,7 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
track.sound.speech = (flags & 128) != 0;
track.sound.volume = MIN(255, body[8] * 2);
track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
+ index = tracks.size();
tracks.push_back(track);
}
if (tracks[index].nextBlock++ != block)
@@ -607,4 +659,27 @@ void MortimerMovie::render() {
}
}
+MortimerCutscene::MortimerCutscene(bool girl) : _girl(girl) {
+ reset();
+}
+
+void MortimerCutscene::reset() {
+ MortimerMovie::reset();
+ memset(_toggles, 0, sizeof(_toggles));
+}
+
+void MortimerCutscene::handleAction(const byte *data, uint32 size) {
+ MortimerMovie::handleAction(data, size);
+ if (READ_LE_UINT16(data) != 2 || READ_LE_UINT16(data + 4) != 1)
+ return;
+ const uint16 id = READ_LE_UINT16(data + 6), toggle = READ_LE_UINT16(data + 8);
+ // INTRO4_H contains both player characters under separate object toggles.
+ if (id == 3000 || id == 4000)
+ _toggles[toggle] = id == 3000 ? _girl : !_girl;
+}
+
+bool MortimerCutscene::skipObject(uint16 a, uint16 b) const {
+ return a != 0xffff && (b ? _toggles[a] != _toggles[b] : _toggles[a]);
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index bdab3b107a7..cb75407320b 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -36,6 +36,8 @@ class SmushDeltaBlocksDecoder;
struct MortimerSprite {
int x, y, width, height;
Common::Array<byte> pixels;
+ // Codec 44 uses skipped pixels for transparency and relative palette colors.
+ Common::Array<byte> mask;
};
struct MortimerSound {
@@ -117,6 +119,20 @@ private:
uint _nextFrame, _frameRate;
};
+class MortimerCutscene : public MortimerMovie {
+public:
+ explicit MortimerCutscene(bool girl = false);
+ void reset() override;
+
+protected:
+ void handleAction(const byte *data, uint32 size) override;
+ bool skipObject(uint16 a, uint16 b) const override;
+
+private:
+ const bool _girl;
+ bool _toggles[256];
+};
+
} // End of namespace Scumm
#endif
diff --git a/engines/scumm/insane/mortimer/text.cpp b/engines/scumm/insane/mortimer/text.cpp
new file mode 100644
index 00000000000..bda3d4fe25a
--- /dev/null
+++ b/engines/scumm/insane/mortimer/text.cpp
@@ -0,0 +1,147 @@
+/* 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 "scumm/insane/mortimer/text.h"
+
+#include "common/file.h"
+#include "common/tokenizer.h"
+#include "common/util.h"
+#include "graphics/surface.h"
+
+namespace Scumm {
+
+bool MortimerFont::load(const char *path) {
+ Common::File file;
+ return file.open(Common::Path(path)) && load(file);
+}
+
+bool MortimerFont::load(Common::SeekableReadStream &stream) {
+ _width = _height = 0;
+ if (!loadMortimerSprites(stream, _glyphs) || _glyphs.size() < 126)
+ return false;
+ for (uint i = 0; i < _glyphs.size(); ++i) {
+ _width = MAX(_width, _glyphs[i].width);
+ _height = MAX(_height, _glyphs[i].height);
+ }
+ return true;
+}
+
+int MortimerFont::getCharWidth(uint32 chr) const {
+ return chr < _glyphs.size() ? _glyphs[chr].width : 0;
+}
+
+void MortimerFont::drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const {
+ if (chr >= _glyphs.size())
+ return;
+ const MortimerSprite &glyph = _glyphs[chr];
+ x += glyph.x;
+ y += glyph.y;
+ Common::Rect clip(x, y, x + glyph.width, y + glyph.height);
+ clip.clip(Common::Rect(surface->w, surface->h));
+ if (clip.isEmpty())
+ return;
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ byte *dst = (byte *)surface->getBasePtr(0, row);
+ for (int col = clip.left; col < clip.right; ++col) {
+ const uint offset = (row - y) * glyph.width + col - x;
+ const byte pixel = glyph.pixels[offset];
+ if (!glyph.mask.empty()) {
+ // Mortimer's codec 44 shifts the whole glyph palette, including shading.
+ if (glyph.mask[offset])
+ dst[col] = byte(pixel + color - 1);
+ } else if (pixel) {
+ dst[col] = pixel;
+ }
+ }
+ }
+}
+
+bool MortimerSubtitles::load(const char *textPath, const char *fontPath, const char *infoFontPath) {
+ Common::File file;
+ return file.open(Common::Path(textPath)) && loadText(file) && _font.load(fontPath) && _infoFont.load(infoFontPath);
+}
+
+bool MortimerSubtitles::loadText(Common::SeekableReadStream &stream) {
+ _entries.clear();
+ int id = -1;
+ while (!stream.eos() && !stream.err()) {
+ Common::String line = stream.readLine();
+ line.trim();
+ if (line.empty())
+ continue;
+ if (line[0] == 0x1a)
+ break;
+ if (line[0] == '#') {
+ // Labels may contain spaces; the numeric ID is the final token.
+ Common::StringTokenizer tokens(line);
+ Common::String token;
+ while (!tokens.empty())
+ token = tokens.nextToken();
+ char *end;
+ const long value = strtol(token.c_str(), &end, 10);
+ if (end == token.c_str() || *end || value < 0 || value > 65535)
+ return false;
+ id = value;
+ } else if (line.hasPrefix("//") && id >= 0) {
+ line = line.substr(2);
+ Entry &entry = _entries[id];
+ if (entry.text.empty() && line.hasPrefix("$")) {
+ entry.information = true;
+ line = line.substr(1);
+ }
+ if (line.size() >= 5 && line.hasPrefix("^c") &&
+ Common::isDigit(line[2]) && Common::isDigit(line[3]) && Common::isDigit(line[4])) {
+ entry.color = atoi(line.substr(2, 3).c_str());
+ line = line.substr(5);
+ }
+ if (!entry.text.empty())
+ entry.text += '\n';
+ entry.text += line;
+ }
+ }
+ return !stream.err() && !_entries.empty();
+}
+
+void MortimerSubtitles::draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled) const {
+ for (uint i = 0; i < captions.size(); ++i) {
+ const MortimerMovie::Caption &caption = captions[i];
+ const auto found = _entries.find(caption.id);
+ if (found == _entries.end())
+ continue;
+ const Entry &entry = found->_value;
+ if ((!enabled && !entry.information) || entry.text.empty())
+ continue;
+ const MortimerFont &font = entry.information ? _infoFont : _font;
+ Common::Array<Common::String> lines;
+ const int width = font.wordWrapText(entry.text, screen.w, lines);
+ const int lineHeight = font.getFontHeight() + 1;
+ const int height = lines.size() * lineHeight - 1;
+ const int x = CLIP<int>(caption.x, width / 2, screen.w - (width + 1) / 2);
+ int y = caption.y - (entry.information ? height / 2 : 0);
+ y = MAX(0, MIN<int>(y, screen.h - height));
+ for (uint j = 0; j < lines.size(); ++j) {
+ const int lineWidth = font.getStringWidth(lines[j]);
+ font.drawString(&screen, lines[j], x - lineWidth / 2, y + j * lineHeight, lineWidth, entry.color,
+ Graphics::kTextAlignLeft, 0, false);
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/text.h b/engines/scumm/insane/mortimer/text.h
new file mode 100644
index 00000000000..53cbf1dbe05
--- /dev/null
+++ b/engines/scumm/insane/mortimer/text.h
@@ -0,0 +1,63 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_TEXT_H
+#define SCUMM_INSANE_MORTIMER_TEXT_H
+
+#include "common/hashmap.h"
+#include "graphics/font.h"
+#include "scumm/insane/mortimer/movie.h"
+
+namespace Scumm {
+
+class MortimerFont : public Graphics::Font {
+public:
+ bool load(const char *path);
+ bool load(Common::SeekableReadStream &stream);
+ int getFontHeight() const override { return _height; }
+ int getMaxCharWidth() const override { return _width; }
+ int getCharWidth(uint32 chr) const override;
+ void drawChar(Graphics::Surface *surface, uint32 chr, int x, int y, uint32 color) const override;
+
+private:
+ Common::Array<MortimerSprite> _glyphs;
+ int _height = 0, _width = 0;
+};
+
+class MortimerSubtitles {
+public:
+ bool load(const char *textPath, const char *fontPath, const char *infoFontPath);
+ bool loadText(Common::SeekableReadStream &stream);
+ void draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled) const;
+
+private:
+ struct Entry {
+ Common::String text;
+ byte color = 1;
+ bool information = false;
+ };
+
+ MortimerFont _font, _infoFont;
+ Common::HashMap<uint16, Entry> _entries;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index fb7f1e2b835..74534a55666 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -473,7 +473,7 @@ Common::Error ScummMetaEngine::createInstance(OSystem *syst, Engine **engine,
// instantiate the appropriate game engine. Hooray!
#ifdef ENABLE_SCUMM_7_8
if (res.game.id == GID_MORTIMER) {
- *engine = new MortimerEngine(syst);
+ *engine = new MortimerEngine(syst, (res.game.features & GF_DEMO) != 0);
return Common::kNoError;
}
#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index a480e3aa651..68d96c91920 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -151,6 +151,7 @@ MODULE_OBJS += \
insane/mortimer/interview.o \
insane/mortimer/movie.o \
insane/mortimer/mortimer.o \
+ insane/mortimer/text.o \
insane/rebel/rebel_audio.o \
insane/rebel/rebel_gamepad.o \
insane/rebel/rebel_touch.o \
Commit: cce591bdbf687a67bb8eaf39acaf4a6b92928a66
https://github.com/scummvm/scummvm/commit/cce591bdbf687a67bb8eaf39acaf4a6b92928a66
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: implement campaign progression
Changed paths:
A engines/scumm/insane/mortimer/campaign.cpp
A engines/scumm/insane/mortimer/campaign.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
engines/scumm/insane/mortimer/text.cpp
engines/scumm/insane/mortimer/text.h
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/campaign.cpp b/engines/scumm/insane/mortimer/campaign.cpp
new file mode 100644
index 00000000000..64978b0501e
--- /dev/null
+++ b/engines/scumm/insane/mortimer/campaign.cpp
@@ -0,0 +1,144 @@
+/* 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 "scumm/insane/mortimer/campaign.h"
+
+#include "common/util.h"
+
+namespace Scumm {
+
+const MortimerRegion kMortimerRegions[] = {
+ {"RESOURCE/SAVANNA", 'S',
+ {"RHI", "BAB", "CHE", "ELE", "GIR", "LIO", "OXP"},
+ {"Rhinoceros", "Baboon", "Cheetah", "Elephant", "Giraffe", "Lion", "Oxpecker"},
+ "RESOURCE/SAVANNA/SAVANNA1.SAN", "RESOURCE/DATA/SAV_SONG.TRS"},
+ {"RESOURCE/FOREST", 'F',
+ {"GRI", "BEA", "BAL", "FLY", "GOA", "MOO", "OPO"},
+ {"Grizzly Bear", "Beaver", "Bald Eagle", "Flying Squirrel", "Rocky Mountain Goat", "Moose", "Opossum"},
+ "RESOURCE/FOREST/FOREST1.SAN", "RESOURCE/DATA/FOR_SONG.TRS"},
+ {"RESOURCE/ICE", 'I',
+ {"WOL", "HUM", "KIL", "NAR", "POL", "TER", "WAL"},
+ {"Gray Wolf", "Humpback Whale", "Killer Whale", "Narwhal", "Polar Bear", "Arctic Tern", "Walrus"},
+ "RESOURCE/ICE/ICE1.SAN", "RESOURCE/DATA/ICE_SONG.TRS"},
+ {"RESOURCE/DESERT", 'D',
+ {"COY", "ELF", "GIL", "HOR", "LBA", "RAT", "ROA"},
+ {"Coyote", "Elf Owl", "Gila Monster", "Horned Lizard", "Long-Tongued Bat", "Rattlesnake", "Roadrunner"},
+ "RESOURCE/DESERT/DES_01.SAN", "RESOURCE/DATA/DES_SONG.TRS"}
+};
+
+const MortimerRegion &mortimerRegion(uint region) {
+ assert(region < ARRAYSIZE(kMortimerRegions));
+ return kMortimerRegions[region];
+}
+
+uint mortimerGuessCost(uint region, uint difficulty) {
+ const uint costs[3][4] = {{7, 9, 12, 15}, {12, 15, 18, 25}, {15, 18, 25, 30}};
+ assert(region < 4 && difficulty < 3);
+ return costs[difficulty][region];
+}
+
+int mortimerGuessAllowance(uint difficulty) {
+ const int guesses[] = {-1, 3, 1};
+ assert(difficulty < ARRAYSIZE(guesses));
+ return guesses[difficulty];
+}
+
+const MortimerCampaign::Scene kMortimerScenes[] = {
+ {24, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/GOLDGUY.SAN", "RESOURCE/DATA/BEGIN.TRS", true},
+ {25, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/INTRO1_H.SAN", "RESOURCE/DATA/BEGIN.TRS", true},
+ {26, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/INTRO2_H.SAN", "RESOURCE/DATA/BEGIN.TRS", true},
+ {27, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/INTRO3_H.SAN", "RESOURCE/DATA/BEGIN.TRS", true},
+ {28, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/INTRO4_H.SAN", nullptr, true},
+ {29, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/INTRO5H%c.SAN", "RESOURCE/DATA/BEGIN.TRS", false},
+ {30, 5, MortimerCampaign::kMovie, "RESOURCE/BEGIN/TITLE.SAN", "RESOURCE/DATA/BEGIN.TRS", true},
+ {0, 0, MortimerCampaign::kFlight, "RESOURCE/SAVANNA/SAVANNA1.SAN", "RESOURCE/DATA/MORTIMER.TRS", true},
+ {12, 0, MortimerCampaign::kInterview, "RESOURCE/SAVANNA/SGATE.SAN", "RESOURCE/DATA/SAV_SONG.TRS", true},
+ {17, 0, MortimerCampaign::kInterview, nullptr, nullptr, true},
+ {4, 0, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/MORTIMER.TRS", true},
+ {8, 0, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/SAV_SONG.TRS", true},
+ {31, 0, MortimerCampaign::kMovie, "RESOURCE/ENDING/MID_1.SAN", "RESOURCE/DATA/MIDTEXT.TRS", true},
+ {1, 1, MortimerCampaign::kFlight, "RESOURCE/FOREST/FOREST1.SAN", "RESOURCE/DATA/MORTIMER.TRS", true},
+ {13, 1, MortimerCampaign::kInterview, "RESOURCE/FOREST/FGATE.SAN", "RESOURCE/DATA/FOR_SONG.TRS", true},
+ {18, 1, MortimerCampaign::kInterview, nullptr, nullptr, true},
+ {5, 1, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/MORTIMER.TRS", true},
+ {9, 1, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/FOR_SONG.TRS", true},
+ {32, 1, MortimerCampaign::kMovie, "RESOURCE/ENDING/MID_2.SAN", "RESOURCE/DATA/MIDTEXT.TRS", true},
+ {2, 2, MortimerCampaign::kFlight, "RESOURCE/ICE/ICE1.SAN", "RESOURCE/DATA/MORTIMER.TRS", true},
+ {14, 2, MortimerCampaign::kInterview, "RESOURCE/ICE/IGATE.SAN", "RESOURCE/DATA/ICE_SONG.TRS", true},
+ {19, 2, MortimerCampaign::kInterview, nullptr, nullptr, true},
+ {6, 2, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/MORTIMER.TRS", true},
+ {10, 2, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/ICE_SONG.TRS", true},
+ {33, 2, MortimerCampaign::kMovie, "RESOURCE/ENDING/MID_3.SAN", "RESOURCE/DATA/MIDTEXT.TRS", true},
+ {3, 3, MortimerCampaign::kFlight, "RESOURCE/DESERT/DES_01.SAN", "RESOURCE/DATA/MORTIMER.TRS", true},
+ {15, 3, MortimerCampaign::kInterview, "RESOURCE/DESERT/DGATE.SAN", "RESOURCE/DATA/DES_SONG.TRS", true},
+ {20, 3, MortimerCampaign::kInterview, nullptr, nullptr, true},
+ {7, 3, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/MORTIMER.TRS", true},
+ {11, 3, MortimerCampaign::kInterview, nullptr, "RESOURCE/DATA/DES_SONG.TRS", true},
+ {34, 2, MortimerCampaign::kMovie, "RESOURCE/ENDING/END_1.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {35, 2, MortimerCampaign::kMovie, "RESOURCE/ENDING/END_2H%c.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {21, 4, MortimerCampaign::kCastle, "RESOURCE/CASTLE/CS_ENTRY.SAN", "RESOURCE/DATA/CAS_SONG.TRS", true},
+ {16, 4, MortimerCampaign::kCastleRiddle, "RESOURCE/CASTLE/CS_GATE.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {22, 4, MortimerCampaign::kCastleRiddle, nullptr, "RESOURCE/DATA/CAS_SONG.TRS", true},
+ {23, 4, MortimerCampaign::kCastleRiddle, "RESOURCE/CASTLE/CASTLE_I.NUT", "RESOURCE/DATA/CAS_SONG.TRS", true},
+ {36, 6, MortimerCampaign::kMovie, "RESOURCE/ENDING/END_7.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {37, 6, MortimerCampaign::kMovie, "RESOURCE/ENDING/END_8H%c.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {38, 6, MortimerCampaign::kMovie, "RESOURCE/ENDING/END_9.SAN", "RESOURCE/DATA/ENDTEXT.TRS", true},
+ {39, 6, MortimerCampaign::kCredits, "RESOURCE/ENDING/CREDITS.SAN", "RESOURCE/DATA/HISCORE.TRS", true},
+};
+
+MortimerCampaign::MortimerCampaign() : _index(0) {
+}
+
+bool MortimerCampaign::start(int sceneId) {
+ for (uint i = 0; i < ARRAYSIZE(kMortimerScenes); ++i) {
+ if (kMortimerScenes[i].id == sceneId) {
+ _index = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MortimerCampaign::advance() {
+ if (!finished())
+ ++_index;
+ return !finished();
+}
+
+bool MortimerCampaign::returnToFlight() {
+ return !finished() && scene().region < 4 && start(scene().region);
+}
+
+const MortimerCampaign::Scene &MortimerCampaign::scene() const {
+ assert(!finished());
+ return kMortimerScenes[_index];
+}
+
+Common::String MortimerCampaign::movieName(bool girl) const {
+ if (!scene().movie)
+ return Common::String();
+ return Common::String::format(scene().movie, girl ? 'G' : 'B');
+}
+
+bool MortimerCampaign::finished() const {
+ return _index >= ARRAYSIZE(kMortimerScenes);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/campaign.h b/engines/scumm/insane/mortimer/campaign.h
new file mode 100644
index 00000000000..ac6008a33c5
--- /dev/null
+++ b/engines/scumm/insane/mortimer/campaign.h
@@ -0,0 +1,68 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_CAMPAIGN_H
+#define SCUMM_INSANE_MORTIMER_CAMPAIGN_H
+
+#include "common/str.h"
+
+namespace Scumm {
+
+struct MortimerRegion {
+ const char *directory;
+ char prefix;
+ const char *animals[7];
+ const char *names[7];
+ const char *flight;
+ const char *subtitles;
+};
+
+const MortimerRegion &mortimerRegion(uint region);
+uint mortimerGuessCost(uint region, uint difficulty);
+int mortimerGuessAllowance(uint difficulty);
+
+// Retail scene IDs and ordering from SHELL's campaign tables.
+class MortimerCampaign {
+public:
+ enum Kind { kMovie, kFlight, kInterview, kCastle, kCastleRiddle, kCredits };
+ struct Scene {
+ int id;
+ uint region;
+ Kind kind;
+ const char *movie;
+ const char *subtitles;
+ bool doubled;
+ };
+
+ MortimerCampaign();
+ bool start(int scene = 24);
+ bool advance();
+ bool returnToFlight();
+ const Scene &scene() const;
+ Common::String movieName(bool girl) const;
+ bool finished() const;
+
+private:
+ uint _index;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index b6d7c9c9582..c05f5e83d8d 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -22,27 +22,37 @@
#include "common/endian.h"
#include "common/util.h"
+#include "common/tokenizer.h"
namespace Scumm {
-MortimerFlight::MortimerFlight() : _initialInteractive(true), _interactive(true) {
+MortimerFlight::MortimerFlight(bool isDemo) : _isDemo(isDemo), _initialInteractive(true), _interactive(true),
+ _inBranch(false), _steering(kWidth / 2, kHeight / 2), _collisionRadius(25) {
+ clearProgress();
reset();
}
-bool MortimerFlight::load(Common::SeekableReadStream &stream, bool interactive) {
+bool MortimerFlight::load(Common::SeekableReadStream &stream, bool interactive, bool continuation) {
_initialInteractive = interactive;
- return MortimerMovie::load(stream);
+ return MortimerMovie::load(stream, true, continuation);
}
-void MortimerFlight::reset() {
- MortimerMovie::reset();
+void MortimerFlight::clearProgress() {
_rescuedCount = _rescuedSpecies = 0;
- _nextFlight.clear();
_weapon = kRevitascope;
+ memset(_hitTargets, 0, sizeof(_hitTargets));
+ context() = MortimerMovieContext();
+}
+
+void MortimerFlight::reset() {
+ MortimerCutscene::reset();
+ if (_isDemo)
+ clearProgress();
+ _nextFlight.clear();
_interactive = _initialInteractive;
_targets.clear();
_obstacles.clear();
- memset(_toggles, 0, sizeof(_toggles));
+ _inBranch = false;
}
bool MortimerFlight::advance(bool renderFrame) {
@@ -56,8 +66,33 @@ int MortimerFlight::objectScale(byte codec) const {
return _interactive ? 1 : MortimerMovie::objectScale(codec);
}
-bool MortimerFlight::skipObject(uint16 a, uint16 b) const {
- return a != 0xffff && (b ? _toggles[a] != _toggles[b] : _toggles[a]);
+bool MortimerFlight::targetHit(uint16 id) const {
+ const uint index = id >= 9000 ? id - 8000 : id;
+ return index < ARRAYSIZE(_hitTargets) && _hitTargets[index];
+}
+
+void MortimerFlight::hitTarget(uint16 id) {
+ const uint index = id >= 9000 ? id - 8000 : id;
+ if (index < ARRAYSIZE(_hitTargets))
+ _hitTargets[index] = true;
+}
+
+bool MortimerFlight::targetsRemain(const Common::String &list) const {
+ Common::StringTokenizer lines(list, "\r\n\x1a");
+ uint count = 0;
+ while (!lines.empty() && count < 100) {
+ const Common::String line = lines.nextToken();
+ uint first = 0, last = 0;
+ const int values = sscanf(line.c_str(), "%u - %u", &first, &last);
+ if (values < 1 || first > 9399)
+ continue;
+ if (values == 1)
+ last = first;
+ for (uint id = first; id <= MIN(last, 9399U) && count < 100; ++id, ++count)
+ if (!targetHit(id))
+ return true;
+ }
+ return false;
}
MortimerFlight::Target *MortimerFlight::findTarget(uint16 id) {
@@ -68,24 +103,19 @@ MortimerFlight::Target *MortimerFlight::findTarget(uint16 id) {
}
void MortimerFlight::handleAction(const byte *data, uint32 size) {
- MortimerMovie::handleAction(data, size);
+ MortimerCutscene::handleAction(data, size);
const uint16 code = READ_LE_UINT16(data), flags = READ_LE_UINT16(data + 2);
const uint16 mode = READ_LE_UINT16(data + 4), id = READ_LE_UINT16(data + 6);
if (code >= 2 && code <= 4 && mode == 1) {
const uint16 toggle = READ_LE_UINT16(data + 8);
- if (id == 2000) {
- // Offscreen control object: the remap SKIPs compare against this
- // false toggle, rather than against the individual stone objects.
- _toggles[toggle] = false;
+ if (id >= 2000 && id < 9000)
return;
- }
- if (id >= 1000) {
+ if (id >= 1000 && id < 2000) {
Target *target = findTarget(id - 1000);
if (target)
target->remapToggle = toggle;
- // Keep the remapped silhouette for the duration of its NUT effect.
const uint duration = target && target->hitWeapon == kSlimeGun ? 7 : 11;
- _toggles[toggle] = !target || !target->hit || frame() - target->rescuedAt >= duration;
+ setToggle(toggle, !target || !target->hit || frame() - target->rescuedAt >= duration);
return;
}
Target *target = findTarget(id);
@@ -94,14 +124,14 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
fresh.id = id;
fresh.toggle = toggle;
fresh.remapToggle = 0xffff;
- fresh.rescuedAt = 0;
+ fresh.rescuedAt = frame() - 1000;
fresh.hitWeapon = kRevitascope;
- fresh.active = fresh.hit = false;
+ fresh.active = false;
+ fresh.hit = targetHit(id);
// Salt belongs to the hornet whose ID is 100 lower. Destroying a
// hornet also disables its subsequent salt objects (SHELL 407c5f).
if (id >= 800 && id < 900) {
- const Target *hornet = findTarget(id - 100);
- fresh.hit = hornet && hornet->hit;
+ fresh.hit = fresh.hit || targetHit(id - 100);
}
_targets.push_back(fresh);
target = &_targets.back();
@@ -109,7 +139,7 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
if (!target)
return;
target->toggle = toggle;
- _toggles[toggle] = target->hit;
+ setToggle(toggle, target->hit);
if (code == 4) {
const int x = READ_LE_INT16(data + 10), y = READ_LE_INT16(data + 12);
const int w = READ_LE_INT16(data + 14), h = READ_LE_INT16(data + 16);
@@ -119,10 +149,13 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
} else if (code == 3) {
target->active = false;
}
- } else if (code == 2 && mode == 3) {
- _toggles[READ_LE_UINT16(data + 8)] = !_toggles[READ_LE_UINT16(data + 10)];
- } else if (code == 5 && flags == 14 && size >= 28) {
+ } else if (code == 4 && mode == 2) {
+ const int x = READ_LE_INT16(data + 10), y = READ_LE_INT16(data + 12);
+ const int w = READ_LE_INT16(data + 14), h = READ_LE_INT16(data + 16);
+ _inBranch = _steering.x >= x && _steering.x <= x + w && _steering.y >= y && _steering.y <= y + h;
+ } else if (code == 5 && (flags == 13 || flags == 14)) {
Obstacle obstacle;
+ obstacle.outside = flags == 13;
for (uint i = 0; i < 4; ++i)
obstacle.corners[i] = Common::Point(READ_LE_INT16(data + 12 + i * 4), READ_LE_INT16(data + 14 + i * 4));
_obstacles.push_back(obstacle);
@@ -131,11 +164,19 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
_weapon = flags == 9 ? kSlimeGun : kRevitascope;
} else if (code == 6 && (flags == 11 || flags == 25)) {
_interactive = flags == 11;
- } else if (code == 8 && flags == 46) {
- _nextFlight = Common::String((const char *)data + 18, size - 21);
- for (uint i = 0; i < _nextFlight.size(); ++i)
- if (_nextFlight[i] == '\\')
- _nextFlight.setChar('/', i);
+ } else if (code == 8 && flags == 46 && size >= 21 && id / 1000 != 7) {
+ Common::String text((const char *)data + 18, size - 21);
+ if (id / 1000 == 11) {
+ context().conditions[MortimerMovieContext::kHostilesRemain] = targetsRemain(text);
+ _weapon = context().conditions[MortimerMovieContext::kHostilesRemain] ? kSlimeGun : kRevitascope;
+ } else if (id / 1000 == 12) {
+ context().conditions[MortimerMovieContext::kAnimalsRemain] = targetsRemain(text);
+ } else if (_inBranch && !text.hasPrefix("@riddles")) {
+ _nextFlight = text;
+ for (uint i = 0; i < _nextFlight.size(); ++i)
+ if (_nextFlight[i] == '\\')
+ _nextFlight.setChar('/', i);
+ }
}
}
@@ -145,24 +186,27 @@ MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
Target &target = _targets[i];
if (!target.active || target.hit || !target.rect.contains(point))
continue;
- const uint species = target.id / 100;
+ const uint species = target.id >= 9000 && target.id < 9400 ? 7 : target.id / 100;
if (!target.id || species > 9 || species == 8 ||
(species < 7 && _weapon != kRevitascope) || (species == 7 && _weapon != kSlimeGun))
continue;
target.hit = true;
+ hitTarget(target.id);
target.rescuedAt = frame();
target.hitWeapon = _weapon;
- _toggles[target.toggle] = true;
+ setToggle(target.toggle, true);
if (target.remapToggle != 0xffff)
- _toggles[target.remapToggle] = false;
+ setToggle(target.remapToggle, false);
if (species < 7) {
++_rescuedCount;
_rescuedSpecies |= 1 << species;
} else if (species == 7) {
- Target *salt = findTarget(target.id + 100);
+ const uint16 saltId = target.id % 100 + 800;
+ hitTarget(saltId);
+ Target *salt = findTarget(saltId);
if (salt) {
salt->hit = true;
- _toggles[salt->toggle] = true;
+ setToggle(salt->toggle, true);
}
}
render();
@@ -172,8 +216,8 @@ MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
}
bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
- // SHELL 40e390 uses a 50 x 50 footprint in movie coordinates.
- const Common::Rect footprint(point.x - 25, point.y - 25, point.x + 25, point.y + 25);
+ const int radius = _collisionRadius;
+ const Common::Rect footprint(point.x - radius, point.y - radius, point.x + radius, point.y + radius);
for (uint i = 0; i < _targets.size(); ++i) {
const Target &target = _targets[i];
if (target.id >= 800 && target.id < 900 && target.active && !target.hit && target.rect.intersects(footprint))
@@ -181,6 +225,22 @@ bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
}
for (uint n = 0; n < _obstacles.size(); ++n) {
const Obstacle &obstacle = _obstacles[n];
+ if (obstacle.outside) {
+ int centerX = 0, centerY = 0;
+ for (uint i = 0; i < 4; ++i) {
+ centerX += obstacle.corners[i].x;
+ centerY += obstacle.corners[i].y;
+ }
+ for (uint i = 0; i < 4; ++i) {
+ const Common::Point &a = obstacle.corners[i], &b = obstacle.corners[(i + 1) % 4];
+ const int dx = a.y - b.y, dy = b.x - a.x;
+ const int side = dx * (centerX - 4 * a.x) + dy * (centerY - 4 * a.y);
+ const int distance = dx * (point.x - a.x) + dy * (point.y - a.y);
+ if ((side < 0 ? -distance : distance) < radius * (ABS(dx) + ABS(dy)))
+ return true;
+ }
+ continue;
+ }
// Separating axes also handle vertical edges and degenerate quads
// without the divisions used by the original executable.
bool separated = false;
@@ -199,8 +259,8 @@ bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
maximum = MAX(maximum, projection);
}
const int64 center = int64(dx) * point.x + int64(dy) * point.y;
- const int64 radius = 25 * (int64(ABS(dx)) + ABS(dy));
- separated = center + radius < minimum || center - radius > maximum;
+ const int64 extent = radius * (int64(ABS(dx)) + ABS(dy));
+ separated = center + extent < minimum || center - extent > maximum;
}
if (!separated)
return true;
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 39724a98d8c..901255dc63f 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -25,7 +25,7 @@
namespace Scumm {
-class MortimerFlight : public MortimerMovie {
+class MortimerFlight : public MortimerCutscene {
public:
enum { kWidth = 384, kHeight = 288 };
enum Weapon { kRevitascope, kSlimeGun };
@@ -38,8 +38,9 @@ public:
bool active, hit;
};
- MortimerFlight();
- bool load(Common::SeekableReadStream &stream, bool interactive = true);
+ explicit MortimerFlight(bool isDemo = true);
+ bool load(Common::SeekableReadStream &stream, bool interactive = true, bool continuation = false);
+ void clearProgress();
void reset() override;
bool advance(bool renderFrame = true) override;
ShotResult shoot(const Common::Point &point);
@@ -48,27 +49,35 @@ public:
uint rescuedCount() const { return _rescuedCount; }
uint rescuedSpecies() const { return _rescuedSpecies; }
Weapon weapon() const { return _weapon; }
+ void resetWeapon() { _weapon = kRevitascope; }
const Common::String &nextFlight() const { return _nextFlight; }
const Common::Array<Target> &targets() const { return _targets; }
bool interactive() const { return _interactive; }
+ void setSteering(const Common::Point &point) { _steering = point; }
+ void setCollisionRadius(uint radius) { _collisionRadius = radius; }
private:
struct Obstacle {
Common::Point corners[4];
+ bool outside;
};
void handleAction(const byte *data, uint32 size) override;
- bool skipObject(uint16 a, uint16 b) const override;
int objectScale(byte codec) const override;
Target *findTarget(uint16 id);
+ bool targetHit(uint16 id) const;
+ void hitTarget(uint16 id);
+ bool targetsRemain(const Common::String &list) const;
Common::Array<Target> _targets;
Common::Array<Obstacle> _obstacles;
Common::String _nextFlight;
- bool _toggles[256];
- bool _initialInteractive, _interactive;
+ const bool _isDemo;
+ bool _hitTargets[1400];
+ bool _initialInteractive, _interactive, _inBranch;
+ Common::Point _steering;
Weapon _weapon;
- uint _rescuedCount, _rescuedSpecies;
+ uint _rescuedCount, _rescuedSpecies, _collisionRadius;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 8baa56f2073..896fab43901 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -19,6 +19,7 @@
*/
#include "scumm/insane/mortimer/interview.h"
+#include "scumm/insane/mortimer/campaign.h"
#include "common/file.h"
#include "common/tokenizer.h"
@@ -27,8 +28,6 @@
namespace Scumm {
-namespace {
-
// SHELL 444030: the order also matches the rescued-animal flags and icons.
const char *const kAnimals[] = {"Rhinoceros", "Baboon", "Cheetah", "Elephant", "Giraffe", "Lion", "Oxpecker"};
const char *const kMovieNames[] = {"RHIN", "BABO", "CHEE", "ELEP", "GIRA", "LION", "OXPE"};
@@ -81,12 +80,11 @@ void blit(Graphics::Surface &screen, const MortimerSprite &sprite, int x, int y)
}
}
-} // End of anonymous namespace
-
-MortimerInterview::MortimerInterview() : _random("mortimer"), _stage(kIdle), _effect(kNoEffect),
- _answer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0),
- _species(0), _stones(0), _guesses(3), _frame(0), _switchFrame(6), _payment(0),
- _moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false) {
+MortimerInterview::MortimerInterview(bool isDemo) : _random("mortimer"), _isDemo(isDemo),
+ _region(0), _difficulty(0), _textDifficulty(0), _tried(0), _stage(kIdle), _effect(kNoEffect),
+ _answer(-1), _previousAnswer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0), _guesses(3),
+ _species(0), _stones(0), _frame(0), _switchFrame(6), _payment(0),
+ _moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false), _restartRegion(false) {
memset(_palette, 0, sizeof(_palette));
memset(_gatePalette, 0, sizeof(_gatePalette));
}
@@ -108,31 +106,38 @@ bool MortimerInterview::loadText(const char *path, bool riddles) {
if (heading.hasPrefix("Answer") && heading.size() == 7) {
answer = heading[6] - '1';
} else if (heading == "Easy" || heading == "Medium" || heading == "Hard") {
- answer = heading == "Easy" ? 0 : -1;
+ answer = heading == "Easy" ? 0 : heading == "Medium" ? 1 : 2;
} else {
animal = answer = -1;
for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i)
- if (heading.equalsIgnoreCase(kAnimals[i]))
+ if (heading.equalsIgnoreCase(_isDemo ? kAnimals[i] : mortimerRegion(_region).names[i]))
animal = i;
}
} else if (animal >= 0 && answer >= 0 && answer < 3) {
- Common::String &text = riddles ? _riddles[animal] : _dialogue[animal][answer];
+ Common::String &text = riddles ? _riddles[animal][answer] : _dialogue[animal][answer];
if (!text.empty())
text += '\n';
text += line;
}
}
for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i) {
- if (riddles && _riddles[i].empty())
- return false;
- for (uint j = 0; !riddles && j < 3; ++j)
- if (_dialogue[i][j].empty())
+ for (uint j = 0; j < (riddles && _isDemo ? 1U : 3U); ++j)
+ if ((riddles ? _riddles[i][j] : _dialogue[i][j]).empty())
return false;
}
return !file.err();
}
-bool MortimerInterview::loadResources() {
+bool MortimerInterview::loadResources(uint region) {
+ _region = region;
+ for (uint i = 0; i < 7; ++i)
+ for (uint j = 0; j < 3; ++j) {
+ _dialogue[i][j].clear();
+ _riddles[i][j].clear();
+ }
+ const MortimerRegion &resources = mortimerRegion(_region);
+ const Common::String portraits = Common::String::format("%s/%c_ANIM_H.NUT", resources.directory, resources.prefix);
+ const Common::String gate = Common::String::format("%s/%c_GATE_H.NUT", resources.directory, resources.prefix);
struct Bank {
const char *name;
Common::Array<MortimerSprite> *sprites;
@@ -141,9 +146,9 @@ bool MortimerInterview::loadResources() {
};
const Bank banks[] = {
{"RESOURCE/GENERAL/INTERV_H.NUT", &_panel, 21, nullptr},
- {"RESOURCE/SAVANNA/S_ANIM_H.NUT", &_portraits, 8, _palette},
- {"RESOURCE/SAVANNA/S_GATE_H.NUT", &_gate, 1, _gatePalette},
- {"RESOURCE/GENERAL/CURS_I_H.NUT", &_cursor, 4, nullptr}
+ {portraits.c_str(), &_portraits, 8, _palette},
+ {gate.c_str(), &_gate, 1, _gatePalette},
+ {_isDemo ? "RESOURCE/GENERAL/CURS_I_H.NUT" : "RESOURCE/GENERAL/ARROWS.NUT", &_cursor, 4, nullptr}
};
Common::File file;
for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
@@ -154,7 +159,7 @@ bool MortimerInterview::loadResources() {
}
file.close();
}
- if (!_font.load("RESOURCE/GENERAL/MORTFNTH.NUT") || !_captionFont.load("RESOURCE/GENERAL/SUBTFONT.NUT")) {
+ if (!_font.load("RESOURCE/GENERAL/MORTFNTH.NUT") || !_captionFont.load(_isDemo ? "RESOURCE/GENERAL/SUBTFONT.NUT" : "RESOURCE/GENERAL/ESTRO20.NUT")) {
_error = "Missing or invalid Mortimer font";
return false;
}
@@ -162,6 +167,8 @@ bool MortimerInterview::loadResources() {
_error = "Missing or invalid Mortimer dialogue or riddle text";
return false;
}
+ if (!_isDemo)
+ return true;
if (!file.open("RESOURCE/DATA/SAV_SONG.TRS")) {
_error = "Missing Mortimer song captions";
return false;
@@ -183,13 +190,22 @@ bool MortimerInterview::loadResources() {
}
void MortimerInterview::resetRiddle() {
+ _previousAnswer = _answer;
_answer = -1;
+ resetAttempts();
+}
+
+void MortimerInterview::resetAttempts() {
+ _tried = 0;
+ _guesses = _isDemo ? 3 : mortimerGuessAllowance(_difficulty);
}
void MortimerInterview::start(uint species, uint stones, int scene) {
_species = species;
_stones = stones;
- _guesses = 3;
+ if (_isDemo || _answer < 0)
+ _guesses = _isDemo ? 3 : mortimerGuessAllowance(_difficulty);
+ _restartRegion = false;
_frame = _payment = 0;
_switchFrame = 6;
_selected = -1;
@@ -198,13 +214,27 @@ void MortimerInterview::start(uint species, uint stones, int scene) {
_riddleView = _solving = _help = false;
_effect = kNoEffect;
if (_answer < 0) {
- // SHELL 40cd65 restricts the demo to baboon, elephant and oxpecker.
- const int answers[] = {1, 3, 6};
- _answer = answers[_random.getRandomNumber(2)];
+ if (_isDemo) {
+ // SHELL 40cd65 restricts the demo to baboon, elephant and oxpecker.
+ const int answers[] = {1, 3, 6};
+ _answer = answers[_random.getRandomNumber(2)];
+ _textDifficulty = 0;
+ } else {
+ do {
+ _answer = _random.getRandomNumber(6);
+ } while (_answer == _previousAnswer);
+ const uint roll = _random.getRandomNumber(99);
+ _textDifficulty = _difficulty == 1 ? (roll < 10 ? 0 : roll < 60 ? 1 : 2) : _difficulty;
+ }
}
for (int i = 0; i < 7 && _selected < 0; ++i)
if (_species & (1 << i))
_selected = i;
+ if (!_isDemo) {
+ enter(scene == int(4 + _region) ? kPopup : scene == int(8 + _region) ? kIdle :
+ scene == int(17 + _region) ? kSong : kGate);
+ return;
+ }
switch (scene) {
case 4: enter(kIdle); break;
case 8: enter(kPopup); break;
@@ -238,9 +268,38 @@ void MortimerInterview::enter(Stage stage) {
case kEnding: _movieName = "RESOURCE/ENDING/DEMOENDH.SAN"; break;
default: break;
}
+ if (!_isDemo) {
+ const MortimerRegion &r = mortimerRegion(_region);
+ switch (stage) {
+ case kGate: _movieName = Common::String::format("%s/%cGATE.SAN", r.directory, r.prefix); break;
+ case kSong: _movieName = Common::String::format("%s/%c_SONG_%d.SAN", r.directory, r.prefix, _answer < 4 ? 1 : 2); break;
+ case kPopup: _movieName = Common::String::format("%s/%c_POPUPH.SAN", r.directory, r.prefix); break;
+ case kAnswer: _movieName = Common::String::format("%s/%c_%s_%d.SAN", r.directory, r.prefix, r.animals[_selected], _question + 1); break;
+ case kRiddle: _movieName = Common::String::format("%s/%c_RIDD_%d.SAN", r.directory, r.prefix, _answer < 4 ? 1 : 2); break;
+ case kSolve: _movieName = Common::String::format("%s/%c_ANSWER.SAN", r.directory, r.prefix); break;
+ case kCost: _movieName = Common::String::format("%s/%c_GLO_%u.SAN", r.directory, r.prefix, _difficulty); break;
+ case kGuess: _movieName = Common::String::format("%s/ANS_%sH.SAN", r.directory, r.animals[_guess]); break;
+ case kCorrect: _movieName = Common::String::format("%s/ANS%s_%u.SAN", r.directory, r.animals[_answer], _textDifficulty); break;
+ case kWrong: _movieName = Common::String::format("%s/%c_WRONGH.SAN", r.directory, r.prefix); break;
+ case kOpenGate: _movieName = Common::String::format("%s/%cGYESH.SAN", r.directory, r.prefix); break;
+ case kNoAnimals: _movieName = "RESOURCE/GENERAL/NOSCOPE.SAN"; break;
+ case kTried: _movieName = "RESOURCE/GENERAL/TRIEDIT.SAN"; break;
+ default: break;
+ }
+ }
_moviePending = !_movieName.empty();
}
+int MortimerInterview::sceneId() const {
+ if (_stage == kGate)
+ return 12 + _region;
+ if (_stage == kSong)
+ return 17 + _region;
+ if (_stage == kPopup)
+ return 4 + _region;
+ return 8 + _region;
+}
+
bool MortimerInterview::fullScreen() const {
return _stage == kGate || _stage == kSong || _stage == kPopup || _stage == kOpenGate || _stage == kEnding || _stage == kFinished;
}
@@ -259,7 +318,7 @@ int MortimerInterview::hitControl(const Common::Point &point) const {
for (uint i = 0; i < ARRAYSIZE(kControls); ++i) {
const Control &c = kControls[i];
if (point.x >= c.left && point.x <= c.right && point.y >= c.top && point.y <= c.bottom) {
- if (!_help && c.id >= 55 && c.id <= 61 && !(_species & (1 << (c.id - 55))))
+ if (_isDemo && !_help && c.id >= 55 && c.id <= 61 && !(_species & (1 << (c.id - 55))))
return -1;
if (!_help && c.id >= 46 && c.id <= 48 && (_riddleView || _selected < 0))
return -1;
@@ -282,7 +341,7 @@ void MortimerInterview::selectAnimal(int animal) {
}
void MortimerInterview::click(const Common::Point &point) {
- if (fullScreen() || _stage == kPay || _stage == kGuess || _stage == kCorrect || _stage == kWrong || _stage == kSolve || _stage == kNoStones)
+ if (fullScreen() || _stage == kPay || _stage == kGuess || _stage == kCorrect || _stage == kWrong || _stage == kSolve || _stage == kNoStones || _stage == kCost || _stage == kNoAnimals)
return;
int action = hitControl(point);
if (action < 0)
@@ -293,7 +352,7 @@ void MortimerInterview::click(const Common::Point &point) {
if (action == 50) {
_help = !_help;
enter(kIdle);
- if (_help && !_helpExplained) {
+ if (_isDemo && _help && !_helpExplained) {
_helpExplained = true;
enter(kHelp);
_movieName = "RESOURCE/GENERAL/HELP/HBUTTON1.SAN";
@@ -315,16 +374,31 @@ void MortimerInterview::click(const Common::Point &point) {
enter(kFlyAway);
return;
}
- if (_selected < 0)
+ if (_selected < 0 && _isDemo)
return;
if (action == 62 || action == 63)
- action = _lastAction;
+ action = _selected < 0 ? 52 : _lastAction;
if (action >= 55 && action <= 61) {
- if (_solving) {
+ if (!_isDemo && !(_species & (1 << (action - 55)))) {
+ enter(kHelp);
+ _movieName = "RESOURCE/GENERAL/FLYAGAIN.SAN";
+ _moviePending = true;
+ } else if (_solving) {
_guess = action - 55;
+ if (!_isDemo && _difficulty == 0 && (_tried & (1 << _guess))) {
+ enter(kTried);
+ return;
+ }
_solving = false;
- _payment = 5;
- --_guesses;
+ _payment = _isDemo ? 5 : mortimerGuessCost(_region, _difficulty);
+ if (_stones < _payment) {
+ _payment = 0;
+ enter(kNoStones);
+ return;
+ }
+ _tried |= 1 << _guess;
+ if (_guesses > 0)
+ --_guesses;
enter(kPay);
} else {
selectAnimal(action - 55);
@@ -340,7 +414,7 @@ void MortimerInterview::click(const Common::Point &point) {
_riddleView = true;
_textScroll = 0;
_lastAction = 52;
- enter(action == 52 ? kRiddle : kSolve);
+ enter(action == 52 ? kRiddle : (!_isDemo && !_species ? kNoAnimals : kSolve));
}
}
@@ -372,7 +446,13 @@ void MortimerInterview::movieFinished() {
case kGate: enter(kSong); break;
case kSong: enter(kPopup); break;
case kSolve:
- if (_stones < 5) {
+ if (!_isDemo) {
+ enter(kCost);
+ break;
+ }
+ // Fall through.
+ case kCost:
+ if (_stones < (_isDemo ? 5 : mortimerGuessCost(_region, _difficulty))) {
enter(kNoStones);
} else {
_solving = true;
@@ -382,12 +462,15 @@ void MortimerInterview::movieFinished() {
case kGuess: enter(_guess == _answer ? kCorrect : kWrong); break;
case kCorrect: enter(kOpenGate); break;
case kWrong:
- if (!_guesses)
+ if (!_guesses) {
+ _restartRegion = !_isDemo;
+ if (_restartRegion)
+ resetRiddle();
enter(kFlyAway);
- else
+ } else
enter(kIdle);
break;
- case kOpenGate: enter(kEnding); break;
+ case kOpenGate: enter(_isDemo ? kEnding : kFinished); break;
case kEnding: enter(kFinished); break;
default: enter(kIdle); break;
}
@@ -408,13 +491,13 @@ void MortimerInterview::drawText(Graphics::Surface &screen, const Common::String
const int first = MIN<int>(_textScroll, lines.size() - visible);
const int top = 96 - visible * height / 2;
for (int i = 0; i < visible; ++i)
- _font.drawString(&screen, lines[first + i], 29, top + i * height, 330, 0, Graphics::kTextAlignCenter);
+ _font.drawString(&screen, lines[first + i], 29, top + i * height, 330, _isDemo ? 0 : 1, Graphics::kTextAlignCenter);
}
void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &movie) const {
if (fullScreen()) {
screen.copyRectToSurface(movie.pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
- for (uint i = 0; i < movie.captions().size(); ++i) {
+ for (uint i = 0; _isDemo && i < movie.captions().size(); ++i) {
const MortimerMovie::Caption &caption = movie.captions()[i];
if (caption.id < ARRAYSIZE(_captions))
_captionFont.drawString(&screen, _captions[caption.id], 0, caption.y, MIN<int>(640, caption.x * 2), 0, Graphics::kTextAlignCenter);
@@ -445,8 +528,8 @@ void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &mov
blit(screen, _panel[9], 400, 6);
if (_solving)
blit(screen, _panel[2], 11, 174);
- if (_selected >= 0)
- drawText(screen, _riddleView ? _riddles[_answer] : _dialogue[_selected][_question]);
+ if (_selected >= 0 || _riddleView)
+ drawText(screen, _riddleView ? _riddles[_answer][_textDifficulty] : _dialogue[_selected][_question]);
}
void MortimerInterview::drawCursor(Graphics::Surface &screen, const Common::Point &point) const {
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index 4487c948ba9..38e63f1e183 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -36,9 +36,11 @@ class MortimerInterview {
public:
enum Effect { kNoEffect, kButtonEffect, kPayoutEffect, kSwitchEffect };
- MortimerInterview();
- bool loadResources();
+ explicit MortimerInterview(bool isDemo = true);
+ bool loadResources(uint region = 0);
+ void setDifficulty(uint difficulty) { _difficulty = difficulty; }
void resetRiddle();
+ void resetAttempts();
void start(uint species, uint stones, int scene = 10);
void click(const Common::Point &point);
void scroll(int direction);
@@ -53,9 +55,14 @@ public:
bool playingMovie() const { return !_movieName.empty(); }
bool fullScreen() const;
bool showDashboard() const { return !fullScreen() || _stage == kPopup; }
+ bool forceSubtitles() const { return _stage == kSong; }
bool movieDoubled() const;
bool wantsFlight() const { return _stage == kFlyAway; }
bool finished() const { return _stage == kFinished; }
+ bool restartsRegion() const { return _restartRegion; }
+ int sceneId() const;
+ int riddleId() const { return _answer * 10 + _textDifficulty; }
+ bool outOfGuesses() const { return _guesses == 0; }
const Common::String &movieName() const { return _movieName; }
const Common::String &errorMessage() const { return _error; }
const byte *palette(const MortimerMovie &movie) const;
@@ -68,7 +75,7 @@ private:
enum Stage {
kGate, kSong, kPopup, kIdle, kAnswer, kRiddle, kSolve,
kNoStones, kPay, kGuess, kCorrect, kWrong, kOpenGate,
- kEnding, kHelp, kFlyAway, kFinished
+ kEnding, kHelp, kFlyAway, kFinished, kCost, kNoAnimals, kTried
};
void enter(Stage stage);
@@ -79,16 +86,18 @@ private:
Common::String animalMovie(const char *pattern, int animal) const;
Common::RandomSource _random;
+ const bool _isDemo;
+ uint _region, _difficulty, _textDifficulty, _tried;
Common::Array<MortimerSprite> _panel, _portraits, _gate, _cursor;
MortimerFont _font, _captionFont;
- Common::String _dialogue[7][3], _riddles[7], _captions[16];
+ Common::String _dialogue[7][3], _riddles[7][3], _captions[16];
Common::String _movieName, _error;
byte _palette[768], _gatePalette[768];
Stage _stage;
Effect _effect;
- int _answer, _selected, _question, _guess, _lastAction, _textScroll;
- uint _species, _stones, _guesses, _frame, _switchFrame, _payment;
- bool _moviePending, _riddleView, _solving, _help, _helpExplained;
+ int _answer, _previousAnswer, _selected, _question, _guess, _lastAction, _textScroll, _guesses;
+ uint _species, _stones, _frame, _switchFrame, _payment;
+ bool _moviePending, _riddleView, _solving, _help, _helpExplained, _restartRegion;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index f620dbdd974..fc07f950470 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -24,8 +24,10 @@
#include "audio/decoders/raw.h"
#include "backends/keymapper/action.h"
#include "common/config-manager.h"
+#include "common/endian.h"
#include "common/events.h"
#include "common/file.h"
+#include "common/memstream.h"
#include "common/system.h"
#include "common/translation.h"
#include "common/util.h"
@@ -40,7 +42,7 @@ namespace Scumm {
// SHELL's demo scene list at 444220 plays all five introductions before
// TITLE.SAN. The title continues directly into the first playable scene.
-static const char *const kDemoIntros[] = {
+const char *const kDemoIntros[] = {
"RESOURCE/BEGIN/INTRO1_H.SAN",
"RESOURCE/BEGIN/INTRO2_H.SAN",
"RESOURCE/BEGIN/INTRO3_H.SAN",
@@ -48,17 +50,7 @@ static const char *const kDemoIntros[] = {
"RESOURCE/BEGIN/INTRO5_H.SAN"
};
-const char *const kRetailIntros[] = {
- "RESOURCE/BEGIN/GOLDGUY.SAN",
- "RESOURCE/BEGIN/INTRO1_H.SAN",
- "RESOURCE/BEGIN/INTRO2_H.SAN",
- "RESOURCE/BEGIN/INTRO3_H.SAN",
- "RESOURCE/BEGIN/INTRO4_H.SAN",
- "RESOURCE/BEGIN/INTRO5HB.SAN",
- "RESOURCE/BEGIN/TITLE.SAN"
-};
-
-static const char *const kDemoFlights[] = {
+const char *const kDemoFlights[] = {
"RESOURCE/DEMO/TITLE.SAN",
"RESOURCE/DEMO/DEMO_PL.SAN",
"RESOURCE/DEMO/DEMO_BT.SAN"
@@ -119,13 +111,15 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
return keymaps;
}
-MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
+MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo), _flight(isDemo), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
- _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _segment(0), _carriedStones(0), _completedSpecies(0),
+ _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0), _segment(0), _carriedStones(0), _completedSpecies(0),
_shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
- _pendingGlowStones(0), _finished(false), _controllerMode(false), _interviewActive(false) {
+ _pendingGlowStones(0), _finished(false), _controllerMode(false), _interviewActive(false), _resourcesLoaded(false), _playingCached(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
+ ConfMan.registerDefault("mortimer_flight_difficulty", 0);
+ ConfMan.registerDefault("mortimer_riddle_difficulty", 0);
}
MortimerEngine::~MortimerEngine() {
@@ -187,7 +181,7 @@ bool MortimerEngine::loadResources() {
}
file.close();
}
- if (!_interview.loadResources()) {
+ if (_isDemo && !_interview.loadResources()) {
_loadError = _interview.errorMessage();
return false;
}
@@ -207,20 +201,20 @@ const MortimerMovie &MortimerEngine::currentMovie() const {
}
uint MortimerEngine::introCount() const {
- return _isDemo ? ARRAYSIZE(kDemoIntros) : ARRAYSIZE(kRetailIntros);
+ return ARRAYSIZE(kDemoIntros);
}
bool MortimerEngine::loadIntro(uint index) {
stopSounds();
_movieHandles.clear();
- const char *name = _isDemo ? kDemoIntros[index] : kRetailIntros[index];
+ const char *name = kDemoIntros[index];
Common::File file;
if (!file.open(Common::Path(name))) {
_loadError = Common::String::format("Missing %s", name);
return false;
}
// The fifth introduction starts with native 640 x 480 animation.
- if (!_intro.load(file, index != (_isDemo ? 4U : 5U))) {
+ if (!_intro.load(file, index != 4)) {
_loadError = Common::String::format("%s: %s", name, _intro.errorMessage().c_str());
return false;
}
@@ -248,6 +242,140 @@ bool MortimerEngine::loadFlight(uint segment) {
return true;
}
+bool MortimerEngine::loadSubtitles(const char *name) {
+ if (!name || _subtitleName == name)
+ return true;
+ Common::File file;
+ if (!file.open(Common::Path(name)) || !_subtitles.loadText(file)) {
+ _loadError = Common::String::format("Missing or invalid %s", name);
+ return false;
+ }
+ _subtitleName = name;
+ return true;
+}
+
+bool MortimerEngine::loadRegion(uint region) {
+ if (_loadedRegion == int(region))
+ return true;
+ stopSounds();
+ if (!_resourcesLoaded) {
+ if (!loadResources())
+ return false;
+ _resourcesLoaded = true;
+ }
+ const MortimerRegion &resources = mortimerRegion(region);
+ const Common::String name = Common::String::format("%s/%c_ICON_H.NUT", resources.directory, resources.prefix);
+ Common::File file;
+ if (!file.open(Common::Path(name)) || !loadMortimerSprites(file, _icons) || _icons.size() < 14) {
+ _loadError = Common::String::format("Missing or invalid %s", name.c_str());
+ return false;
+ }
+ if (!_interview.loadResources(region)) {
+ _loadError = _interview.errorMessage();
+ return false;
+ }
+ _loadedRegion = region;
+ _flight.clearProgress();
+ _flight.setCollisionRadius(20 * (CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2) + 1));
+ _interview.resetRiddle();
+ _carriedStones = _completedSpecies = _creditedRescues = _shellShocks = 0;
+ return true;
+}
+
+bool MortimerEngine::loadRetailFlight(const Common::String &name, bool cached) {
+ stopSounds();
+ _movieHandles.clear();
+ bool loaded;
+ if (cached) {
+ // Loading a movie releases its LOAD buffer, so retain our own copy.
+ const Common::Array<byte> data = _flight.cachedMovie();
+ if (data.size() < 8 || READ_BE_UINT32(data.begin() + 4) != data.size() - 8) {
+ _loadError = "Incomplete Mortimer cached flight transition";
+ return false;
+ }
+ Common::MemoryReadStream stream(data.begin(), data.size());
+ loaded = _flight.load(stream, true, true);
+ } else {
+ Common::File file;
+ if (!file.open(Common::Path(name))) {
+ _loadError = Common::String::format("Missing %s", name.c_str());
+ return false;
+ }
+ loaded = _flight.load(file);
+ }
+ if (!loaded) {
+ _loadError = Common::String::format("%s: %s", name.c_str(), _flight.errorMessage().c_str());
+ return false;
+ }
+ _playingCached = cached;
+ _introIndex = -1;
+ _movieHandles.resize(_flight.sounds().size());
+ return true;
+}
+
+bool MortimerEngine::followFlightBranch() {
+ const Common::String next = _flight.nextFlight();
+ const bool cached = !_flight.cachedMovie().empty();
+ _pendingFlight = cached ? next : Common::String();
+ return loadRetailFlight(next, cached) && startMovie();
+}
+
+void MortimerEngine::updateMovieContext() {
+ _flight.setSteering(moviePoint(_aim));
+ if (_isDemo)
+ return;
+ MortimerMovieContext &context = _flight.context();
+ context.conditions[MortimerMovieContext::kHasStones] = _interviewActive ? _interview.stones() != 0 :
+ _carriedStones + _flight.rescuedCount() - _creditedRescues != 0;
+ context.conditions[MortimerMovieContext::kDamaged] = _shellShocks != 0;
+ context.conditions[MortimerMovieContext::kOutOfGuesses] = _interview.outOfGuesses();
+ context.riddle = _interview.riddleId();
+ _intro.context() = context;
+}
+
+bool MortimerEngine::enterCampaignScene() {
+ stopSounds();
+ const MortimerCampaign::Scene &scene = _campaign.scene();
+ _interviewActive = false;
+ _pendingFlight.clear();
+ _playingCached = false;
+ if (!loadSubtitles(scene.subtitles))
+ return false;
+ if (scene.kind == MortimerCampaign::kFlight) {
+ if (!loadRegion(scene.region))
+ return false;
+ _flight.context().conditions[MortimerMovieContext::kAtGate] = false;
+ return loadRetailFlight(scene.movie) && startFlight();
+ }
+ if (scene.kind == MortimerCampaign::kInterview) {
+ if (!loadRegion(scene.region))
+ return false;
+ _interviewActive = true;
+ _flight.context().conditions[MortimerMovieContext::kAtGate] = true;
+ _interview.start(_completedSpecies, _carriedStones, scene.id);
+ _glowStones = _carriedStones;
+ return processInterview();
+ }
+ if (scene.kind == MortimerCampaign::kCastle || scene.kind == MortimerCampaign::kCastleRiddle) {
+ _loadError = "Mortimer castle gameplay is not implemented yet";
+ return false;
+ }
+ _movieHandles.clear();
+ const Common::String name = _campaign.movieName(_intro.context().girl);
+ Common::File file;
+ if (!file.open(Common::Path(name))) {
+ _loadError = Common::String::format("Missing %s", name.c_str());
+ return false;
+ }
+ if (!_intro.load(file, scene.doubled)) {
+ _loadError = Common::String::format("%s: %s", name.c_str(), _intro.errorMessage().c_str());
+ return false;
+ }
+ _introIndex = scene.id;
+ _movieHandles.resize(_intro.sounds().size());
+ return startMovie();
+}
+
void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset) {
_mixer->stopHandle(handle);
// The engine owns the samples until all its handles have been stopped.
@@ -260,7 +388,7 @@ void MortimerEngine::playMovieSounds(bool seek) {
const MortimerMovie &movie = currentMovie();
for (uint i = 0; i < movie.sounds().size(); ++i) {
const MortimerSound &sound = movie.sounds()[i];
- if (sound.frame > movie.frame() || (!seek && sound.frame != movie.frame()))
+ if (!movie.soundEnabled(i) || sound.frame > movie.frame() || (!seek && sound.frame != movie.frame()))
continue;
const uint64 offset = uint64(movie.frame() - sound.frame) * sound.rate / movie.frameRate();
if (offset < sound.samples.size())
@@ -281,9 +409,11 @@ void MortimerEngine::stopSounds() {
bool MortimerEngine::startMovie(bool preserveScreen) {
stopSounds();
MortimerMovie &movie = currentMovie();
- movie.reset();
+ if (movie.started())
+ movie.reset();
if (preserveScreen)
movie.setBackground((const byte *)_screen.getPixels());
+ updateMovieContext();
if (!movie.advance()) {
_loadError = movie.errorMessage();
return false;
@@ -296,10 +426,11 @@ bool MortimerEngine::startMovie(bool preserveScreen) {
}
bool MortimerEngine::startFlight() {
+ _flight.resetWeapon();
_uiFrame = 0;
_shotFrame = 3;
_glowFrame = 10;
- _glowStones = MIN(_carriedStones, 999U);
+ _glowStones = MIN(_carriedStones + (_isDemo ? 0 : _flight.rescuedCount() - _creditedRescues), 999U);
_pendingGlowStones = 0;
memset(_iconFlash, 0, sizeof(_iconFlash));
for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
@@ -310,9 +441,30 @@ bool MortimerEngine::startFlight() {
return startMovie();
}
-bool MortimerEngine::restart(bool all) {
- if (!_isDemo)
- return all ? loadIntro(0) && startMovie() : startMovie();
+bool MortimerEngine::restart(bool all, bool crashed) {
+ if (!_isDemo) {
+ _finished = false;
+ if (all) {
+ _loadedRegion = -1;
+ _campaign.start();
+ return enterCampaignScene();
+ }
+ if (!_interviewActive && _introIndex >= 0)
+ return startMovie();
+ const uint region = _campaign.scene().region;
+ // Easy flight keeps rescues after a crash. Other flight difficulties
+ // clear the region's rescues and payments, but keep its current riddle.
+ if (!crashed || ConfMan.getInt("mortimer_flight_difficulty") > 0) {
+ _flight.clearProgress();
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
+ if (crashed)
+ _interview.resetAttempts();
+ else
+ _interview.resetRiddle();
+ }
+ _shellShocks = 0;
+ return _campaign.start(region) && enterCampaignScene();
+ }
if (_interviewActive)
all = true;
if (_introIndex >= 0 && !all)
@@ -334,12 +486,29 @@ bool MortimerEngine::nextScene(bool skip) {
_interview.skip();
return processInterview();
}
+ if (!_isDemo) {
+ if (_introIndex < 0) {
+ if (!skip && !_flight.nextFlight().empty())
+ return followFlightBranch();
+ if (!skip && _playingCached) {
+ const Common::String next = _pendingFlight;
+ _pendingFlight.clear();
+ return loadRetailFlight(next) && startMovie();
+ }
+ _carriedStones = MIN(999U, _carriedStones + _flight.rescuedCount() - _creditedRescues);
+ _creditedRescues = _flight.rescuedCount();
+ _completedSpecies |= _flight.rescuedSpecies();
+ _flight.context().conditions[MortimerMovieContext::kAtGate] = true;
+ }
+ if (!_campaign.advance()) {
+ _finished = true;
+ return true;
+ }
+ return enterCampaignScene();
+ }
if (_introIndex >= 0) {
if (uint(_introIndex + 1) < introCount())
return loadIntro(_introIndex + 1) && startMovie();
- // Retail gameplay follows the title once its campaign is implemented.
- if (!_isDemo)
- return false;
return loadFlight(0) && startFlight();
}
if (skip && !_flight.interactive()) {
@@ -388,13 +557,34 @@ bool MortimerEngine::processInterview() {
_completedSpecies = _interview.species();
_interviewActive = false;
_shellShocks = 0;
+ if (!_isDemo) {
+ if (_interview.restartsRegion()) {
+ _flight.clearProgress();
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
+ }
+ return _campaign.returnToFlight() && enterCampaignScene();
+ }
return loadFlight(0) && startFlight();
}
if (_interview.finished()) {
stopSounds();
+ if (!_isDemo) {
+ _interviewActive = false;
+ _carriedStones = 0;
+ _campaign.start(8 + _loadedRegion);
+ _campaign.advance();
+ return enterCampaignScene();
+ }
_finished = true;
return true;
}
+ if (!_isDemo) {
+ _campaign.start(_interview.sceneId());
+ const char *subtitles = _interview.fullScreen() ? mortimerRegion(_loadedRegion).subtitles : "RESOURCE/DATA/MORTIMER.TRS";
+ if (!loadSubtitles(subtitles))
+ return false;
+ updateMovieContext();
+ }
if (_interview.needsMovie()) {
stopSounds();
_movieHandles.clear();
@@ -438,6 +628,7 @@ bool MortimerEngine::updateScene() {
updateSteering();
updateInterface();
}
+ updateMovieContext();
MortimerMovie &movie = currentMovie();
if (!movie.advance()) {
if (!movie.errorMessage().empty()) {
@@ -451,14 +642,16 @@ bool MortimerEngine::updateScene() {
}
return nextScene();
}
+ if (!_isDemo && !_interviewActive && _introIndex < 0 && !_flight.nextFlight().empty())
+ return followFlightBranch();
playMovieSounds();
_system->getPaletteManager()->setPalette(_interviewActive ? _interview.palette(movie) : movie.palette(), 0, 256);
if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
++_shellShocks;
playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
- // SHELL 40be36 resets the flight after eighteen shell shocks.
- if (_shellShocks >= 18)
- return restart();
+ // Retail crashes on the fifteenth hit; the demo allows eighteen.
+ if (_shellShocks >= (_isDemo ? 18U : 15U))
+ return restart(false, true);
}
return true;
}
@@ -561,7 +754,7 @@ void MortimerEngine::pauseEngineIntern(bool pause) {
}
Common::Error MortimerEngine::run() {
- if (_isDemo && !loadResources())
+ if (_isDemo && !(_resourcesLoaded = loadResources()))
return Common::Error(Common::kReadingFailed, _loadError);
if (!_isDemo && !_subtitles.load("RESOURCE/DATA/BEGIN.TRS", "RESOURCE/GENERAL/ESTRO20.NUT", "RESOURCE/GENERAL/MORTFNTH.NUT"))
return Common::Error(Common::kReadingFailed, "Missing or invalid Mortimer subtitle resources");
@@ -576,7 +769,11 @@ Common::Error MortimerEngine::run() {
// Interview entry points supply rescued animals and glowstones.
const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : -1;
if (!_isDemo) {
- exit = !(loadIntro(boot >= 24 && boot <= 30 ? boot - 24 : 0) && startMovie());
+ _subtitleName = "RESOURCE/DATA/BEGIN.TRS";
+ if (!_campaign.start(boot < 0 ? 24 : boot))
+ _campaign.start();
+ _interview.setDifficulty(CLIP(ConfMan.getInt("mortimer_riddle_difficulty"), 0, 2));
+ exit = !enterCampaignScene();
} else if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
stopSounds();
_introIndex = -1;
@@ -704,7 +901,8 @@ void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y, bool
void MortimerEngine::drawTargets() {
for (uint i = 0; i < _flight.targets().size(); ++i) {
const MortimerFlight::Target &target = _flight.targets()[i];
- if (!target.active || !target.id || target.id >= 800)
+ const bool hostile = (target.id >= 700 && target.id < 800) || (target.id >= 9000 && target.id < 9400);
+ if (!target.active || !target.id || (target.id >= 800 && !hostile))
continue;
if (target.hit) {
const Common::Array<MortimerSprite> &effect = target.hitWeapon == MortimerFlight::kSlimeGun ? _slimeHit : _scopeHit;
@@ -715,7 +913,7 @@ void MortimerEngine::drawTargets() {
drawSprite(effect[frame], center.x, center.y, true, &_viewport);
}
} else if ((target.id < 700 && _flight.weapon() == MortimerFlight::kRevitascope) ||
- (target.id >= 700 && _flight.weapon() == MortimerFlight::kSlimeGun)) {
+ (hostile && _flight.weapon() == MortimerFlight::kSlimeGun)) {
const Common::Point top = screenPoint(Common::Point(target.rect.left, target.rect.top));
const Common::Point bottom = screenPoint(Common::Point(target.rect.right, target.rect.bottom));
drawSprite(_corners[0], top.x, top.y, true, &_viewport);
@@ -809,9 +1007,10 @@ void MortimerEngine::draw() {
drawProjectile();
} else {
_screen.copyRectToSurface(currentMovie().pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
- if (!_isDemo)
- _subtitles.draw(_screen, currentMovie().captions(), ConfMan.getBool("subtitles"));
}
+ if (!_isDemo)
+ _subtitles.draw(_screen, currentMovie().captions(), ConfMan.getBool("subtitles") ||
+ (_interviewActive && _interview.forceSubtitles()), &_intro.context());
if (isPaused()) {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 3fbaa77f00c..7ef7240a716 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -27,6 +27,7 @@
#include "graphics/surface.h"
#include "scumm/insane/mortimer/flight.h"
#include "scumm/insane/mortimer/interview.h"
+#include "scumm/insane/mortimer/campaign.h"
namespace Scumm {
@@ -48,12 +49,18 @@ private:
uint introCount() const;
bool loadIntro(uint index);
bool loadFlight(uint segment);
+ bool enterCampaignScene();
+ bool loadRegion(uint region);
+ bool loadRetailFlight(const Common::String &name, bool cached = false);
+ bool followFlightBranch();
+ bool loadSubtitles(const char *name);
+ void updateMovieContext();
bool startMovie(bool preserveScreen = false);
bool startFlight();
bool processInterview();
bool updateScene();
bool nextScene(bool skip = false);
- bool restart(bool all = false);
+ bool restart(bool all = false, bool crashed = false);
MortimerMovie ¤tMovie();
const MortimerMovie ¤tMovie() const;
bool interactive() const { return !_interviewActive && _introIndex < 0 && _flight.interactive(); }
@@ -78,6 +85,7 @@ private:
MortimerFlight _flight;
MortimerInterview _interview;
MortimerSubtitles _subtitles;
+ MortimerCampaign _campaign;
Graphics::Surface _screen;
Common::Array<MortimerSprite> _cockpit, _dashboard, _reticles, _icons, _head;
Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
@@ -86,6 +94,7 @@ private:
Common::Array<Audio::SoundHandle> _movieHandles;
Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle, _glowHandle;
Common::String _loadError;
+ Common::String _pendingFlight, _subtitleName;
Common::Rect _viewport;
Common::Point _mouse, _view, _aim, _shot;
Common::Point _steeringHistory[10];
@@ -93,10 +102,13 @@ private:
MortimerFlight::Weapon _shotWeapon;
uint32 _nextFrameTime, _pauseTime;
int _introIndex;
+ int _loadedRegion;
+ uint _creditedRescues;
uint _segment, _carriedStones, _completedSpecies, _shellShocks;
uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
uint _iconFlash[7];
bool _finished, _controllerMode, _interviewActive;
+ bool _resourcesLoaded, _playingCached;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 850686f1bbd..12147ddeafc 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -94,9 +94,9 @@ bool validRows(const byte *src, uint32 size, int height, bool remap) {
}
// Codec37's existing decoder has no input-length argument. Validate the
-// encodings present in the demo movies before passing any data to it.
+// encodings present in the movies before passing any data to it.
bool validBackground(const byte *src, uint32 size, int width, int height) {
- if (size < 16 || src[1] != 1 || (src[12] & ~6))
+ if (size < 16 || src[1] != 1 || (src[12] & ~7))
return false;
uint32 pos = 16;
if (src[0] == 2) {
@@ -152,7 +152,7 @@ bool validObject(const byte *data, uint32 size, bool sprite = false) {
if (sprite)
return codec == 44 || (codec == 1 && validRows(data + 14, size - 14, height, false));
if (codec == 37)
- return !(width & 3) && !(height & 3) && validBackground(data + 14, size - 14, width, height);
+ return validBackground(data + 14, size - 14, (width + 3) & ~3, (height + 3) & ~3);
if (codec != 1 && codec != 3 && codec != 23)
return false;
uint32 offset = 14;
@@ -172,12 +172,12 @@ bool validPalette(const byte *data, uint32 size) {
}
bool validTransition(const byte *data, uint32 size) {
- // IACT 8/46 contains a length-prefixed filename followed by CR/LF/EOF.
+ // Text packets contain either a filename or a list of target IDs.
if (size < 22 || READ_LE_UINT16(data + 14) != size - 18 ||
data[size - 3] != '\r' || data[size - 2] != '\n' || data[size - 1] != 0x1a)
return false;
for (uint i = 18; i < size - 3; ++i)
- if (data[i] < 32 || data[i] > 126)
+ if ((data[i] < 32 && data[i] != '\r' && data[i] != '\n' && data[i] != '\t') || data[i] > 126)
return false;
return true;
}
@@ -317,13 +317,29 @@ bool MortimerMovie::fail(const char *message) {
return false;
}
-bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
+bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled, bool continuation) {
const uint32 started = g_system->getMillis();
+ // A LOAD bridge can begin with delta blocks referring to the flight.
+ SmushDeltaBlocksDecoder *previousDecoder = continuation ? _decoder : nullptr;
+ const int previousWidth = _decoderWidth, previousHeight = _decoderHeight;
+ Common::Array<byte> previousPixels;
+ if (continuation) {
+ _decoder = nullptr;
+ previousPixels = _pixels;
+ }
_initialDoubled = doubled;
_frames.clear();
_sounds.clear();
+ _soundOffsets.clear();
+ _soundEnabled.clear();
_error.clear();
reset();
+ if (continuation) {
+ _decoder = previousDecoder;
+ _decoderWidth = previousWidth;
+ _decoderHeight = previousHeight;
+ _pixels = Common::move(previousPixels);
+ }
if (!readData(stream, _data) || !isContainer(_data, MKTAG('A', 'N', 'I', 'M')))
return fail("Invalid Mortimer movie ANIM container");
@@ -336,7 +352,7 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
Common::Array<Track> tracks;
uint32 pos = 8;
uint expectedFrames = 0;
- int decoderWidth = 0, decoderHeight = 0;
+ int decoderWidth = _decoderWidth, decoderHeight = _decoderHeight;
while (pos < _data.size()) {
Chunk chunk;
if (!nextChunk(_data.begin(), _data.size(), pos, chunk))
@@ -366,26 +382,29 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
return fail("Unsupported or truncated Mortimer movie image");
if (body[0] == 37) {
++backgrounds;
- const int width = READ_LE_UINT16(body + 6), height = READ_LE_UINT16(body + 8);
+ const int width = (READ_LE_UINT16(body + 6) + 3) & ~3;
+ const int height = (READ_LE_UINT16(body + 8) + 3) & ~3;
if ((width != decoderWidth || height != decoderHeight) && body[14] != 2)
return fail("Mortimer movie changes dimensions without a keyframe");
decoderWidth = width;
decoderHeight = height;
}
} else if (sub.tag == MKTAG('S', 'K', 'I', 'P')) {
- if (sub.size != 4 || READ_LE_UINT16(body) >= 256 || READ_LE_UINT16(body + 2) >= 256)
+ if (sub.size != 4)
return fail("Invalid Mortimer object toggle");
} else if (sub.tag == MKTAG('I', 'A', 'C', 'T')) {
if (sub.size < 8)
return fail("Truncated Mortimer movie action");
const uint16 code = READ_LE_UINT16(body);
- if ((code >= 2 && code <= 4 && (sub.size < (code == 4 ? 18U : 12U) || READ_LE_UINT16(body + 8) >= 256)) ||
+ if ((code >= 2 && code <= 4 && sub.size < (code == 4 ? 18U : 12U)) ||
(code == 5 && sub.size < 28))
return fail("Invalid Mortimer movie action");
- if (code == 2 && READ_LE_UINT16(body + 4) == 3 && READ_LE_UINT16(body + 10) >= 256)
- return fail("Invalid Mortimer conditional object");
- if (code == 8 && READ_LE_UINT16(body + 2) == 46 && !validTransition(body, sub.size))
+ if (code == 8 && READ_LE_UINT16(body + 2) == 46 &&
+ (sub.size < 18 || (READ_LE_UINT16(body + 6) / 1000 != 7 && !validTransition(body, sub.size))))
return fail("Invalid Mortimer movie transition");
+ } else if (sub.tag == MKTAG('L', 'O', 'A', 'D')) {
+ if (sub.size < 10)
+ return fail("Truncated Mortimer cached movie block");
} else if (sub.tag == MKTAG('X', 'P', 'A', 'L')) {
if (!validPalette(body, sub.size))
return fail("Invalid Mortimer palette change");
@@ -455,6 +474,8 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled) {
if (!decodeSound(bytes, track.sound))
return fail("Unsupported or incomplete Mortimer audio");
_sounds.push_back(Common::move(track.sound));
+ _soundOffsets.push_back(track.blocks[0].offset);
+ _soundEnabled.push_back(false);
}
debug(1, "Mortimer: loaded %u movie frames in %u ms", _frames.size(), g_system->getMillis() - started);
return true;
@@ -470,6 +491,10 @@ void MortimerMovie::reset() {
_storedObject = Object();
_objects.clear();
_captions.clear();
+ _cachedMovie.clear();
+ _cacheBlock = 0;
+ for (uint i = 0; i < _soundEnabled.size(); ++i)
+ _soundEnabled[i] = false;
_background.clear();
memcpy(_palette, _initialPalette, sizeof(_palette));
memset(_paletteDelta, 0, sizeof(_paletteDelta));
@@ -535,15 +560,20 @@ bool MortimerMovie::decodeObject(Object &object) {
}
object.pixels.resize(object.width * object.height);
if (object.codec == 37) {
- if (_decoderWidth != object.width || _decoderHeight != object.height) {
+ // Interview clips crop the last 4x4 block to their portrait bounds.
+ const int width = (object.width + 3) & ~3, height = (object.height + 3) & ~3;
+ if (_decoderWidth != width || _decoderHeight != height) {
if (src[14] != 2)
return fail("Mortimer movie changes dimensions without a keyframe");
delete _decoder;
- _decoderWidth = object.width;
- _decoderHeight = object.height;
+ _decoderWidth = width;
+ _decoderHeight = height;
_decoder = new SmushDeltaBlocksDecoder(_decoderWidth, _decoderHeight);
}
- _decoder->decode(object.pixels.begin(), src + 14);
+ _scratch.resize(width * height);
+ _decoder->decode(_scratch.begin(), src + 14);
+ for (int row = 0; row < object.height; ++row)
+ memcpy(object.pixels.begin() + row * object.width, _scratch.begin() + row * width, object.width);
} else {
memset(object.pixels.begin(), 0, object.pixels.size());
smushDecodeRLEOpaque(object.pixels.begin(), src + 14, 0, 0,
@@ -572,8 +602,31 @@ bool MortimerMovie::advance(bool renderFrame) {
skipB = READ_LE_UINT16(src + 2);
continue;
}
+ // Image toggles can change again when the player shoots this frame.
+ // Commands and sound starts must use the toggle at their position in it.
+ if (chunk.tag != MKTAG('F', 'O', 'B', 'J') && chunk.tag != MKTAG('F', 'T', 'C', 'H') && skipObject(skipA, skipB)) {
+ skipA = 0xffff;
+ skipB = 0;
+ continue;
+ }
if (chunk.tag == MKTAG('I', 'A', 'C', 'T')) {
handleAction(src, chunk.size);
+ } else if (chunk.tag == MKTAG('P', 'S', 'A', 'D')) {
+ if (!READ_LE_UINT16(src + 2))
+ for (uint i = 0; i < _soundOffsets.size(); ++i)
+ if (_soundOffsets[i] == chunk.offset)
+ _soundEnabled[i] = true;
+ } else if (chunk.tag == MKTAG('L', 'O', 'A', 'D')) {
+ const uint block = READ_LE_UINT16(src + 2);
+ if (!block) {
+ _cachedMovie.clear();
+ _cacheBlock = 0;
+ }
+ if (block != _cacheBlock++ || chunk.size - 10 > 32 * 1024 * 1024 - _cachedMovie.size())
+ return fail("Invalid Mortimer cached movie sequence");
+ const uint offset = _cachedMovie.size();
+ _cachedMovie.resize(offset + chunk.size - 10);
+ memcpy(_cachedMovie.begin() + offset, src + 10, chunk.size - 10);
} else if (chunk.tag == MKTAG('X', 'P', 'A', 'L')) {
handlePalette(src, chunk.size);
} else if (chunk.tag == MKTAG('N', 'P', 'A', 'L')) {
@@ -659,7 +712,17 @@ void MortimerMovie::render() {
}
}
-MortimerCutscene::MortimerCutscene(bool girl) : _girl(girl) {
+bool MortimerMovieContext::matches(uint code) const {
+ for (uint i = 0; i < kConditionCount; ++i, code /= 3) {
+ const uint digit = code % 3;
+ if (digit && conditions[i] != (digit == 2))
+ return false;
+ }
+ return code == 0;
+}
+
+MortimerCutscene::MortimerCutscene(bool girl) {
+ _context.girl = girl;
reset();
}
@@ -670,16 +733,24 @@ void MortimerCutscene::reset() {
void MortimerCutscene::handleAction(const byte *data, uint32 size) {
MortimerMovie::handleAction(data, size);
- if (READ_LE_UINT16(data) != 2 || READ_LE_UINT16(data + 4) != 1)
+ if (READ_LE_UINT16(data) != 2)
return;
const uint16 id = READ_LE_UINT16(data + 6), toggle = READ_LE_UINT16(data + 8);
- // INTRO4_H contains both player characters under separate object toggles.
- if (id == 3000 || id == 4000)
- _toggles[toggle] = id == 3000 ? _girl : !_girl;
+ if (READ_LE_UINT16(data + 4) == 3) {
+ setToggle(toggle, !this->toggle(READ_LE_UINT16(data + 10)));
+ } else if (READ_LE_UINT16(data + 4) == 1) {
+ const uint family = id / 1000;
+ if (family == 2)
+ setToggle(toggle, false);
+ else if (family == 3 || family == 4 || family == 6)
+ setToggle(toggle, (family == 3 && _context.girl) || (family == 4 && !_context.girl) || !_context.matches(id % 1000));
+ else if (family == 5)
+ setToggle(toggle, _context.riddle != id - 5000);
+ }
}
bool MortimerCutscene::skipObject(uint16 a, uint16 b) const {
- return a != 0xffff && (b ? _toggles[a] != _toggles[b] : _toggles[a]);
+ return a != 0xffff && (b ? toggle(a) != toggle(b) : toggle(a));
}
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index cb75407320b..ec54f7b0aa5 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -50,6 +50,14 @@ struct MortimerSound {
Common::Array<byte> samples;
};
+struct MortimerMovieContext {
+ enum Condition { kAtGate, kHasStones, kHostilesRemain, kAnimalsRemain, kDamaged, kOutOfGuesses, kConditionCount };
+ bool girl = false;
+ int riddle = -1;
+ bool conditions[kConditionCount] = {};
+ bool matches(uint code) const;
+};
+
bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<MortimerSprite> &sprites, byte *palette = nullptr);
bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound);
@@ -64,7 +72,7 @@ public:
MortimerMovie();
virtual ~MortimerMovie();
- bool load(Common::SeekableReadStream &stream, bool doubled = true);
+ bool load(Common::SeekableReadStream &stream, bool doubled = true, bool continuation = false);
virtual void reset();
// Suppress compositing while seeking to a frame that replaces the image.
virtual bool advance(bool renderFrame = true);
@@ -74,9 +82,12 @@ public:
const byte *palette() const { return _palette; }
uint frame() const { return _nextFrame ? _nextFrame - 1 : 0; }
uint frameCount() const { return _frames.size(); }
+ bool started() const { return _nextFrame != 0; }
uint frameRate() const { return _frameRate; }
const Common::Array<MortimerSound> &sounds() const { return _sounds; }
const Common::Array<Caption> &captions() const { return _captions; }
+ bool soundEnabled(uint index) const { return _soundEnabled[index]; }
+ const Common::Array<byte> &cachedMovie() const { return _cachedMovie; }
const Common::String &errorMessage() const { return _error; }
protected:
@@ -107,6 +118,10 @@ private:
Common::Array<Frame> _frames;
Common::Array<Object> _objects;
Common::Array<MortimerSound> _sounds;
+ Common::Array<uint32> _soundOffsets;
+ Common::Array<bool> _soundEnabled;
+ Common::Array<byte> _cachedMovie;
+ uint _cacheBlock;
Common::Array<Caption> _captions;
Object _storedObject;
SmushDeltaBlocksDecoder *_decoder;
@@ -123,14 +138,18 @@ class MortimerCutscene : public MortimerMovie {
public:
explicit MortimerCutscene(bool girl = false);
void reset() override;
+ MortimerMovieContext &context() { return _context; }
+ const MortimerMovieContext &context() const { return _context; }
protected:
void handleAction(const byte *data, uint32 size) override;
bool skipObject(uint16 a, uint16 b) const override;
+ bool toggle(uint16 id) const { return id > 0 && id <= 1024 && _toggles[id - 1]; }
+ void setToggle(uint16 id, bool value) { if (id > 0 && id <= 1024) _toggles[id - 1] = value; }
private:
- const bool _girl;
- bool _toggles[256];
+ MortimerMovieContext _context;
+ bool _toggles[1024];
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/text.cpp b/engines/scumm/insane/mortimer/text.cpp
index bda3d4fe25a..9b07ee0ae5f 100644
--- a/engines/scumm/insane/mortimer/text.cpp
+++ b/engines/scumm/insane/mortimer/text.cpp
@@ -99,9 +99,14 @@ bool MortimerSubtitles::loadText(Common::SeekableReadStream &stream) {
if (end == token.c_str() || *end || value < 0 || value > 65535)
return false;
id = value;
- } else if (line.hasPrefix("//") && id >= 0) {
- line = line.substr(2);
+ } else if (id >= 0 && (line.hasPrefix("//") || line.hasPrefix("^c"))) {
+ if (line.hasPrefix("//"))
+ line = line.substr(2);
Entry &entry = _entries[id];
+ if (entry.text.empty() && line.hasPrefix("!")) {
+ entry.conditional = true;
+ line = line.substr(1);
+ }
if (entry.text.empty() && line.hasPrefix("$")) {
entry.information = true;
line = line.substr(1);
@@ -119,13 +124,17 @@ bool MortimerSubtitles::loadText(Common::SeekableReadStream &stream) {
return !stream.err() && !_entries.empty();
}
-void MortimerSubtitles::draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled) const {
+void MortimerSubtitles::draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled,
+ const MortimerMovieContext *context) const {
for (uint i = 0; i < captions.size(); ++i) {
const MortimerMovie::Caption &caption = captions[i];
const auto found = _entries.find(caption.id);
if (found == _entries.end())
continue;
const Entry &entry = found->_value;
+ if (entry.conditional && context && ((caption.id < 1000 && context->riddle != caption.id) ||
+ (caption.id / 1000 == 3 && context->girl) || (caption.id / 1000 == 4 && !context->girl)))
+ continue;
if ((!enabled && !entry.information) || entry.text.empty())
continue;
const MortimerFont &font = entry.information ? _infoFont : _font;
diff --git a/engines/scumm/insane/mortimer/text.h b/engines/scumm/insane/mortimer/text.h
index 53cbf1dbe05..e4abc3c8354 100644
--- a/engines/scumm/insane/mortimer/text.h
+++ b/engines/scumm/insane/mortimer/text.h
@@ -45,13 +45,15 @@ class MortimerSubtitles {
public:
bool load(const char *textPath, const char *fontPath, const char *infoFontPath);
bool loadText(Common::SeekableReadStream &stream);
- void draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled) const;
+ void draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled,
+ const MortimerMovieContext *context = nullptr) const;
private:
struct Entry {
Common::String text;
byte color = 1;
bool information = false;
+ bool conditional = false;
};
MortimerFont _font, _infoFont;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 68d96c91920..0d7adbfa221 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -147,6 +147,7 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
+ insane/mortimer/campaign.o \
insane/mortimer/flight.o \
insane/mortimer/interview.o \
insane/mortimer/movie.o \
Commit: 4f4543fa81f399e98fe706b19e8cec281cdce4bf
https://github.com/scummvm/scummvm/commit/4f4543fa81f399e98fe706b19e8cec281cdce4bf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: use copyRectToSurfaceWithKey for sprite drawing
Changed paths:
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 896fab43901..2c9c0d60717 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -64,22 +64,6 @@ const Control kControls[] = {
const Common::Point kQuestionOrigins[] = {{122, 191}, {120, 234}, {124, 278}};
-void blit(Graphics::Surface &screen, const MortimerSprite &sprite, int x, int y) {
- x += sprite.x;
- y += sprite.y;
- Common::Rect clip(x, y, x + sprite.width, y + sprite.height);
- clip.clip(Common::Rect(screen.w, screen.h));
- if (clip.isEmpty())
- return;
- for (int row = clip.top; row < clip.bottom; ++row) {
- const byte *src = sprite.pixels.begin() + (row - y) * sprite.width;
- byte *dst = (byte *)screen.getBasePtr(0, row);
- for (int col = clip.left; col < clip.right; ++col)
- if (src[col - x])
- dst[col] = src[col - x];
- }
-}
-
MortimerInterview::MortimerInterview(bool isDemo) : _random("mortimer"), _isDemo(isDemo),
_region(0), _difficulty(0), _textDifficulty(0), _tried(0), _stage(kIdle), _effect(kNoEffect),
_answer(-1), _previousAnswer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0), _guesses(3),
@@ -505,29 +489,28 @@ void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &mov
return;
}
screen.fillRect(Common::Rect(640, 480), 10);
- blit(screen, _riddleView ? _gate[0] : _portraits[_selected < 0 ? 7 : _selected], 354, 69);
+ const MortimerSprite &portrait = _riddleView ? _gate[0] : _portraits[_selected < 0 ? 7 : _selected];
+ portrait.draw(screen, 354, 69);
if (playingMovie() && _stage != kHelp && _stage != kNoStones) {
- for (int y = 0; y < 320 && y + 69 < screen.h; ++y) {
- const byte *src = movie.pixels() + y * MortimerMovie::kScreenWidth;
- byte *dst = (byte *)screen.getBasePtr(354, y + 69);
- for (int x = 0; x < 640 - 354; ++x)
- if (src[x])
- dst[x] = src[x];
- }
+ Common::Rect clip(354, 69, 640, 389);
+ clip.clip(Common::Rect(screen.w, screen.h));
+ if (!clip.isEmpty())
+ screen.copyRectToSurfaceWithKey(movie.pixels(), MortimerMovie::kScreenWidth,
+ clip.left, clip.top, clip.width(), clip.height(), 0);
}
if (_switchFrame < 6)
- blit(screen, _panel[10 + _switchFrame], 354, 69);
- blit(screen, _panel[0], 0, 0);
+ _panel[10 + _switchFrame].draw(screen, 354, 69);
+ _panel[0].draw(screen, 0, 0);
if (_riddleView || _selected < 0) {
- blit(screen, _panel[7], 123, 175);
- blit(screen, _panel[1], (_frame / 4) % 2 ? 102 : 30, (_frame / 4) % 2 ? 282 : 286);
+ _panel[7].draw(screen, 123, 175);
+ _panel[1].draw(screen, (_frame / 4) % 2 ? 102 : 30, (_frame / 4) % 2 ? 282 : 286);
} else if (_selected >= 0) {
- blit(screen, _panel[4 + _question], kQuestionOrigins[_question].x, kQuestionOrigins[_question].y);
+ _panel[4 + _question].draw(screen, kQuestionOrigins[_question].x, kQuestionOrigins[_question].y);
}
if (_help)
- blit(screen, _panel[9], 400, 6);
+ _panel[9].draw(screen, 400, 6);
if (_solving)
- blit(screen, _panel[2], 11, 174);
+ _panel[2].draw(screen, 11, 174);
if (_selected >= 0 || _riddleView)
drawText(screen, _riddleView ? _riddles[_answer][_textDifficulty] : _dialogue[_selected][_question]);
}
@@ -535,9 +518,9 @@ void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &mov
void MortimerInterview::drawCursor(Graphics::Surface &screen, const Common::Point &point) const {
if (!fullScreen()) {
if (_stage == kPay)
- blit(screen, _panel[19 + _frame % 2], 508, 451);
+ _panel[19 + _frame % 2].draw(screen, 508, 451);
const int cursor = (_help ? 2 : 0) + (hitControl(point) < 0 ? 1 : 0);
- blit(screen, _cursor[cursor], point.x, point.y);
+ _cursor[cursor].draw(screen, point.x, point.y);
}
}
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index fc07f950470..7ebeaa177be 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -875,27 +875,7 @@ Common::Error MortimerEngine::run() {
}
void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y, bool centered, const Common::Rect *bounds) {
- // NUT offsets are relative to the animation's origin, including when its
- // frames have different bounding boxes (SHELL 402c83/4141bc).
- if (centered) {
- x -= (sprite.x + sprite.width) / 2;
- y -= (sprite.y + sprite.height) / 2;
- }
- x += sprite.x;
- y += sprite.y;
- Common::Rect clip(x, y, x + sprite.width, y + sprite.height);
- clip.clip(Common::Rect(_screen.w, _screen.h));
- if (bounds)
- clip.clip(*bounds);
- if (clip.isEmpty())
- return;
- for (int row = clip.top; row < clip.bottom; ++row) {
- const byte *src = sprite.pixels.begin() + (row - y) * sprite.width + clip.left - x;
- byte *dst = (byte *)_screen.getBasePtr(clip.left, row);
- for (int col = clip.left; col < clip.right; ++col, ++src, ++dst)
- if (*src)
- *dst = *src;
- }
+ sprite.draw(_screen, x, y, centered, bounds);
}
void MortimerEngine::drawTargets() {
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 12147ddeafc..03facdbd32f 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -26,6 +26,7 @@
#include "common/stream.h"
#include "common/system.h"
#include "common/util.h"
+#include "graphics/surface.h"
namespace Scumm {
@@ -208,6 +209,24 @@ bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
} // End of anonymous namespace
+void MortimerSprite::draw(Graphics::Surface &screen, int dstX, int dstY, bool centered, const Common::Rect *bounds) const {
+ // Sprite offsets are relative to the animation's origin, even when centered.
+ if (centered) {
+ dstX -= (x + width) / 2;
+ dstY -= (y + height) / 2;
+ }
+ dstX += x;
+ dstY += y;
+ Common::Rect clip(dstX, dstY, dstX + width, dstY + height);
+ clip.clip(Common::Rect(screen.w, screen.h));
+ if (bounds)
+ clip.clip(*bounds);
+ if (clip.isEmpty())
+ return;
+ const byte *src = pixels.begin() + (clip.top - dstY) * width + clip.left - dstX;
+ screen.copyRectToSurfaceWithKey(src, width, clip.left, clip.top, clip.width(), clip.height(), 0);
+}
+
bool decodeMortimerGlyph(const byte *src, uint32 size, MortimerSprite &sprite) {
sprite.mask.resize(sprite.pixels.size());
memset(sprite.mask.begin(), 0, sprite.mask.size());
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index ec54f7b0aa5..605784740f6 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -29,6 +29,10 @@ namespace Common {
class SeekableReadStream;
}
+namespace Graphics {
+struct Surface;
+}
+
namespace Scumm {
class SmushDeltaBlocksDecoder;
@@ -38,6 +42,8 @@ struct MortimerSprite {
Common::Array<byte> pixels;
// Codec 44 uses skipped pixels for transparency and relative palette colors.
Common::Array<byte> mask;
+
+ void draw(Graphics::Surface &screen, int dstX, int dstY, bool centered = false, const Common::Rect *bounds = nullptr) const;
};
struct MortimerSound {
Commit: 5996c0daa7cb1fcb0e220cc7cc969fea79585fa0
https://github.com/scummvm/scummvm/commit/5996c0daa7cb1fcb0e220cc7cc969fea79585fa0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: shared SAUD/SAD reader for RA1 and mortimer
Changed paths:
A engines/scumm/smush/saud.cpp
A engines/scumm/smush/saud.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/rebel1/audio.cpp
engines/scumm/insane/rebel2/audio.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 03facdbd32f..6d68a9f0ba7 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -21,8 +21,10 @@
#include "scumm/insane/mortimer/movie.h"
#include "scumm/smush/codec37.h"
#include "scumm/smush/rebel/codec_ra2.h"
+#include "scumm/smush/saud.h"
#include "common/endian.h"
+#include "common/memstream.h"
#include "common/stream.h"
#include "common/system.h"
#include "common/util.h"
@@ -36,7 +38,7 @@ struct Chunk {
uint32 tag, offset, size;
};
-bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk, bool padded = true) {
+bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk) {
if (pos > end || end - pos < 8)
return false;
chunk.tag = READ_BE_UINT32(data + pos);
@@ -45,7 +47,7 @@ bool nextChunk(const byte *data, uint32 end, uint32 &pos, Chunk &chunk, bool pad
if (chunk.size > end - chunk.offset)
return false;
pos = chunk.offset + chunk.size;
- if (padded && (chunk.size & 1)) {
+ if (chunk.size & 1) {
if (pos == end)
return false;
++pos;
@@ -183,30 +185,6 @@ bool validTransition(const byte *data, uint32 size) {
return true;
}
-bool decodeSound(const Common::Array<byte> &data, MortimerSound &sound) {
- if (!isContainer(data, MKTAG('S', 'A', 'U', 'D')))
- return false;
- sound.samples.clear();
- uint32 pos = 8;
- while (pos < data.size()) {
- Chunk chunk;
- // SAUD chunks are not word-aligned, unlike the surrounding SMUSH file.
- if (!nextChunk(data.begin(), data.size(), pos, chunk, false))
- return false;
- const byte *src = data.begin() + chunk.offset;
- if (chunk.tag == MKTAG('S', 'D', 'A', 'T')) {
- if (!sound.samples.empty())
- return false;
- sound.samples.assign(src, src + chunk.size);
- } else if (chunk.tag == MKTAG('S', 'H', 'D', 'R') && chunk.size == 4) {
- sound.rate = READ_LE_UINT32(src);
- } else if (chunk.tag == MKTAG('S', 'T', 'R', 'K') && chunk.size >= 14 && src[0] == 6 && src[1] == 12) {
- sound.rate = READ_BE_UINT32(src + 10);
- }
- }
- return !sound.samples.empty() && sound.rate >= 4000 && sound.rate <= 48000;
-}
-
} // End of anonymous namespace
void MortimerSprite::draw(Graphics::Surface &screen, int dstX, int dstY, bool centered, const Common::Rect *bounds) const {
@@ -314,8 +292,18 @@ bool loadMortimerSprites(Common::SeekableReadStream &stream, Common::Array<Morti
}
bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound) {
- Common::Array<byte> data;
- return readData(stream, data) && decodeSound(data, sound);
+ if (stream.size() > 64 * 1024 * 1024 || !stream.seek(0))
+ return false;
+ SaudHeader header;
+ if (!readSaudHeader(stream, header) || header.sampleRate < 4000 || header.sampleRate > 48000)
+ return false;
+ sound.samples.resize(header.dataSize);
+ if (stream.read(sound.samples.begin(), header.dataSize) != header.dataSize || stream.err()) {
+ sound.samples.clear();
+ return false;
+ }
+ sound.rate = header.sampleRate;
+ return true;
}
MortimerMovie::MortimerMovie() : _decoder(nullptr), _decoderWidth(0), _decoderHeight(0),
@@ -490,7 +478,8 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled, bool
memcpy(bytes.begin() + offset, _data.begin() + block.offset + 10, block.size - 10);
offset += block.size - 10;
}
- if (!decodeSound(bytes, track.sound))
+ Common::MemoryReadStream soundStream(bytes.begin(), bytes.size());
+ if (!loadMortimerSound(soundStream, track.sound))
return fail("Unsupported or incomplete Mortimer audio");
_sounds.push_back(Common::move(track.sound));
_soundOffsets.push_back(track.blocks[0].offset);
diff --git a/engines/scumm/insane/rebel1/audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
index cc885a00ae4..da61d39708c 100644
--- a/engines/scumm/insane/rebel1/audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -30,6 +30,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/insane/rebel1/rebel.h"
+#include "scumm/smush/saud.h"
namespace Scumm {
@@ -99,45 +100,26 @@ void InsaneRebel1::loadSfx() {
continue;
}
- const uint32 fileSize = file->size();
- if (fileSize < 16) {
- debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: %s too small (%u bytes)", kRA1SfxFiles[i], fileSize);
+ SaudHeader header;
+ if (!readSaudHeader(*file, header, true)) {
+ debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: invalid SAUD file %s", kRA1SfxFiles[i]);
file->close();
delete file;
continue;
}
- const uint32 tag = file->readUint32BE();
- if (tag != MKTAG('S', 'A', 'U', 'D')) {
- debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA1SfxFiles[i], tag);
- file->close();
- delete file;
- continue;
- }
- file->readUint32BE();
-
- bool foundSdat = false;
- while (file->pos() + 8 <= (int64)fileSize) {
- const uint32 chunkTag = file->readUint32BE();
- const uint32 chunkSize = file->readUint32BE();
- if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
- const uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
- byte *pcm = (byte *)malloc(pcmSize);
- if (pcm) {
- file->read(pcm, pcmSize);
- _sfxData[i] = pcm;
- _sfxSize[i] = pcmSize;
- debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: loaded %s (%u bytes PCM)", kRA1SfxFiles[i], pcmSize);
- }
- foundSdat = true;
- break;
+ byte *pcm = (byte *)malloc(header.dataSize);
+ if (pcm) {
+ if (file->read(pcm, header.dataSize) == header.dataSize && !file->err()) {
+ _sfxData[i] = pcm;
+ _sfxSize[i] = header.dataSize;
+ debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: loaded %s (%u bytes PCM)", kRA1SfxFiles[i], header.dataSize);
+ } else {
+ debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: could not read PCM from %s", kRA1SfxFiles[i]);
+ free(pcm);
}
- file->seek(chunkSize, SEEK_CUR);
}
- if (!foundSdat)
- debugC(DEBUG_INSANE, "InsaneRebel1::loadSfx: no SDAT chunk in %s", kRA1SfxFiles[i]);
-
file->close();
delete file;
}
diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index 663413424b0..fe1be7b4895 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -19,12 +19,14 @@
*
*/
+#include "common/memstream.h"
#include "common/system.h"
#include "common/util.h"
#include "scumm/file.h"
#include "scumm/scumm_v7.h"
+#include "scumm/smush/saud.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/rebel2/rebel.h"
@@ -79,47 +81,26 @@ void InsaneRebel2::loadSfx() {
continue;
}
- uint32 fileSize = file->size();
- if (fileSize < 38) { // Minimum: 8 (SAUD) + 22 (STRK) + 8 (SDAT header)
- debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: %s too small (%d bytes)", kRA2SfxFiles[i], fileSize);
+ SaudHeader header;
+ if (!readSaudHeader(*file, header, true)) {
+ debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: Invalid SAUD file %s", kRA2SfxFiles[i]);
file->close();
delete file;
continue;
}
- uint32 tag = file->readUint32BE();
- if (tag != MKTAG('S', 'A', 'U', 'D')) {
- debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA2SfxFiles[i], tag);
- file->close();
- delete file;
- continue;
- }
- file->readUint32BE();
-
- bool foundSdat = false;
- while (file->pos() + 8 <= (int64)fileSize) {
- uint32 chunkTag = file->readUint32BE();
- uint32 chunkSize = file->readUint32BE();
-
- if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
- uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
- _sfxData[i] = (byte *)malloc(pcmSize);
- if (_sfxData[i]) {
- file->read(_sfxData[i], pcmSize);
- _sfxSize[i] = pcmSize;
- debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: Loaded %s (%d bytes PCM)", kRA2SfxFiles[i], pcmSize);
- }
- foundSdat = true;
- break;
+ byte *pcm = (byte *)malloc(header.dataSize);
+ if (pcm) {
+ if (file->read(pcm, header.dataSize) == header.dataSize && !file->err()) {
+ _sfxData[i] = pcm;
+ _sfxSize[i] = header.dataSize;
+ debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: Loaded %s (%u bytes PCM)", kRA2SfxFiles[i], header.dataSize);
} else {
- file->seek(chunkSize, SEEK_CUR);
+ debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: Could not read PCM from %s", kRA2SfxFiles[i]);
+ free(pcm);
}
}
- if (!foundSdat) {
- debugC(DEBUG_INSANE, "InsaneRebel2::loadSfx: No SDAT chunk in %s", kRA2SfxFiles[i]);
- }
-
file->close();
delete file;
}
@@ -200,18 +181,15 @@ void InsaneRebel2::playAuxSfx(int buffer, int volume, int pan) {
const byte *pcmStart = _auxSfxData[buffer];
uint32 pcmSize = _auxSfxSize[buffer];
- if (pcmSize > 8 && READ_BE_UINT32(pcmStart) == MKTAG('S', 'A', 'U', 'D')) {
- uint32 pos = 8;
- while (pos + 8 <= pcmSize) {
- uint32 chunkTag = READ_BE_UINT32(pcmStart + pos);
- uint32 chunkSize = READ_BE_UINT32(pcmStart + pos + 4);
- if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
- pcmStart = pcmStart + pos + 8;
- pcmSize = MIN(chunkSize, pcmSize - pos - 8);
- break;
- }
- pos += 8 + chunkSize;
+ if (pcmSize >= 4 && READ_BE_UINT32(pcmStart) == MKTAG('S', 'A', 'U', 'D')) {
+ Common::MemoryReadStream soundStream(pcmStart, pcmSize);
+ SaudHeader header;
+ if (!readSaudHeader(soundStream, header, true)) {
+ debugC(DEBUG_INSANE, "InsaneRebel2::playAuxSfx: Invalid SAUD in buffer %d", buffer);
+ return;
}
+ pcmStart += soundStream.pos();
+ pcmSize = header.dataSize;
}
byte *pcmCopy = (byte *)malloc(pcmSize);
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 0d7adbfa221..b340a3c4a71 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -178,6 +178,7 @@ MODULE_OBJS += \
smush/codec20.o \
smush/codec37.o \
smush/codec47.o \
+ smush/saud.o \
smush/smush_player.o \
smush/rebel/codec_ra1.o \
smush/rebel/codec_ra2.o \
diff --git a/engines/scumm/smush/saud.cpp b/engines/scumm/smush/saud.cpp
new file mode 100644
index 00000000000..28ec9195a50
--- /dev/null
+++ b/engines/scumm/smush/saud.cpp
@@ -0,0 +1,78 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "scumm/smush/saud.h"
+
+#include "common/endian.h"
+#include "common/stream.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+bool readSaudHeader(Common::SeekableReadStream &stream, SaudHeader &header, bool allowTruncated) {
+ int64 end = stream.size();
+ if (stream.pos() < 0 || end - stream.pos() < 8 || stream.readUint32BE() != MKTAG('S', 'A', 'U', 'D'))
+ return false;
+ const uint32 size = stream.readUint32BE();
+ if (!allowTruncated && size != end - stream.pos())
+ return false;
+ end = MIN(end, stream.pos() + size);
+
+ SaudHeader parsed;
+ int64 dataOffset = -1;
+ while (stream.pos() < end) {
+ if (end - stream.pos() < 8)
+ return false;
+ const uint32 tag = stream.readUint32BE();
+ uint32 chunkSize = stream.readUint32BE();
+ const int64 offset = stream.pos();
+ if (chunkSize > end - offset) {
+ if (!allowTruncated || tag != MKTAG('S', 'D', 'A', 'T'))
+ return false;
+ chunkSize = end - offset;
+ }
+ if (tag == MKTAG('S', 'D', 'A', 'T')) {
+ if (dataOffset >= 0 || !chunkSize)
+ return false;
+ dataOffset = offset;
+ parsed.dataSize = chunkSize;
+ if (allowTruncated)
+ break;
+ } else if (tag == MKTAG('S', 'H', 'D', 'R') && chunkSize == 4) {
+ parsed.sampleRate = stream.readUint32LE();
+ } else if (tag == MKTAG('S', 'T', 'R', 'K') && chunkSize >= 14) {
+ const byte code = stream.readByte(), length = stream.readByte();
+ if (code == 6 && length == 12) {
+ if (!stream.skip(8))
+ return false;
+ parsed.sampleRate = stream.readUint32BE();
+ }
+ }
+ // SAUD chunks have no word-alignment padding.
+ if (stream.err() || stream.eos() || !stream.seek(offset + chunkSize))
+ return false;
+ }
+ if (dataOffset < 0 || stream.err() || stream.eos() || !stream.seek(dataOffset))
+ return false;
+ header = parsed;
+ return true;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/saud.h b/engines/scumm/smush/saud.h
new file mode 100644
index 00000000000..65e9f55563b
--- /dev/null
+++ b/engines/scumm/smush/saud.h
@@ -0,0 +1,46 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_SMUSH_SAUD_H
+#define SCUMM_SMUSH_SAUD_H
+
+#include "common/scummsys.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Scumm {
+
+struct SaudHeader {
+ uint32 dataSize = 0;
+ uint32 sampleRate = 11025;
+};
+
+// Read a SAUD container spanning the rest of the stream, leaving it at its PCM.
+// The sample rate defaults to 11025 Hz when no rate metadata is present.
+// allowTruncated stops at the first SDAT, accepting the available PCM prefix
+// without reading trailing metadata. This supports Rebel SFX and auxiliary buffers.
+// The header is only updated on success.
+bool readSaudHeader(Common::SeekableReadStream &stream, SaudHeader &header, bool allowTruncated = false);
+
+} // End of namespace Scumm
+
+#endif
Commit: 6a4ad1273ec6d4643567b1c8040fba8765418f2b
https://github.com/scummvm/scummvm/commit/6a4ad1273ec6d4643567b1c8040fba8765418f2b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: implement shellshock-related code and indicators
Changed paths:
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index c05f5e83d8d..e4141345ef5 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -44,6 +44,12 @@ void MortimerFlight::clearProgress() {
context() = MortimerMovieContext();
}
+void MortimerFlight::clearHostiles() {
+ // Retail hornet hits also suppress their salt projectiles.
+ memset(_hitTargets + 800, 0, 100 * sizeof(_hitTargets[0]));
+ memset(_hitTargets + 1000, 0, 400 * sizeof(_hitTargets[0]));
+}
+
void MortimerFlight::reset() {
MortimerCutscene::reset();
if (_isDemo)
@@ -171,7 +177,10 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
_weapon = context().conditions[MortimerMovieContext::kHostilesRemain] ? kSlimeGun : kRevitascope;
} else if (id / 1000 == 12) {
context().conditions[MortimerMovieContext::kAnimalsRemain] = targetsRemain(text);
- } else if (_inBranch && !text.hasPrefix("@riddles")) {
+ } else if ((_isDemo || _inBranch) && !text.hasPrefix("@riddles")) {
+ // The demo has a fixed route. TITLE.SAN gives its destination
+ // before the full-frame branch rectangle, so it cannot use the
+ // retail steering predicate at this point in the frame.
_nextFlight = text;
for (uint i = 0; i < _nextFlight.size(); ++i)
if (_nextFlight[i] == '\\')
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 901255dc63f..47e072750db 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -41,6 +41,7 @@ public:
explicit MortimerFlight(bool isDemo = true);
bool load(Common::SeekableReadStream &stream, bool interactive = true, bool continuation = false);
void clearProgress();
+ void clearHostiles();
void reset() override;
bool advance(bool renderFrame = true) override;
ShotResult shoot(const Common::Point &point);
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 7ebeaa177be..27fc51be18c 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -111,11 +111,12 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
return keymaps;
}
-MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo), _flight(isDemo), _interview(isDemo),
+MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo), _random("mortimer-flight"), _flight(isDemo), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
- _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0), _segment(0), _carriedStones(0), _completedSpecies(0),
- _shellShocks(0), _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0),
- _pendingGlowStones(0), _finished(false), _controllerMode(false), _interviewActive(false), _resourcesLoaded(false), _playingCached(false) {
+ _crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
+ _segment(0), _carriedStones(0), _completedSpecies(0), _shellShocks(0), _shockFrame(0), _crashCount(0),
+ _uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0), _pendingGlowStones(0), _finished(false),
+ _controllerMode(false), _interviewActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
ConfMan.registerDefault("mortimer_flight_difficulty", 0);
@@ -181,6 +182,8 @@ bool MortimerEngine::loadResources() {
}
file.close();
}
+ // The hit sound animation uses volume 50 on the original 0..127 scale.
+ _collisionSound.volume = 100;
if (_isDemo && !_interview.loadResources()) {
_loadError = _interview.errorMessage();
return false;
@@ -189,13 +192,13 @@ bool MortimerEngine::loadResources() {
}
MortimerMovie &MortimerEngine::currentMovie() {
- if (_interviewActive || _introIndex >= 0)
+ if (_crashStage != kNoCrash || _interviewActive || _introIndex >= 0)
return _intro;
return _flight;
}
const MortimerMovie &MortimerEngine::currentMovie() const {
- if (_interviewActive || _introIndex >= 0)
+ if (_crashStage != kNoCrash || _interviewActive || _introIndex >= 0)
return _intro;
return _flight;
}
@@ -279,6 +282,7 @@ bool MortimerEngine::loadRegion(uint region) {
_flight.setCollisionRadius(20 * (CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2) + 1));
_interview.resetRiddle();
_carriedStones = _completedSpecies = _creditedRescues = _shellShocks = 0;
+ _shockFrame = _crashCount = 0;
return true;
}
@@ -308,6 +312,7 @@ bool MortimerEngine::loadRetailFlight(const Common::String &name, bool cached) {
return false;
}
_playingCached = cached;
+ _flightName = name;
_introIndex = -1;
_movieHandles.resize(_flight.sounds().size());
return true;
@@ -397,12 +402,23 @@ void MortimerEngine::playMovieSounds(bool seek) {
}
}
+void MortimerEngine::playCollisionSound() {
+ // Each hit queues a new sample; earlier impacts can still be playing.
+ uint channel = 0;
+ while (channel < _collisionHandles.size() && _mixer->isSoundHandleActive(_collisionHandles[channel]))
+ ++channel;
+ if (channel == _collisionHandles.size())
+ _collisionHandles.push_back(Audio::SoundHandle());
+ playSound(_collisionSound, _collisionHandles[channel], Audio::Mixer::kSFXSoundType);
+}
+
void MortimerEngine::stopSounds() {
for (uint i = 0; i < _movieHandles.size(); ++i)
_mixer->stopHandle(_movieHandles[i]);
+ for (uint i = 0; i < _collisionHandles.size(); ++i)
+ _mixer->stopHandle(_collisionHandles[i]);
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
- _mixer->stopHandle(_collisionHandle);
_mixer->stopHandle(_glowHandle);
}
@@ -427,6 +443,7 @@ bool MortimerEngine::startMovie(bool preserveScreen) {
bool MortimerEngine::startFlight() {
_flight.resetWeapon();
+ _hitJolt = false;
_uiFrame = 0;
_shotFrame = 3;
_glowFrame = 10;
@@ -441,7 +458,64 @@ bool MortimerEngine::startFlight() {
return startMovie();
}
-bool MortimerEngine::restart(bool all, bool crashed) {
+bool MortimerEngine::startCrash() {
+ // SHELL's difficulty table permits these crashes before a full repair.
+ const uint allowances[] = {4, 3, 1};
+ const uint difficulty = CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2);
+ _needsRepair = ++_crashCount > allowances[difficulty];
+ if (_needsRepair)
+ _crashCount = 0;
+ _hitJolt = false;
+ _flight.resetWeapon();
+ return loadCrashMovie(false);
+}
+
+bool MortimerEngine::loadCrashMovie(bool repair) {
+ stopSounds();
+ _movieHandles.clear();
+ const char *name = repair ? "RESOURCE/GENERAL/REPAIR.SAN" :
+ (_campaign.scene().region == 3 ? "RESOURCE/GENERAL/D_CRASH.SAN" : "RESOURCE/GENERAL/CRASH.SAN");
+ Common::File file;
+ if (!file.open(Common::Path(name))) {
+ _loadError = Common::String::format("Missing %s", name);
+ return false;
+ }
+ if (!_intro.load(file)) {
+ _loadError = Common::String::format("%s: %s", name, _intro.errorMessage().c_str());
+ return false;
+ }
+ _crashStage = repair ? kRepairMovie : kCrashMovie;
+ _movieHandles.resize(_intro.sounds().size());
+ return startMovie();
+}
+
+bool MortimerEngine::finishCrash() {
+ const uint difficulty = CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2);
+ _crashStage = kNoCrash;
+ _shellShocks = _shockFrame = 0;
+ _pendingFlight.clear();
+ if (_needsRepair) {
+ _needsRepair = false;
+ // Easy flight keeps rescues and stones even after a full repair.
+ if (difficulty != 0) {
+ const bool girl = _flight.context().girl;
+ _flight.clearProgress();
+ _flight.context().girl = girl;
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
+ _interview.resetAttempts();
+ }
+ return _campaign.returnToFlight() && enterCampaignScene();
+ }
+ if (difficulty == 2)
+ _flight.clearHostiles();
+ // Ordinary crashes replay the current branch, including a cached branch.
+ return loadRetailFlight(_flightName) && startFlight();
+}
+
+bool MortimerEngine::restart(bool all) {
+ _crashStage = kNoCrash;
+ _needsRepair = _hitJolt = false;
+ _shockFrame = _crashCount = 0;
if (!_isDemo) {
_finished = false;
if (all) {
@@ -452,16 +526,9 @@ bool MortimerEngine::restart(bool all, bool crashed) {
if (!_interviewActive && _introIndex >= 0)
return startMovie();
const uint region = _campaign.scene().region;
- // Easy flight keeps rescues after a crash. Other flight difficulties
- // clear the region's rescues and payments, but keep its current riddle.
- if (!crashed || ConfMan.getInt("mortimer_flight_difficulty") > 0) {
- _flight.clearProgress();
- _carriedStones = _completedSpecies = _creditedRescues = 0;
- if (crashed)
- _interview.resetAttempts();
- else
- _interview.resetRiddle();
- }
+ _flight.clearProgress();
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
+ _interview.resetRiddle();
_shellShocks = 0;
return _campaign.start(region) && enterCampaignScene();
}
@@ -482,6 +549,11 @@ bool MortimerEngine::restart(bool all, bool crashed) {
bool MortimerEngine::nextScene(bool skip) {
stopSounds();
+ if (_crashStage != kNoCrash) {
+ if (_crashStage == kCrashMovie && _needsRepair)
+ return loadCrashMovie(true);
+ return finishCrash();
+ }
if (_interviewActive) {
_interview.skip();
return processInterview();
@@ -543,9 +615,13 @@ bool MortimerEngine::nextScene(bool skip) {
}
// The demo's flight movies must follow their expected SAN branches.
if (!skip && !_flight.nextFlight().equalsIgnoreCase(kDemoFlights[_segment + 1])) {
- _loadError = Common::String::format("Unexpected flight transition in %s", kDemoFlights[_segment]);
+ _loadError = Common::String::format("Unexpected flight transition in %s at frame %u/%u: expected '%s', got '%s'",
+ kDemoFlights[_segment], _flight.frame() + 1, _flight.frameCount(), kDemoFlights[_segment + 1],
+ _flight.nextFlight().empty() ? "<none>" : _flight.nextFlight().c_str());
return false;
}
+ debug(1, "Mortimer: demo flight transition %s -> %s at frame %u/%u%s",
+ kDemoFlights[_segment], kDemoFlights[_segment + 1], _flight.frame() + 1, _flight.frameCount(), skip ? " (skipped)" : "");
_carriedStones += _flight.rescuedCount();
_completedSpecies |= _flight.rescuedSpecies();
return loadFlight(_segment + 1) && startFlight();
@@ -557,10 +633,12 @@ bool MortimerEngine::processInterview() {
_completedSpecies = _interview.species();
_interviewActive = false;
_shellShocks = 0;
+ _shockFrame = 0;
if (!_isDemo) {
if (_interview.restartsRegion()) {
_flight.clearProgress();
_carriedStones = _completedSpecies = _creditedRescues = 0;
+ _crashCount = 0;
}
return _campaign.returnToFlight() && enterCampaignScene();
}
@@ -625,6 +703,13 @@ bool MortimerEngine::updateScene() {
if (changed || !_interview.playingMovie())
return true;
} else if (interactive()) {
+ if (_hitJolt) {
+ const int amount = 64 * (CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2) + 1);
+ const int y = _random.getRandomBit() ? amount : -amount;
+ const int x = _random.getRandomBit() ? amount : -amount;
+ updateView(x, y);
+ _hitJolt = false;
+ }
updateSteering();
updateInterface();
}
@@ -642,16 +727,22 @@ bool MortimerEngine::updateScene() {
}
return nextScene();
}
- if (!_isDemo && !_interviewActive && _introIndex < 0 && !_flight.nextFlight().empty())
+ if (!_isDemo && _crashStage == kNoCrash && !_interviewActive && _introIndex < 0 && !_flight.nextFlight().empty())
return followFlightBranch();
playMovieSounds();
_system->getPaletteManager()->setPalette(_interviewActive ? _interview.palette(movie) : movie.palette(), 0, 256);
if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
- ++_shellShocks;
- playSound(_collisionSound, _collisionHandle, Audio::Mixer::kSFXSoundType);
// Retail crashes on the fifteenth hit; the demo allows eighteen.
- if (_shellShocks >= (_isDemo ? 18U : 15U))
- return restart(false, true);
+ if (_shellShocks + 1 >= (_isDemo ? 18U : 15U)) {
+ if (!(_isDemo ? restart() : startCrash()))
+ return false;
+ } else {
+ ++_shellShocks;
+ _shockFrame = 0;
+ _hitJolt = !_isDemo;
+ }
+ // Movie changes stop audio, so enqueue the fatal impact afterwards.
+ playCollisionSound();
}
return true;
}
@@ -667,6 +758,16 @@ void MortimerEngine::updateSteering() {
x = (_steering[1] - _steering[0]) * 128 / Common::JOYAXIS_MAX;
y = (_steering[2] - _steering[3]) * 128 / Common::JOYAXIS_MAX;
}
+ updateView(x, y);
+ if (ABS(x) < 3)
+ x = 0;
+ if (ABS(y) < 3)
+ y = 0;
+ _aim.x = 320 + CLIP(x * 296 / 109, -296, 295);
+ _aim.y = 190 - CLIP(y * 170 / 128, -169, 170);
+}
+
+void MortimerEngine::updateView(int x, int y) {
// SHELL 40db90 averages ten input samples before moving the viewport.
const int weights[] = {1, 1, 3, 3, 8, 8, 14, 16, 16, 14};
int averageX = 0, averageY = 0;
@@ -680,12 +781,6 @@ void MortimerEngine::updateSteering() {
// The movie is doubled, and its translation is aligned to four pixels.
_view.x = ((88 + CLIP(averageX * 88 / 128, -88, 88) + 3) & ~3) / 2;
_view.y = ((118 - CLIP(averageY * 118 / 128, -118, 118) + 3) & ~3) / 2;
- if (ABS(x) < 3)
- x = 0;
- if (ABS(y) < 3)
- y = 0;
- _aim.x = 320 + CLIP(x * 296 / 109, -296, 295);
- _aim.y = 190 - CLIP(y * 170 / 128, -169, 170);
}
Common::Point MortimerEngine::moviePoint(const Common::Point &point) const {
@@ -720,13 +815,14 @@ void MortimerEngine::fire() {
} else if (result == MortimerFlight::kHornetHit) {
playSound(_slimeHitSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
} else if (result == MortimerFlight::kShellRestored) {
- _shellShocks = 0;
+ _shellShocks = _shockFrame = 0;
playSound(_balloonSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
}
}
void MortimerEngine::updateInterface() {
++_uiFrame;
+ _shockFrame ^= 1;
if (_shotFrame < 3)
++_shotFrame;
for (uint i = 0; i < ARRAYSIZE(_iconFlash); ++i)
@@ -871,6 +967,8 @@ Common::Error MortimerEngine::run() {
}
stopSounds();
CursorMan.showMouse(oldCursor);
+ if (!_loadError.empty())
+ warning("Mortimer: %s", _loadError.c_str());
return _loadError.empty() ? Common::kNoError : Common::Error(Common::kReadingFailed, _loadError);
}
@@ -936,7 +1034,9 @@ void MortimerEngine::drawDashboard() {
drawSprite(_dashboard[35], 0, 0);
drawSprite(_dashboard[29 + _uiFrame % 6], 0, 439);
drawSprite(_dashboard[speechPlaying() ? 16 + _uiFrame % 3 : 16], 21, 312);
- const uint gauge = _shellShocks < 16 ? 19 + _shellShocks / 2 : 27 + _uiFrame % 2;
+ // Retail advances every hit after ten shocks and flashes from thirteen.
+ const uint level = _isDemo || _shellShocks <= 10 ? _shellShocks / 2 : _shellShocks - 5;
+ const uint gauge = level < 8 ? 19 + level : 27 + _shockFrame;
drawSprite(_dashboard[gauge], 180, 442);
const Common::Point iconOrigins[] = {{158, 364}, {203, 358}, {248, 358}, {293, 354}, {339, 355}, {383, 360}, {428, 361}};
const Common::Point flashOrigins[] = {{154, 345}, {198, 338}, {243, 334}, {288, 332}, {333, 334}, {378, 338}, {422, 345}};
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 7ef7240a716..040143dc985 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -24,6 +24,7 @@
#include "engines/engine.h"
#include "backends/keymapper/keymap.h"
#include "audio/mixer.h"
+#include "common/random.h"
#include "graphics/surface.h"
#include "scumm/insane/mortimer/flight.h"
#include "scumm/insane/mortimer/interview.h"
@@ -45,6 +46,8 @@ protected:
void pauseEngineIntern(bool pause) override;
private:
+ enum CrashStage { kNoCrash, kCrashMovie, kRepairMovie };
+
bool loadResources();
uint introCount() const;
bool loadIntro(uint index);
@@ -57,15 +60,19 @@ private:
void updateMovieContext();
bool startMovie(bool preserveScreen = false);
bool startFlight();
+ bool startCrash();
+ bool loadCrashMovie(bool repair);
+ bool finishCrash();
bool processInterview();
bool updateScene();
bool nextScene(bool skip = false);
- bool restart(bool all = false, bool crashed = false);
+ bool restart(bool all = false);
MortimerMovie ¤tMovie();
const MortimerMovie ¤tMovie() const;
- bool interactive() const { return !_interviewActive && _introIndex < 0 && _flight.interactive(); }
+ bool interactive() const { return _crashStage == kNoCrash && !_interviewActive && _introIndex < 0 && _flight.interactive(); }
void setMouse(const Common::Point &position);
void updateSteering();
+ void updateView(int x, int y);
void updateInterface();
void fire();
Common::Point moviePoint(const Common::Point &point) const;
@@ -78,9 +85,11 @@ private:
bool speechPlaying() const;
void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset = 0);
void playMovieSounds(bool seek = false);
+ void playCollisionSound();
void stopSounds();
const bool _isDemo;
+ Common::RandomSource _random;
MortimerCutscene _intro;
MortimerFlight _flight;
MortimerInterview _interview;
@@ -92,23 +101,26 @@ private:
MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound, _glowSound, _balloonSound;
MortimerSound _buttonSound, _payoutSound;
Common::Array<Audio::SoundHandle> _movieHandles;
- Audio::SoundHandle _scopeHandle, _rescueHandle, _collisionHandle, _glowHandle;
+ Common::Array<Audio::SoundHandle> _collisionHandles;
+ Audio::SoundHandle _scopeHandle, _rescueHandle, _glowHandle;
Common::String _loadError;
- Common::String _pendingFlight, _subtitleName;
+ Common::String _flightName, _pendingFlight, _subtitleName;
Common::Rect _viewport;
Common::Point _mouse, _view, _aim, _shot;
Common::Point _steeringHistory[10];
int _steering[4];
MortimerFlight::Weapon _shotWeapon;
+ CrashStage _crashStage;
uint32 _nextFrameTime, _pauseTime;
int _introIndex;
int _loadedRegion;
uint _creditedRescues;
- uint _segment, _carriedStones, _completedSpecies, _shellShocks;
+ uint _segment, _carriedStones, _completedSpecies, _shellShocks, _shockFrame, _crashCount;
uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
uint _iconFlash[7];
bool _finished, _controllerMode, _interviewActive;
bool _resourcesLoaded, _playingCached;
+ bool _needsRepair, _hitJolt;
};
} // End of namespace Scumm
Commit: 625e660068e930fac480f47fef51fd7658993e00
https://github.com/scummvm/scummvm/commit/625e660068e930fac480f47fef51fd7658993e00
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: added restore content option for the demo with some extra unused videos
Changed paths:
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 4cb7b85d199..c0df2e1ab35 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -46,6 +46,7 @@ namespace Scumm {
#define GAMEOPTION_REBEL1_NO_DAMAGE GUIO_GAMEOPTIONS15
#define GAMEOPTION_REBEL1_RESTORED_CONTENT GUIO_GAMEOPTIONS16
#define GAMEOPTION_REBEL2_RESTORED_CONTENT GUIO_GAMEOPTIONS17
+#define GAMEOPTION_MORTIMER_RESTORED_CONTENT GUIO_GAMEOPTIONS18
/**
* Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index d3ebd48c32b..6ba62a46092 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -252,7 +252,7 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
{"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO3(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT)},
- {"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT)},
+ {"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO5(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT, GAMEOPTION_MORTIMER_RESTORED_CONTENT)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index e4141345ef5..57b3728fa55 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -26,7 +26,7 @@
namespace Scumm {
-MortimerFlight::MortimerFlight(bool isDemo) : _isDemo(isDemo), _initialInteractive(true), _interactive(true),
+MortimerFlight::MortimerFlight(bool isDemo, bool keepProgress) : _isDemo(isDemo), _keepProgress(keepProgress), _initialInteractive(true), _interactive(true),
_inBranch(false), _steering(kWidth / 2, kHeight / 2), _collisionRadius(25) {
clearProgress();
reset();
@@ -45,14 +45,16 @@ void MortimerFlight::clearProgress() {
}
void MortimerFlight::clearHostiles() {
- // Retail hornet hits also suppress their salt projectiles.
+ if (_isDemo)
+ memset(_hitTargets + 700, 0, 100 * sizeof(_hitTargets[0]));
+ // Hornet hits also suppress their salt projectiles.
memset(_hitTargets + 800, 0, 100 * sizeof(_hitTargets[0]));
memset(_hitTargets + 1000, 0, 400 * sizeof(_hitTargets[0]));
}
void MortimerFlight::reset() {
MortimerCutscene::reset();
- if (_isDemo)
+ if (_isDemo && !_keepProgress)
clearProgress();
_nextFlight.clear();
_interactive = _initialInteractive;
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 47e072750db..84f6c9c1a94 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -38,7 +38,7 @@ public:
bool active, hit;
};
- explicit MortimerFlight(bool isDemo = true);
+ explicit MortimerFlight(bool isDemo = true, bool keepProgress = false);
bool load(Common::SeekableReadStream &stream, bool interactive = true, bool continuation = false);
void clearProgress();
void clearHostiles();
@@ -73,7 +73,7 @@ private:
Common::Array<Target> _targets;
Common::Array<Obstacle> _obstacles;
Common::String _nextFlight;
- const bool _isDemo;
+ const bool _isDemo, _keepProgress;
bool _hitTargets[1400];
bool _initialInteractive, _interactive, _inBranch;
Common::Point _steering;
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 27fc51be18c..c512932b53d 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -111,7 +111,8 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
return keymaps;
}
-MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo), _random("mortimer-flight"), _flight(isDemo), _interview(isDemo),
+MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
+ _restoredContent(isDemo && ConfMan.getBool("mortimer_restored_content")), _random("mortimer-flight"), _flight(isDemo, _restoredContent), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
_segment(0), _carriedStones(0), _completedSpecies(0), _shellShocks(0), _shockFrame(0), _crashCount(0),
@@ -447,7 +448,7 @@ bool MortimerEngine::startFlight() {
_uiFrame = 0;
_shotFrame = 3;
_glowFrame = 10;
- _glowStones = MIN(_carriedStones + (_isDemo ? 0 : _flight.rescuedCount() - _creditedRescues), 999U);
+ _glowStones = MIN(_carriedStones + (_isDemo && !_restoredContent ? 0 : _flight.rescuedCount() - _creditedRescues), 999U);
_pendingGlowStones = 0;
memset(_iconFlash, 0, sizeof(_iconFlash));
for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
@@ -474,7 +475,7 @@ bool MortimerEngine::loadCrashMovie(bool repair) {
stopSounds();
_movieHandles.clear();
const char *name = repair ? "RESOURCE/GENERAL/REPAIR.SAN" :
- (_campaign.scene().region == 3 ? "RESOURCE/GENERAL/D_CRASH.SAN" : "RESOURCE/GENERAL/CRASH.SAN");
+ (!_isDemo && _campaign.scene().region == 3 ? "RESOURCE/GENERAL/D_CRASH.SAN" : "RESOURCE/GENERAL/CRASH.SAN");
Common::File file;
if (!file.open(Common::Path(name))) {
_loadError = Common::String::format("Missing %s", name);
@@ -504,14 +505,26 @@ bool MortimerEngine::finishCrash() {
_carriedStones = _completedSpecies = _creditedRescues = 0;
_interview.resetAttempts();
}
+ if (_isDemo)
+ return loadFlight(0) && startFlight();
return _campaign.returnToFlight() && enterCampaignScene();
}
if (difficulty == 2)
_flight.clearHostiles();
+ if (_isDemo)
+ return loadFlight(_segment) && startFlight();
// Ordinary crashes replay the current branch, including a cached branch.
return loadRetailFlight(_flightName) && startFlight();
}
+void MortimerEngine::creditRescues() {
+ // Restored demo flights retain target IDs across retries and repairs.
+ const uint credited = _isDemo && !_restoredContent ? 0 : _creditedRescues;
+ _carriedStones = MIN(999U, _carriedStones + _flight.rescuedCount() - credited);
+ _creditedRescues = _flight.rescuedCount();
+ _completedSpecies |= _flight.rescuedSpecies();
+}
+
bool MortimerEngine::restart(bool all) {
_crashStage = kNoCrash;
_needsRepair = _hitJolt = false;
@@ -532,7 +545,7 @@ bool MortimerEngine::restart(bool all) {
_shellShocks = 0;
return _campaign.start(region) && enterCampaignScene();
}
- if (_interviewActive)
+ if (_interviewActive || (_restoredContent && _introIndex < 0))
all = true;
if (_introIndex >= 0 && !all)
return startMovie();
@@ -540,7 +553,8 @@ bool MortimerEngine::restart(bool all) {
if (all) {
_interviewActive = false;
_interview.resetRiddle();
- _carriedStones = _completedSpecies = 0;
+ _flight.clearProgress();
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
if (!loadFlight(0))
return false;
}
@@ -567,9 +581,7 @@ bool MortimerEngine::nextScene(bool skip) {
_pendingFlight.clear();
return loadRetailFlight(next) && startMovie();
}
- _carriedStones = MIN(999U, _carriedStones + _flight.rescuedCount() - _creditedRescues);
- _creditedRescues = _flight.rescuedCount();
- _completedSpecies |= _flight.rescuedSpecies();
+ creditRescues();
_flight.context().conditions[MortimerMovieContext::kAtGate] = true;
}
if (!_campaign.advance()) {
@@ -604,8 +616,7 @@ bool MortimerEngine::nextScene(bool skip) {
return true;
}
if (_segment + 1 == ARRAYSIZE(kDemoFlights)) {
- _carriedStones += _flight.rescuedCount();
- _completedSpecies |= _flight.rescuedSpecies();
+ creditRescues();
_glowStones = MIN(_carriedStones, 999U);
_pendingGlowStones = 0;
_glowFrame = 10;
@@ -622,8 +633,7 @@ bool MortimerEngine::nextScene(bool skip) {
}
debug(1, "Mortimer: demo flight transition %s -> %s at frame %u/%u%s",
kDemoFlights[_segment], kDemoFlights[_segment + 1], _flight.frame() + 1, _flight.frameCount(), skip ? " (skipped)" : "");
- _carriedStones += _flight.rescuedCount();
- _completedSpecies |= _flight.rescuedSpecies();
+ creditRescues();
return loadFlight(_segment + 1) && startFlight();
}
@@ -642,6 +652,11 @@ bool MortimerEngine::processInterview() {
}
return _campaign.returnToFlight() && enterCampaignScene();
}
+ if (_restoredContent) {
+ // A new demo flight after an interview must offer rescues again.
+ _flight.clearProgress();
+ _creditedRescues = _crashCount = 0;
+ }
return loadFlight(0) && startFlight();
}
if (_interview.finished()) {
@@ -734,7 +749,7 @@ bool MortimerEngine::updateScene() {
if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
// Retail crashes on the fifteenth hit; the demo allows eighteen.
if (_shellShocks + 1 >= (_isDemo ? 18U : 15U)) {
- if (!(_isDemo ? restart() : startCrash()))
+ if (!(_isDemo && !_restoredContent ? restart() : startCrash()))
return false;
} else {
++_shellShocks;
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 040143dc985..4ce6d7cbac0 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -63,6 +63,7 @@ private:
bool startCrash();
bool loadCrashMovie(bool repair);
bool finishCrash();
+ void creditRescues();
bool processInterview();
bool updateScene();
bool nextScene(bool skip = false);
@@ -88,7 +89,7 @@ private:
void playCollisionSound();
void stopSounds();
- const bool _isDemo;
+ const bool _isDemo, _restoredContent;
Common::RandomSource _random;
MortimerCutscene _intro;
MortimerFlight _flight;
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 74534a55666..bdb1c553263 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -986,6 +986,15 @@ const ExtraGuiOption enableRebel1RestoredContent = {
0
};
+const ExtraGuiOption enableMortimerRestoredContent = {
+ _s("Enable restored content"),
+ _s("Play the demo's unused crash and repair animations using the full game's recovery rules"),
+ "mortimer_restored_content",
+ false,
+ 0,
+ 0
+};
+
const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
ExtraGuiOptions options;
// Query the GUI options
@@ -1051,6 +1060,10 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_RESTORED_CONTENT)) {
options.push_back(enableRebel1RestoredContent);
}
+ if (target.empty() || guiOptions.contains(GAMEOPTION_MORTIMER_RESTORED_CONTENT) ||
+ (gameid == "mortimer" && extra == "Demo")) {
+ options.push_back(enableMortimerRestoredContent);
+ }
if (target.empty() || gameid == "comi") {
options.push_back(comiObjectLabelsOption);
Commit: eaa45832a073b17e8926cff9d8addec4368192da
https://github.com/scummvm/scummvm/commit/eaa45832a073b17e8926cff9d8addec4368192da
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: implemented dificulty levels
Changed paths:
A engines/scumm/insane/mortimer/dialogs.cpp
A engines/scumm/insane/mortimer/dialogs.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/dialogs.cpp b/engines/scumm/insane/mortimer/dialogs.cpp
new file mode 100644
index 00000000000..c9ffa44b701
--- /dev/null
+++ b/engines/scumm/insane/mortimer/dialogs.cpp
@@ -0,0 +1,95 @@
+/* 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 "scumm/insane/mortimer/dialogs.h"
+
+#include "common/translation.h"
+#include "common/util.h"
+#include "gui/gui-manager.h"
+#include "gui/ThemeEval.h"
+#include "gui/widgets/popup.h"
+
+namespace Scumm {
+
+MortimerDifficultyDialog::MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty)
+ : GUI::Dialog("MortimerDifficultyDialog") {
+ new GUI::StaticTextWidget(this, _name + ".Title", _("Select difficulty"));
+ new GUI::StaticTextWidget(this, _name + ".FlightLabel", _("Flight difficulty:"));
+ new GUI::StaticTextWidget(this, _name + ".RiddleLabel", _("Riddle difficulty:"));
+ _flight = new GUI::PopUpWidget(this, _name + ".Flight", _("Choose how difficult flying and recovering from crashes should be."));
+ _riddle = new GUI::PopUpWidget(this, _name + ".Riddle", isDemo ?
+ _("The demo includes only one set of riddles.") : _("Choose the riddle difficulty and the cost and number of guesses."));
+ const char *const levels[] = {_s("Easy"), _s("Medium"), _s("Hard")};
+ for (uint i = 0; i < ARRAYSIZE(levels); ++i) {
+ _flight->appendEntry(_(levels[i]), i);
+ if (!isDemo)
+ _riddle->appendEntry(_(levels[i]), i);
+ }
+ if (isDemo) {
+ _riddle->appendEntry(_("Fixed (demo)"), 0);
+ _riddle->setEnabled(false);
+ }
+ _flight->setSelectedTag(MIN(flightDifficulty, 2U));
+ _riddle->setSelectedTag(isDemo ? 0 : MIN(riddleDifficulty, 2U));
+ new GUI::ButtonWidget(this, _name + ".Cancel", _("~C~ancel"), Common::U32String(), GUI::kCloseCmd);
+ new GUI::ButtonWidget(this, _name + ".Start", _("Start"), Common::U32String(), GUI::kOKCmd, Common::ASCII_RETURN);
+}
+
+uint MortimerDifficultyDialog::flightDifficulty() const {
+ return _flight->getSelectedTag();
+}
+
+uint MortimerDifficultyDialog::riddleDifficulty() const {
+ return _riddle->getSelectedTag();
+}
+
+void MortimerDifficultyDialog::reflowLayout() {
+ GUI::ThemeEval &layouts = *g_gui.xmlEval();
+ const int lineHeight = layouts.getVar("Globals.Line.Height");
+ const int width = 240 * g_gui.getScaleFactor();
+ layouts.addDialog(_name, "screen_center")
+ .addLayout(GUI::ThemeLayout::kLayoutVertical, 8)
+ .addPadding(8, 8, 8, 8)
+ .addWidget("Title", "", width, lineHeight, Graphics::kTextAlignCenter)
+ .addWidget("FlightLabel", "", -1, lineHeight)
+ .addWidget("Flight", "PopUp")
+ .addWidget("RiddleLabel", "", -1, lineHeight)
+ .addWidget("Riddle", "PopUp")
+ .addLayout(GUI::ThemeLayout::kLayoutHorizontal, 8, GUI::ThemeLayout::kItemAlignEnd)
+ .addPadding(0, 0, 0, 0)
+ .addSpace()
+ .addWidget("Cancel", "Button")
+ .addWidget("Start", "Button")
+ .closeLayout()
+ .closeLayout()
+ .closeDialog();
+ GUI::Dialog::reflowLayout();
+}
+
+void MortimerDifficultyDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
+ if (cmd == GUI::kOKCmd) {
+ setResult(1);
+ close();
+ } else {
+ GUI::Dialog::handleCommand(sender, cmd, data);
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/dialogs.h b/engines/scumm/insane/mortimer/dialogs.h
new file mode 100644
index 00000000000..b69188539e5
--- /dev/null
+++ b/engines/scumm/insane/mortimer/dialogs.h
@@ -0,0 +1,48 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_DIALOGS_H
+#define SCUMM_INSANE_MORTIMER_DIALOGS_H
+
+#include "gui/dialog.h"
+
+namespace GUI {
+class PopUpWidget;
+}
+
+namespace Scumm {
+
+class MortimerDifficultyDialog : public GUI::Dialog {
+public:
+ MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty);
+ uint flightDifficulty() const;
+ uint riddleDifficulty() const;
+ void reflowLayout() override;
+
+protected:
+ void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
+
+private:
+ GUI::PopUpWidget *_flight, *_riddle;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index c512932b53d..a82d4a4777e 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -19,6 +19,7 @@
*/
#include "scumm/insane/mortimer/mortimer.h"
+#include "scumm/insane/mortimer/dialogs.h"
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
@@ -112,7 +113,8 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
}
MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
- _restoredContent(isDemo && ConfMan.getBool("mortimer_restored_content")), _random("mortimer-flight"), _flight(isDemo, _restoredContent), _interview(isDemo),
+ _restoredContent(isDemo && ConfMan.getBool("mortimer_restored_content")), _flightDifficulty(0), _riddleDifficulty(0),
+ _random("mortimer-flight"), _flight(isDemo, _restoredContent), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
_segment(0), _carriedStones(0), _completedSpecies(0), _shellShocks(0), _shockFrame(0), _crashCount(0),
@@ -120,8 +122,6 @@ MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _
_controllerMode(false), _interviewActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
- ConfMan.registerDefault("mortimer_flight_difficulty", 0);
- ConfMan.registerDefault("mortimer_riddle_difficulty", 0);
}
MortimerEngine::~MortimerEngine() {
@@ -133,6 +133,23 @@ bool MortimerEngine::hasFeature(EngineFeature feature) const {
return feature == kSupportsReturnToLauncher;
}
+bool MortimerEngine::selectDifficulty() {
+ if (_isDemo && !_restoredContent)
+ return true;
+ MortimerDifficultyDialog dialog(_isDemo, _flightDifficulty, _riddleDifficulty);
+ if (runDialog(dialog) != 1 || shouldQuit())
+ return false;
+ setDifficulty(dialog.flightDifficulty(), dialog.riddleDifficulty());
+ return true;
+}
+
+void MortimerEngine::setDifficulty(uint flight, uint riddle) {
+ _flightDifficulty = MIN(flight, 2U);
+ _riddleDifficulty = MIN(riddle, 2U);
+ _flight.setCollisionRadius(_isDemo && !_restoredContent ? 25 : 20 * (_flightDifficulty + 1));
+ _interview.setDifficulty(_riddleDifficulty);
+}
+
bool MortimerEngine::loadResources() {
Common::File file;
@@ -280,7 +297,6 @@ bool MortimerEngine::loadRegion(uint region) {
}
_loadedRegion = region;
_flight.clearProgress();
- _flight.setCollisionRadius(20 * (CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2) + 1));
_interview.resetRiddle();
_carriedStones = _completedSpecies = _creditedRescues = _shellShocks = 0;
_shockFrame = _crashCount = 0;
@@ -462,8 +478,7 @@ bool MortimerEngine::startFlight() {
bool MortimerEngine::startCrash() {
// SHELL's difficulty table permits these crashes before a full repair.
const uint allowances[] = {4, 3, 1};
- const uint difficulty = CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2);
- _needsRepair = ++_crashCount > allowances[difficulty];
+ _needsRepair = ++_crashCount > allowances[_flightDifficulty];
if (_needsRepair)
_crashCount = 0;
_hitJolt = false;
@@ -491,14 +506,13 @@ bool MortimerEngine::loadCrashMovie(bool repair) {
}
bool MortimerEngine::finishCrash() {
- const uint difficulty = CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2);
_crashStage = kNoCrash;
_shellShocks = _shockFrame = 0;
_pendingFlight.clear();
if (_needsRepair) {
_needsRepair = false;
// Easy flight keeps rescues and stones even after a full repair.
- if (difficulty != 0) {
+ if (_flightDifficulty != 0) {
const bool girl = _flight.context().girl;
_flight.clearProgress();
_flight.context().girl = girl;
@@ -509,7 +523,7 @@ bool MortimerEngine::finishCrash() {
return loadFlight(0) && startFlight();
return _campaign.returnToFlight() && enterCampaignScene();
}
- if (difficulty == 2)
+ if (_flightDifficulty == 2)
_flight.clearHostiles();
if (_isDemo)
return loadFlight(_segment) && startFlight();
@@ -719,7 +733,7 @@ bool MortimerEngine::updateScene() {
return true;
} else if (interactive()) {
if (_hitJolt) {
- const int amount = 64 * (CLIP(ConfMan.getInt("mortimer_flight_difficulty"), 0, 2) + 1);
+ const int amount = 64 * (_flightDifficulty + 1);
const int y = _random.getRandomBit() ? amount : -amount;
const int x = _random.getRandomBit() ? amount : -amount;
updateView(x, y);
@@ -873,6 +887,8 @@ Common::Error MortimerEngine::run() {
_screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
_screen.fillRect(Common::Rect(640, 480), 10);
syncSoundSettings();
+ if (!selectDifficulty())
+ return Common::kNoError;
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
bool exit;
@@ -883,7 +899,6 @@ Common::Error MortimerEngine::run() {
_subtitleName = "RESOURCE/DATA/BEGIN.TRS";
if (!_campaign.start(boot < 0 ? 24 : boot))
_campaign.start();
- _interview.setDifficulty(CLIP(ConfMan.getInt("mortimer_riddle_difficulty"), 0, 2));
exit = !enterCampaignScene();
} else if (boot == 4 || boot == 10 || boot == 18 || boot == 8 || boot == 25) {
stopSounds();
@@ -975,7 +990,7 @@ Common::Error MortimerEngine::run() {
draw();
if (_finished) {
GUI::MessageDialog dialog(_("Play again?"), _("Restart"), _("Quit"));
- if (dialog.runModal() != GUI::kMessageOK || !restart(true))
+ if (dialog.runModal() != GUI::kMessageOK || !selectDifficulty() || !restart(true))
break;
}
_system->delayMillis(10);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 4ce6d7cbac0..2937ad9f5be 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -49,6 +49,8 @@ private:
enum CrashStage { kNoCrash, kCrashMovie, kRepairMovie };
bool loadResources();
+ bool selectDifficulty();
+ void setDifficulty(uint flight, uint riddle);
uint introCount() const;
bool loadIntro(uint index);
bool loadFlight(uint segment);
@@ -90,6 +92,7 @@ private:
void stopSounds();
const bool _isDemo, _restoredContent;
+ uint _flightDifficulty, _riddleDifficulty;
Common::RandomSource _random;
MortimerCutscene _intro;
MortimerFlight _flight;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b340a3c4a71..e651899e80e 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -148,6 +148,7 @@ MODULE_OBJS += \
insane/insane_scenes.o \
insane/insane_iact.o \
insane/mortimer/campaign.o \
+ insane/mortimer/dialogs.o \
insane/mortimer/flight.o \
insane/mortimer/interview.o \
insane/mortimer/movie.o \
Commit: 972d6b1a9a1a5fd5c88661354cf38bc425327649
https://github.com/scummvm/scummvm/commit/972d6b1a9a1a5fd5c88661354cf38bc425327649
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: enable view jolt when taking damage in demo
Changed paths:
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index a82d4a4777e..809055acb53 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -768,7 +768,7 @@ bool MortimerEngine::updateScene() {
} else {
++_shellShocks;
_shockFrame = 0;
- _hitJolt = !_isDemo;
+ _hitJolt = !_isDemo || _restoredContent;
}
// Movie changes stop audio, so enqueue the fatal impact afterwards.
playCollisionSound();
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index bdb1c553263..422b8e9d353 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -988,7 +988,7 @@ const ExtraGuiOption enableRebel1RestoredContent = {
const ExtraGuiOption enableMortimerRestoredContent = {
_s("Enable restored content"),
- _s("Play the demo's unused crash and repair animations using the full game's recovery rules"),
+ _s("Enable the full game's view jolt and recovery rules, including the demo's unused crash and repair animations"),
"mortimer_restored_content",
false,
0,
Commit: 09f67f2a8e034423fe3b700eef3345c3b42c0ec7
https://github.com/scummvm/scummvm/commit/09f67f2a8e034423fe3b700eef3345c3b42c0ec7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: abstracted SAUD code and use it for Mortimer and RA
Changed paths:
A engines/scumm/smush/saud_stream.cpp
A engines/scumm/smush/saud_stream.h
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/insane/mortimer/movie.h
engines/scumm/insane/rebel/rebel_audio.cpp
engines/scumm/insane/rebel/rebel_audio.h
engines/scumm/module.mk
engines/scumm/smush/saud.cpp
engines/scumm/smush/saud.h
engines/scumm/smush/smush_player.h
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 2c9c0d60717..5a832cf4284 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -143,6 +143,15 @@ bool MortimerInterview::loadResources(uint region) {
}
file.close();
}
+ _mouth.clear();
+ if (!_isDemo) {
+ const Common::String mouth = Common::String::format("%s/%cG_MOUTH.NUT", resources.directory, resources.prefix);
+ if (!file.open(Common::Path(mouth)) || !loadMortimerSprites(file, _mouth) || _mouth.size() < 6) {
+ _error = Common::String::format("Missing or invalid %s", mouth.c_str());
+ return false;
+ }
+ file.close();
+ }
if (!_font.load("RESOURCE/GENERAL/MORTFNTH.NUT") || !_captionFont.load(_isDemo ? "RESOURCE/GENERAL/SUBTFONT.NUT" : "RESOURCE/GENERAL/ESTRO20.NUT")) {
_error = "Missing or invalid Mortimer font";
return false;
@@ -478,9 +487,14 @@ void MortimerInterview::drawText(Graphics::Surface &screen, const Common::String
_font.drawString(&screen, lines[first + i], 29, top + i * height, 330, _isDemo ? 0 : 1, Graphics::kTextAlignCenter);
}
-void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &movie) const {
+void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &movie, byte mouth) const {
if (fullScreen()) {
screen.copyRectToSurface(movie.pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
+ if (!_isDemo && _stage == kSong) {
+ // Song callback 4119a0 and tables 4438a0/442d4c use STRK variable 1.
+ const Common::Point origins[] = {{97, 63}, {88, 17}, {181, 0}, {100, 33}};
+ _mouth[MIN<uint>(mouth, 5)].draw(screen, origins[_region].x * 2, origins[_region].y * 2, false, nullptr, 2);
+ }
for (uint i = 0; _isDemo && i < movie.captions().size(); ++i) {
const MortimerMovie::Caption &caption = movie.captions()[i];
if (caption.id < ARRAYSIZE(_captions))
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index 38e63f1e183..b1e75453a0d 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -48,7 +48,7 @@ public:
void movieFinished();
void movieLoaded() { _moviePending = false; }
void skip();
- void draw(Graphics::Surface &screen, const MortimerMovie &movie) const;
+ void draw(Graphics::Surface &screen, const MortimerMovie &movie, byte mouth) const;
void drawCursor(Graphics::Surface &screen, const Common::Point &point) const;
bool needsMovie() const { return _moviePending; }
@@ -88,7 +88,7 @@ private:
Common::RandomSource _random;
const bool _isDemo;
uint _region, _difficulty, _textDifficulty, _tried;
- Common::Array<MortimerSprite> _panel, _portraits, _gate, _cursor;
+ Common::Array<MortimerSprite> _panel, _portraits, _gate, _cursor, _mouth;
MortimerFont _font, _captionFont;
Common::String _dialogue[7][3], _riddles[7][3], _captions[16];
Common::String _movieName, _error;
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 809055acb53..9dc2e35efed 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -22,7 +22,6 @@
#include "scumm/insane/mortimer/dialogs.h"
#include "audio/audiostream.h"
-#include "audio/decoders/raw.h"
#include "backends/keymapper/action.h"
#include "common/config-manager.h"
#include "common/endian.h"
@@ -201,7 +200,7 @@ bool MortimerEngine::loadResources() {
file.close();
}
// The hit sound animation uses volume 50 on the original 0..127 scale.
- _collisionSound.volume = 100;
+ _collisionSound.volume = 50;
if (_isDemo && !_interview.loadResources()) {
_loadError = _interview.errorMessage();
return false;
@@ -398,24 +397,28 @@ bool MortimerEngine::enterCampaignScene() {
return startMovie();
}
-void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset) {
+void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint64 skipFrames) {
_mixer->stopHandle(handle);
- // The engine owns the samples until all its handles have been stopped.
- Audio::AudioStream *stream = Audio::makeRawStream(sound.samples.begin() + offset, sound.samples.size() - offset, sound.rate,
- Audio::FLAG_UNSIGNED, DisposeAfterUse::NO);
- _mixer->playStream(type, &handle, stream, -1, sound.volume, sound.balance);
+ SaudStream *stream = new SaudStream(sound.samples, sound.script, sound.rate, _soundState, sound.volume, sound.balance);
+ stream->skip(skipFrames);
+ if (stream->endOfData()) {
+ delete stream;
+ return;
+ }
+ _mixer->playStream(type, &handle, stream);
}
void MortimerEngine::playMovieSounds(bool seek) {
+ if (seek)
+ stopSounds();
const MortimerMovie &movie = currentMovie();
for (uint i = 0; i < movie.sounds().size(); ++i) {
const MortimerSound &sound = movie.sounds()[i];
if (!movie.soundEnabled(i) || sound.frame > movie.frame() || (!seek && sound.frame != movie.frame()))
continue;
- const uint64 offset = uint64(movie.frame() - sound.frame) * sound.rate / movie.frameRate();
- if (offset < sound.samples.size())
- playSound(sound, _movieHandles[i], sound.music ? Audio::Mixer::kMusicSoundType :
- (sound.speech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType), offset);
+ const uint64 elapsed = uint64(movie.frame() - sound.frame) * SaudStream::kRate / movie.frameRate();
+ playSound(sound, _movieHandles[i], sound.music ? Audio::Mixer::kMusicSoundType :
+ (sound.speech ? Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType), elapsed);
}
}
@@ -437,6 +440,7 @@ void MortimerEngine::stopSounds() {
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
_mixer->stopHandle(_glowHandle);
+ _soundState.reset();
}
bool MortimerEngine::startMovie(bool preserveScreen) {
@@ -864,6 +868,8 @@ void MortimerEngine::updateInterface() {
}
bool MortimerEngine::speechPlaying() const {
+ if (!_soundState.variable(2))
+ return false;
for (uint i = 0; i < _movieHandles.size(); ++i)
if (currentMovie().sounds()[i].speech && _mixer->isSoundHandleActive(_movieHandles[i]))
return true;
@@ -1095,7 +1101,7 @@ void MortimerEngine::drawDashboard() {
void MortimerEngine::draw() {
if (_interviewActive) {
- _interview.draw(_screen, _intro);
+ _interview.draw(_screen, _intro, _soundState.variable(1));
if (_interview.showDashboard())
drawDashboard();
_interview.drawCursor(_screen, _mouse);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 2937ad9f5be..24401d946b5 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -29,6 +29,7 @@
#include "scumm/insane/mortimer/flight.h"
#include "scumm/insane/mortimer/interview.h"
#include "scumm/insane/mortimer/campaign.h"
+#include "scumm/smush/saud_stream.h"
namespace Scumm {
@@ -86,7 +87,7 @@ private:
void drawTargets();
void drawDashboard();
bool speechPlaying() const;
- void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint offset = 0);
+ void playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint64 skipFrames = 0);
void playMovieSounds(bool seek = false);
void playCollisionSound();
void stopSounds();
@@ -104,6 +105,7 @@ private:
Common::Array<MortimerSprite> _scopeHit, _slimeHit, _scopeBeam, _slimeBall, _numbers, _corners;
MortimerSound _scopeSound, _rescueSound, _collisionSound, _slimeSound, _slimeHitSound, _glowSound, _balloonSound;
MortimerSound _buttonSound, _payoutSound;
+ SaudStreamState _soundState;
Common::Array<Audio::SoundHandle> _movieHandles;
Common::Array<Audio::SoundHandle> _collisionHandles;
Audio::SoundHandle _scopeHandle, _rescueHandle, _glowHandle;
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 6d68a9f0ba7..7b374fc7214 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -153,7 +153,7 @@ bool validObject(const byte *data, uint32 size, bool sprite = false) {
return false;
const byte codec = data[0];
if (sprite)
- return codec == 44 || (codec == 1 && validRows(data + 14, size - 14, height, false));
+ return codec == 44 || ((codec == 1 || codec == 3) && validRows(data + 14, size - 14, height, false));
if (codec == 37)
return validBackground(data + 14, size - 14, (width + 3) & ~3, (height + 3) & ~3);
if (codec != 1 && codec != 3 && codec != 23)
@@ -187,20 +187,32 @@ bool validTransition(const byte *data, uint32 size) {
} // End of anonymous namespace
-void MortimerSprite::draw(Graphics::Surface &screen, int dstX, int dstY, bool centered, const Common::Rect *bounds) const {
+void MortimerSprite::draw(Graphics::Surface &screen, int dstX, int dstY, bool centered, const Common::Rect *bounds, int scale) const {
// Sprite offsets are relative to the animation's origin, even when centered.
if (centered) {
- dstX -= (x + width) / 2;
- dstY -= (y + height) / 2;
+ dstX -= (x + width) * scale / 2;
+ dstY -= (y + height) * scale / 2;
}
- dstX += x;
- dstY += y;
- Common::Rect clip(dstX, dstY, dstX + width, dstY + height);
+ dstX += x * scale;
+ dstY += y * scale;
+ Common::Rect clip(dstX, dstY, dstX + width * scale, dstY + height * scale);
clip.clip(Common::Rect(screen.w, screen.h));
if (bounds)
clip.clip(*bounds);
if (clip.isEmpty())
return;
+ if (scale != 1) {
+ for (int row = clip.top; row < clip.bottom; ++row) {
+ const byte *src = pixels.begin() + (row - dstY) / scale * width;
+ byte *dst = (byte *)screen.getBasePtr(clip.left, row);
+ for (int col = clip.left; col < clip.right; ++col, ++dst) {
+ const byte color = src[(col - dstX) / scale];
+ if (color)
+ *dst = color;
+ }
+ }
+ return;
+ }
const byte *src = pixels.begin() + (clip.top - dstY) * width + clip.left - dstX;
screen.copyRectToSurfaceWithKey(src, width, clip.left, clip.top, clip.width(), clip.height(), 0);
}
@@ -302,6 +314,16 @@ bool loadMortimerSound(Common::SeekableReadStream &stream, MortimerSound &sound)
sound.samples.clear();
return false;
}
+ sound.script.clear();
+ if (header.scriptOffset >= 0) {
+ sound.script.resize(header.scriptSize);
+ if (!stream.seek(header.scriptOffset) || stream.read(sound.script.begin(), header.scriptSize) != header.scriptSize || stream.err())
+ return false;
+ if (sound.script.empty()) {
+ sound.script.push_back(SAUD_OP_STOP);
+ sound.script.push_back(0);
+ }
+ }
sound.rate = header.sampleRate;
return true;
}
@@ -449,8 +471,8 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled, bool
track.sound.frame = _frames.size();
track.sound.music = (flags & 64) != 0;
track.sound.speech = (flags & 128) != 0;
- track.sound.volume = MIN(255, body[8] * 2);
- track.sound.balance = body[9] == 128 ? 0 : (int(body[9]) - 64) * 2;
+ track.sound.volume = MIN(127, body[8] * 127 >> 7);
+ track.sound.balance = body[9] == 128 ? 0 : int8(body[9]);
index = tracks.size();
tracks.push_back(track);
}
diff --git a/engines/scumm/insane/mortimer/movie.h b/engines/scumm/insane/mortimer/movie.h
index 605784740f6..121824f0fe6 100644
--- a/engines/scumm/insane/mortimer/movie.h
+++ b/engines/scumm/insane/mortimer/movie.h
@@ -43,17 +43,18 @@ struct MortimerSprite {
// Codec 44 uses skipped pixels for transparency and relative palette colors.
Common::Array<byte> mask;
- void draw(Graphics::Surface &screen, int dstX, int dstY, bool centered = false, const Common::Rect *bounds = nullptr) const;
+ void draw(Graphics::Surface &screen, int dstX, int dstY, bool centered = false, const Common::Rect *bounds = nullptr, int scale = 1) const;
};
struct MortimerSound {
uint frame = 0;
uint rate = 11025;
- byte volume = 255;
+ byte volume = 127;
int8 balance = 0;
bool music = false;
bool speech = true;
Common::Array<byte> samples;
+ Common::Array<byte> script;
};
struct MortimerMovieContext {
diff --git a/engines/scumm/insane/rebel/rebel_audio.cpp b/engines/scumm/insane/rebel/rebel_audio.cpp
index e5779d9f79b..1d9bd065e69 100644
--- a/engines/scumm/insane/rebel/rebel_audio.cpp
+++ b/engines/scumm/insane/rebel/rebel_audio.cpp
@@ -315,192 +315,37 @@ void RebelAudio::setGroupPan(SmushPlayer *player, int groupId, int pan) {
}
bool RebelAudio::processAudioCodes(SmushPlayer *player, int idx, int32 &tmpFeedSize, int &mixVolume) {
- uint8 *code, *buf, subcode, value;
- int chunk;
-
- while (tmpFeedSize) {
- code = player->_smushDispatch[idx].headerPtr;
-
- switch (code[0]) {
- case SAUD_OP_INIT:
- player->_smushDispatch[idx].audioLength = 0;
- buf = player->_smushDispatch[idx].headerPtr;
- player->_smushDispatch[idx].audioRemaining = READ_BE_UINT32(buf + 2);
- player->_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 6);
- player->_smushDispatch[idx].sampleRate = player->_smushAudioSampleRate;
- player->_smushDispatch[idx].headerPtr += player->_smushDispatch[idx].headerPtr[1] + 2;
- if (player->_smushDispatch[idx].audioRemaining < player->_smushTracks[idx].availableSize + (player->_smushTracks[idx].availableSize >= player->_smushTracks[idx].sdatSize ? 0 : 15000) - player->_smushTracks[idx].dataSize) {
- chunk = player->_smushTracks[idx].availableSize - player->_smushTracks[idx].dataSize - player->_smushDispatch[idx].audioRemaining + 15000;
- if (chunk > player->_smushDispatch[idx].currentOffset) {
- player->_smushTracks[idx].state = TRK_STATE_INACTIVE;
- player->_smushTracks[idx].groupId = GRP_MASTER;
- tmpFeedSize = 0;
- break;
- }
-
- player->_smushDispatch[idx].audioRemaining += chunk;
- player->_smushDispatch[idx].currentOffset -= chunk;
- }
- break;
-
- case SAUD_OP_UPDATE_HEADER:
- case SAUD_OP_COMPARE_GT:
- case SAUD_OP_COMPARE_LT:
- case SAUD_OP_COMPARE_EQ:
- case SAUD_OP_COMPARE_NE:
- subcode = code[4];
- switch (subcode) {
- case SAUD_VALUEID_ALL_VOLS:
- value = player->_smushTrackVols[0];
- break;
- case SAUD_VALUEID_TRK_VOL:
- value = player->_smushTracks[idx].volume;
- break;
- case SAUD_VALUEID_TRK_PAN:
- value = player->_smushTracks[idx].pan;
- break;
- default:
- value = player->_smushAudioTable[subcode];
- break;
- }
-
- switch (code[0]) {
- case SAUD_OP_UPDATE_HEADER:
- value = value || (subcode == 0);
- break;
- case SAUD_OP_COMPARE_GT:
- value = value > code[5];
- break;
- case SAUD_OP_COMPARE_LT:
- value = value < code[5];
- break;
- case SAUD_OP_COMPARE_EQ:
- value = value == code[5];
- break;
- case SAUD_OP_COMPARE_NE:
- value = value != code[5];
- break;
- default:
- break;
- }
-
- if (!value) {
- player->_smushDispatch[idx].headerPtr = &code[code[1] + 2];
- } else {
- // Rebel Assault SAUD branch targets are signed relative displacements.
- player->_smushDispatch[idx].headerPtr = code + READ_BE_INT16(&code[2]);
- }
- break;
-
- case SAUD_OP_SET_PARAM:
- switch (code[2]) {
- case SAUD_VALUEID_ALL_VOLS:
- player->_smushTrackVols[0] = code[3];
- mixVolume = computeMixVolume(player, idx);
- break;
- case SAUD_VALUEID_TRK_VOL:
- player->_smushTracks[idx].volume = code[3];
- mixVolume = computeMixVolume(player, idx);
- break;
- case SAUD_VALUEID_TRK_PAN:
- player->_smushTracks[idx].pan = code[3];
- break;
- default:
- player->_smushAudioTable[code[2]] = code[3];
- break;
- }
- player->_smushDispatch[idx].headerPtr = &code[code[1] + 2];
+ if (tmpFeedSize <= 0)
+ return true;
+ SmushPlayer::SmushAudioTrack &track = player->_smushTracks[idx];
+ SmushPlayer::SmushAudioDispatch &dispatch = player->_smushDispatch[idx];
+ SaudSpan span;
+ uint skippedSpans = 0;
+ while (tmpFeedSize > 0 && _scripts[idx].nextSpan(span, player->_smushAudioTable,
+ player->_smushTrackVols[0], track.volume, track.pan)) {
+ if (++skippedSpans > 65536) {
+ warning("SAUD: track %d loops over replaced audio", idx);
break;
-
- case SAUD_OP_INCR_PARAM:
- switch (code[2]) {
- case SAUD_VALUEID_ALL_VOLS:
- player->_smushTrackVols[0] += code[3];
- mixVolume = computeMixVolume(player, idx);
- break;
- case SAUD_VALUEID_TRK_VOL:
- player->_smushTracks[idx].volume += code[3];
- mixVolume = computeMixVolume(player, idx);
- break;
- case SAUD_VALUEID_TRK_PAN:
- player->_smushTracks[idx].pan += code[3];
- break;
- default:
- player->_smushAudioTable[code[2]] += code[3];
+ }
+ mixVolume = computeMixVolume(player, idx);
+ dispatch.audioRemaining = span.offset;
+ dispatch.currentOffset = span.length;
+ dispatch.sampleRate = span.rate;
+
+ // A backward branch may refer to samples already replaced in the ring buffer.
+ if (dispatch.audioRemaining < track.availableSize + (track.availableSize >= track.sdatSize ? 0 : 15000) - track.dataSize) {
+ const int32 skipped = track.availableSize - track.dataSize - dispatch.audioRemaining + 15000;
+ if (skipped > dispatch.currentOffset)
break;
- }
- player->_smushDispatch[idx].headerPtr = &code[code[1] + 2];
- break;
-
- case SAUD_OP_SET_OFFSET:
- player->_smushDispatch[idx].audioLength = 0;
- buf = player->_smushDispatch[idx].headerPtr;
- player->_smushDispatch[idx].audioRemaining = READ_BE_UINT32(buf + 2);
- player->_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 6);
- player->_smushDispatch[idx].sampleRate = READ_BE_UINT32(buf + 10);
-
- player->_smushDispatch[idx].headerPtr += player->_smushDispatch[idx].headerPtr[1] + 2;
- if (player->_smushDispatch[idx].audioRemaining < player->_smushTracks[idx].availableSize + (player->_smushTracks[idx].availableSize >= player->_smushTracks[idx].sdatSize ? 0 : 15000) - player->_smushTracks[idx].dataSize) {
- chunk = player->_smushTracks[idx].availableSize - player->_smushTracks[idx].dataSize - player->_smushDispatch[idx].audioRemaining + 15000;
- if (chunk > player->_smushDispatch[idx].currentOffset) {
- player->_smushTracks[idx].state = TRK_STATE_INACTIVE;
- player->_smushTracks[idx].groupId = GRP_MASTER;
- tmpFeedSize = 0;
- break;
- }
-
- player->_smushDispatch[idx].audioRemaining += chunk;
- player->_smushDispatch[idx].currentOffset -= chunk;
- }
- break;
-
- case SAUD_OP_SET_LENGTH:
- if (!player->_smushDispatch[idx].audioLength) {
- player->_smushDispatch[idx].audioLength = READ_BE_UINT32(&code[6]);
- player->_smushDispatch[idx].elapsedAudio = 0;
- }
-
- buf = player->_smushDispatch[idx].headerPtr;
- player->_smushDispatch[idx].audioRemaining = player->_smushDispatch[idx].elapsedAudio + READ_BE_UINT32(buf + 2);
-
- player->_smushDispatch[idx].currentOffset = READ_BE_UINT32(buf + 14);
- if (player->_smushDispatch[idx].currentOffset > player->_smushDispatch[idx].audioLength)
- player->_smushDispatch[idx].currentOffset = player->_smushDispatch[idx].audioLength;
-
- player->_smushDispatch[idx].sampleRate = READ_BE_UINT32(buf + 10);
-
- player->_smushDispatch[idx].audioLength -= player->_smushDispatch[idx].currentOffset;
- player->_smushDispatch[idx].elapsedAudio += player->_smushDispatch[idx].currentOffset;
-
- if (player->_smushDispatch[idx].audioLength) {
- player->_smushDispatch[idx].headerPtr = &code[code[1] + 2];
- } else {
- player->_smushDispatch[idx].headerPtr = code + READ_BE_INT16(&code[18]);
- }
-
- if (player->_smushDispatch[idx].audioRemaining >= player->_smushTracks[idx].availableSize + (player->_smushTracks[idx].availableSize >= player->_smushTracks[idx].sdatSize ? 0 : 15000) - player->_smushTracks[idx].dataSize) {
- chunk = player->_smushTracks[idx].availableSize - player->_smushTracks[idx].dataSize - player->_smushDispatch[idx].audioRemaining + 15000;
- if (chunk > player->_smushDispatch[idx].currentOffset) {
- player->_smushTracks[idx].state = TRK_STATE_INACTIVE;
- player->_smushTracks[idx].groupId = GRP_MASTER;
- tmpFeedSize = 0;
- } else {
- player->_smushDispatch[idx].audioRemaining += chunk;
- player->_smushDispatch[idx].currentOffset -= chunk;
- }
- }
- break;
-
- default:
- player->_smushTracks[idx].state = TRK_STATE_INACTIVE;
- player->_smushTracks[idx].groupId = GRP_MASTER;
- tmpFeedSize = 0;
+ dispatch.audioRemaining += skipped;
+ dispatch.currentOffset -= skipped;
}
-
- if (player->_smushDispatch[idx].currentOffset > 0)
+ if (dispatch.currentOffset > 0)
return false;
}
-
+ track.state = TRK_STATE_INACTIVE;
+ track.groupId = GRP_MASTER;
+ tmpFeedSize = 0;
return true;
}
@@ -628,7 +473,7 @@ void RebelAudio::processFrame(SmushPlayer *player, int16 feedSize) {
dispatch.volumeStep = 16;
stopStream(i);
- dispatch.headerPtr = track.dataBuf;
+ _scripts[i].reset(track.dataBuf, track.subChunkPtr - track.dataBuf - 8, player->_smushAudioSampleRate);
dispatch.dataBuf = track.subChunkPtr;
dispatch.dataSize = track.dataSize;
dispatch.currentOffset = 0;
diff --git a/engines/scumm/insane/rebel/rebel_audio.h b/engines/scumm/insane/rebel/rebel_audio.h
index 20ae8d10a0d..856bb0d75eb 100644
--- a/engines/scumm/insane/rebel/rebel_audio.h
+++ b/engines/scumm/insane/rebel/rebel_audio.h
@@ -24,6 +24,7 @@
#include "audio/mixer.h"
#include "common/scummsys.h"
+#include "scumm/smush/saud.h"
namespace Audio {
class QueuingAudioStream;
@@ -70,6 +71,7 @@ private:
bool _fadeTrackActive[kMaxTracks];
Audio::Mixer::SoundType _streamTypes[kMaxTracks];
Audio::Mixer::SoundType _fadeStreamTypes[kMaxTracks];
+ SaudScript _scripts[kMaxTracks];
int _sampleRate;
};
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index e651899e80e..faabd6bb7c3 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -180,6 +180,7 @@ MODULE_OBJS += \
smush/codec37.o \
smush/codec47.o \
smush/saud.o \
+ smush/saud_stream.o \
smush/smush_player.o \
smush/rebel/codec_ra1.o \
smush/rebel/codec_ra2.o \
diff --git a/engines/scumm/smush/saud.cpp b/engines/scumm/smush/saud.cpp
index 28ec9195a50..0a890a19667 100644
--- a/engines/scumm/smush/saud.cpp
+++ b/engines/scumm/smush/saud.cpp
@@ -22,6 +22,7 @@
#include "common/endian.h"
#include "common/stream.h"
+#include "common/textconsole.h"
#include "common/util.h"
namespace Scumm {
@@ -57,12 +58,18 @@ bool readSaudHeader(Common::SeekableReadStream &stream, SaudHeader &header, bool
break;
} else if (tag == MKTAG('S', 'H', 'D', 'R') && chunkSize == 4) {
parsed.sampleRate = stream.readUint32LE();
- } else if (tag == MKTAG('S', 'T', 'R', 'K') && chunkSize >= 14) {
- const byte code = stream.readByte(), length = stream.readByte();
- if (code == 6 && length == 12) {
- if (!stream.skip(8))
- return false;
- parsed.sampleRate = stream.readUint32BE();
+ } else if (tag == MKTAG('S', 'T', 'R', 'K')) {
+ if (parsed.scriptOffset >= 0)
+ return false;
+ parsed.scriptOffset = offset;
+ parsed.scriptSize = chunkSize;
+ if (chunkSize >= 14) {
+ const byte code = stream.readByte(), length = stream.readByte();
+ if (code == SAUD_OP_SET_OFFSET && length == 12) {
+ if (!stream.skip(8))
+ return false;
+ parsed.sampleRate = stream.readUint32BE();
+ }
}
}
// SAUD chunks have no word-alignment padding.
@@ -75,4 +82,132 @@ bool readSaudHeader(Common::SeekableReadStream &stream, SaudHeader &header, bool
return true;
}
+SaudScript::SaudScript() {
+ reset(nullptr, 0, 11025);
+}
+
+void SaudScript::reset(const byte *data, uint32 size, uint32 defaultRate) {
+ _data = data;
+ _size = data ? size : 0;
+ _position = 0;
+ _defaultRate = defaultRate;
+ _remaining = _elapsed = 0;
+}
+
+bool SaudScript::fail(const char *message) {
+ warning("SAUD: %s at STRK offset %u", message, _position);
+ _position = _size;
+ return false;
+}
+
+bool SaudScript::branch(int16 displacement) {
+ const int64 target = int64(_position) + displacement;
+ if (target < 0 || target > _size)
+ return fail("branch outside script");
+ _position = target;
+ return true;
+}
+
+bool SaudScript::nextSpan(SaudSpan &span, byte *variables, int &masterVolume, byte &volume, byte &pan) {
+ // A loop must eventually produce samples or terminate.
+ uint instructions = 0;
+ while (_position < _size) {
+ if (++instructions > 65536)
+ return fail("script loop without audio");
+ if (_size - _position < 2)
+ return fail("truncated instruction");
+ const byte *code = _data + _position;
+ const uint length = code[1] + 2;
+ if (length > _size - _position)
+ return fail("truncated instruction operands");
+
+ switch (code[0]) {
+ case SAUD_OP_INIT:
+ case SAUD_OP_SET_OFFSET:
+ case SAUD_OP_SET_LENGTH: {
+ const bool sliced = code[0] == SAUD_OP_SET_LENGTH;
+ const uint required = sliced ? 20 : (code[0] == SAUD_OP_INIT ? 10 : 14);
+ if (length < required)
+ return fail("short sample instruction");
+ span.offset = READ_BE_UINT32(code + 2);
+ span.length = READ_BE_UINT32(code + 6);
+ span.rate = code[0] == SAUD_OP_INIT ? _defaultRate : READ_BE_UINT32(code + 10);
+ if (span.offset > 0x7fffffff || span.length > 0x7fffffff || !span.rate || span.rate > 192000)
+ return fail("invalid sample span or rate");
+ if (sliced) {
+ if (!_remaining) {
+ _remaining = span.length;
+ _elapsed = 0;
+ }
+ if (_elapsed > 0x7fffffff - span.offset)
+ return fail("sample offset overflow");
+ span.offset += _elapsed;
+ span.length = MIN(_remaining, READ_BE_UINT32(code + 14));
+ _remaining -= span.length;
+ _elapsed += span.length;
+ } else {
+ _remaining = _elapsed = 0;
+ }
+ if (span.length > 0x7fffffff - span.offset)
+ return fail("sample span overflow");
+ if (sliced && !_remaining) {
+ if (!branch(READ_BE_INT16(code + 18)))
+ return false;
+ } else {
+ _position += length;
+ }
+ if (span.length)
+ return true;
+ break;
+ }
+ case SAUD_OP_UPDATE_HEADER:
+ case SAUD_OP_COMPARE_GT:
+ case SAUD_OP_COMPARE_LT:
+ case SAUD_OP_COMPARE_EQ:
+ case SAUD_OP_COMPARE_NE: {
+ if (length < 6)
+ return fail("short branch instruction");
+ const byte id = code[4];
+ const byte value = id == SAUD_VALUEID_ALL_VOLS ? masterVolume :
+ (id == SAUD_VALUEID_TRK_VOL ? volume : (id == SAUD_VALUEID_TRK_PAN ? pan : variables[id]));
+ bool take = false;
+ switch (code[0]) {
+ case SAUD_OP_UPDATE_HEADER: take = value || id == 0; break;
+ case SAUD_OP_COMPARE_GT: take = value > code[5]; break;
+ case SAUD_OP_COMPARE_LT: take = value < code[5]; break;
+ case SAUD_OP_COMPARE_EQ: take = value == code[5]; break;
+ case SAUD_OP_COMPARE_NE: take = value != code[5]; break;
+ }
+ if (take) {
+ if (!branch(READ_BE_INT16(code + 2)))
+ return false;
+ } else {
+ _position += length;
+ }
+ break;
+ }
+ case SAUD_OP_SET_PARAM:
+ case SAUD_OP_INCR_PARAM: {
+ if (length < 4)
+ return fail("short parameter instruction");
+ const byte id = code[2], value = code[3];
+ if (id == SAUD_VALUEID_ALL_VOLS) {
+ masterVolume = code[0] == SAUD_OP_SET_PARAM ? value : (masterVolume + value) & 0xffff;
+ } else {
+ byte ¶meter = id == SAUD_VALUEID_TRK_VOL ? volume : (id == SAUD_VALUEID_TRK_PAN ? pan : variables[id]);
+ parameter = code[0] == SAUD_OP_SET_PARAM ? value : parameter + value;
+ }
+ _position += length;
+ break;
+ }
+ case SAUD_OP_STOP:
+ _position = _size;
+ return false;
+ default:
+ return fail("unknown instruction");
+ }
+ }
+ return false;
+}
+
} // End of namespace Scumm
diff --git a/engines/scumm/smush/saud.h b/engines/scumm/smush/saud.h
index 65e9f55563b..5d6f0b1b095 100644
--- a/engines/scumm/smush/saud.h
+++ b/engines/scumm/smush/saud.h
@@ -32,6 +32,8 @@ namespace Scumm {
struct SaudHeader {
uint32 dataSize = 0;
uint32 sampleRate = 11025;
+ int64 scriptOffset = -1;
+ uint32 scriptSize = 0;
};
// Read a SAUD container spanning the rest of the stream, leaving it at its PCM.
@@ -41,6 +43,49 @@ struct SaudHeader {
// The header is only updated on success.
bool readSaudHeader(Common::SeekableReadStream &stream, SaudHeader &header, bool allowTruncated = false);
+enum SaudOpcode {
+ SAUD_OP_INIT = 1,
+ SAUD_OP_UPDATE_HEADER = 2,
+ SAUD_OP_SET_PARAM = 3,
+ SAUD_OP_INCR_PARAM = 4,
+ SAUD_OP_STOP = 5,
+ SAUD_OP_SET_OFFSET = 6,
+ SAUD_OP_SET_LENGTH = 7,
+ SAUD_OP_COMPARE_GT = 8,
+ SAUD_OP_COMPARE_LT = 9,
+ SAUD_OP_COMPARE_EQ = 10,
+ SAUD_OP_COMPARE_NE = 11
+};
+
+enum SaudValueId {
+ SAUD_VALUEID_ALL_VOLS = 0xff,
+ SAUD_VALUEID_TRK_VOL = 0xfe,
+ SAUD_VALUEID_TRK_PAN = 0xfd
+};
+
+struct SaudSpan {
+ uint32 offset = 0;
+ uint32 length = 0;
+ uint32 rate = 0;
+};
+
+// STRK interpreter shared by the standalone INSANE sound players.
+// Script storage and the 256 global variables belong to the caller.
+class SaudScript {
+public:
+ SaudScript();
+ void reset(const byte *data, uint32 size, uint32 defaultRate);
+ bool nextSpan(SaudSpan &span, byte *variables, int &masterVolume, byte &volume, byte &pan);
+
+private:
+ bool fail(const char *message);
+ bool branch(int16 displacement);
+
+ const byte *_data;
+ uint32 _size, _position, _defaultRate;
+ uint32 _remaining, _elapsed;
+};
+
} // End of namespace Scumm
#endif
diff --git a/engines/scumm/smush/saud_stream.cpp b/engines/scumm/smush/saud_stream.cpp
new file mode 100644
index 00000000000..8f18f0582a5
--- /dev/null
+++ b/engines/scumm/smush/saud_stream.cpp
@@ -0,0 +1,127 @@
+/* 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 "scumm/smush/saud_stream.h"
+
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
+#include "audio/rate.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+SaudStreamState::SaudStreamState() {
+ reset();
+}
+
+void SaudStreamState::reset() {
+ Common::StackLock lock(_mutex);
+ memset(_variables, 0, sizeof(_variables));
+ _masterVolume = 127;
+}
+
+byte SaudStreamState::variable(byte id) const {
+ Common::StackLock lock(_mutex);
+ return id == SAUD_VALUEID_ALL_VOLS ? _masterVolume : _variables[id];
+}
+
+SaudStream::SaudStream(const Common::Array<byte> &samples, const Common::Array<byte> &script,
+ uint rate, SaudStreamState &state, byte volume, int8 pan) :
+ _samples(samples), _state(state), _raw(nullptr), _converter(nullptr), _volume(volume), _pan(pan), _ended(false) {
+ _script.reset(script.begin(), script.size(), kRate);
+ if (script.empty()) {
+ SaudSpan span;
+ span.length = samples.size();
+ span.rate = rate;
+ _ended = !startSpan(span);
+ }
+}
+
+SaudStream::~SaudStream() {
+ delete _raw;
+ delete _converter;
+}
+
+bool SaudStream::startSpan(const SaudSpan &span) {
+ if (span.offset >= _samples.size() || !span.length) {
+ warning("SAUD: sample span %u + %u exceeds SDAT size %u", span.offset, span.length, _samples.size());
+ return false;
+ }
+ const uint32 length = MIN<uint32>(span.length, _samples.size() - span.offset);
+ if (length < span.length) {
+ // DUST.SAN contains a speech script extending past its SDAT.
+ warning("SAUD: truncating sample span %u + %u to SDAT size %u", span.offset, span.length, _samples.size());
+ _script.reset(nullptr, 0, kRate);
+ }
+ _raw = Audio::makeRawStream(_samples.begin() + span.offset, length, span.rate, Audio::FLAG_UNSIGNED, DisposeAfterUse::NO);
+ if (!_converter || _converter->getInputRate() != span.rate) {
+ delete _converter;
+ _converter = Audio::makeRateConverter(span.rate, kRate, false, true, false);
+ }
+ return true;
+}
+
+int SaudStream::readBuffer(int16 *buffer, int numSamples) {
+ Common::StackLock lock(_state._mutex);
+ const int frames = numSamples / 2;
+ memset(buffer, 0, frames * 2 * sizeof(*buffer));
+ int done = 0;
+ uint emptySpans = 0;
+ while (done < frames && !_ended) {
+ if (!_raw) {
+ if (++emptySpans > 65536) {
+ warning("SAUD: script loop without output samples");
+ _ended = true;
+ break;
+ }
+ SaudSpan span;
+ if (!_script.nextSpan(span, _state._variables, _state._masterVolume, _volume, _pan) || !startSpan(span)) {
+ _ended = true;
+ break;
+ }
+ }
+ const int volume = CLIP<int>(_volume * _state._masterVolume / 127, 0, 127) * Audio::Mixer::kMaxMixerVolume / 127;
+ const int pan = CLIP<int>(int8(_pan), -127, 127);
+ const int left = pan > 0 ? volume * (127 - pan) / 127 : volume;
+ const int right = pan < 0 ? volume * (127 + pan) / 127 : volume;
+ const int count = _converter->convert(*_raw, (byte *)(buffer + done * 2), sizeof(*buffer), frames - done, left, right, Audio::MIX_ADD);
+ done += count;
+ if (count == 0) {
+ delete _raw;
+ _raw = nullptr;
+ } else {
+ emptySpans = 0;
+ }
+ }
+ return done * 2;
+}
+
+void SaudStream::skip(uint64 frames) {
+ int16 buffer[2048];
+ while (frames && !_ended) {
+ const int count = readBuffer(buffer, MIN<uint64>(frames, ARRAYSIZE(buffer) / 2) * 2) / 2;
+ if (!count)
+ break;
+ frames -= count;
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/saud_stream.h b/engines/scumm/smush/saud_stream.h
new file mode 100644
index 00000000000..3cd0c4ac8f9
--- /dev/null
+++ b/engines/scumm/smush/saud_stream.h
@@ -0,0 +1,78 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_SMUSH_SAUD_STREAM_H
+#define SCUMM_SMUSH_SAUD_STREAM_H
+
+#include "audio/audiostream.h"
+#include "common/array.h"
+#include "common/mutex.h"
+#include "scumm/smush/saud.h"
+
+namespace Audio {
+class RateConverter;
+}
+
+namespace Scumm {
+
+class SaudStreamState {
+public:
+ SaudStreamState();
+ void reset();
+ byte variable(byte id) const;
+
+private:
+ friend class SaudStream;
+ Common::Mutex _mutex;
+ byte _variables[256];
+ int _masterVolume;
+};
+
+// Plays an in-memory SAUD through the same STRK interpreter as RebelAudio.
+// The caller keeps samples, script and shared state alive until playback stops.
+class SaudStream : public Audio::AudioStream {
+public:
+ enum { kRate = 22050 };
+ SaudStream(const Common::Array<byte> &samples, const Common::Array<byte> &script,
+ uint rate, SaudStreamState &state, byte volume, int8 pan);
+ ~SaudStream() override;
+
+ int readBuffer(int16 *buffer, int numSamples) override;
+ bool isStereo() const override { return true; }
+ int getRate() const override { return kRate; }
+ bool endOfData() const override { return _ended; }
+ // Advance the script and audio together before handing the stream to the mixer.
+ void skip(uint64 frames);
+
+private:
+ bool startSpan(const SaudSpan &span);
+
+ const Common::Array<byte> &_samples;
+ SaudStreamState &_state;
+ SaudScript _script;
+ Audio::AudioStream *_raw;
+ Audio::RateConverter *_converter;
+ byte _volume, _pan;
+ bool _ended;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 0889ae373fa..15975a2aa2d 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -26,6 +26,7 @@
#include "common/list.h"
#include "common/rect.h"
#include "scumm/charset_v7.h"
+#include "scumm/smush/saud.h"
namespace Audio {
class SoundHandle;
@@ -57,21 +58,6 @@ namespace Scumm {
#define TRK_TYPE_MASK 0xC0
-#define SAUD_OP_INIT 1
-#define SAUD_OP_UPDATE_HEADER 2
-#define SAUD_OP_SET_PARAM 3
-#define SAUD_OP_INCR_PARAM 4
-#define SAUD_OP_SET_OFFSET 6
-#define SAUD_OP_SET_LENGTH 7
-#define SAUD_OP_COMPARE_GT 8
-#define SAUD_OP_COMPARE_LT 9
-#define SAUD_OP_COMPARE_EQ 10
-#define SAUD_OP_COMPARE_NE 11
-
-#define SAUD_VALUEID_ALL_VOLS 0xFF
-#define SAUD_VALUEID_TRK_VOL 0xFE
-#define SAUD_VALUEID_TRK_PAN 0xFD
-
#define TRK_USERID_SPEECH 1
#define TRK_USERID_MUSIC 2
#define TRK_USERID_SFX 3
Commit: f8a95c9e4c8e61cdaa0346ccef1de974928fcb94
https://github.com/scummvm/scummvm/commit/f8a95c9e4c8e61cdaa0346ccef1de974928fcb94
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: initial implementation of castle gameplay
Changed paths:
A engines/scumm/insane/mortimer/castle.cpp
A engines/scumm/insane/mortimer/castle.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/flight.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/movie.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/mortimer/castle.cpp b/engines/scumm/insane/mortimer/castle.cpp
new file mode 100644
index 00000000000..81d14d23e22
--- /dev/null
+++ b/engines/scumm/insane/mortimer/castle.cpp
@@ -0,0 +1,545 @@
+/* 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 "scumm/insane/mortimer/castle.h"
+#include "scumm/insane/mortimer/campaign.h"
+
+#include "common/endian.h"
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "graphics/surface.h"
+
+namespace Scumm {
+
+// SHELL 4439e0: explanation, starting animal, then five correct/distractor pairs.
+const byte kCastlePuzzles[3][8][12] = {
+ {
+ {0, 34, 13, 11, 6, 1, 12, 0, 25, 4, 31, 21},
+ {1, 30, 24, 6, 32, 35, 0, 25, 13, 12, 2, 22},
+ {3, 24, 22, 32, 12, 5, 26, 4, 10, 36, 25, 0},
+ {5, 11, 22, 25, 26, 13, 5, 36, 23, 0, 35, 32},
+ {6, 10, 13, 21, 11, 32, 26, 35, 5, 12, 34, 33},
+ {8, 34, 16, 2, 3, 24, 15, 32, 0, 31, 20, 13},
+ {9, 21, 26, 32, 3, 13, 15, 25, 4, 31, 24, 11},
+ {10, 32, 33, 21, 13, 10, 31, 26, 11, 15, 6, 0}
+ },
+ {
+ {0, 34, 13, 30, 6, 4, 12, 35, 25, 2, 31, 20},
+ {1, 30, 24, 36, 32, 22, 0, 35, 13, 6, 2, 12},
+ {4, 22, 13, 5, 25, 20, 21, 14, 12, 2, 11, 15},
+ {5, 11, 22, 13, 26, 30, 5, 0, 23, 13, 35, 21},
+ {6, 10, 13, 22, 11, 12, 26, 36, 5, 23, 34, 25},
+ {7, 26, 11, 35, 21, 32, 24, 12, 22, 36, 23, 33},
+ {8, 34, 16, 13, 3, 10, 15, 32, 0, 11, 20, 5},
+ {10, 32, 33, 0, 13, 12, 31, 15, 11, 3, 6, 10}
+ },
+ {
+ {3, 24, 22, 3, 12, 14, 26, 6, 10, 0, 25, 32},
+ {4, 22, 13, 3, 25, 20, 21, 36, 12, 0, 11, 10},
+ {7, 26, 11, 20, 21, 0, 24, 25, 22, 2, 23, 12},
+ {11, 21, 20, 3, 6, 15, 16, 34, 10, 13, 12, 14},
+ {12, 24, 35, 22, 4, 10, 33, 3, 2, 26, 5, 0},
+ {13, 5, 13, 0, 32, 15, 24, 35, 16, 14, 2, 3},
+ {9, 21, 26, 20, 3, 11, 15, 2, 4, 16, 24, 1},
+ {11, 24, 20, 34, 6, 15, 16, 13, 10, 4, 12, 0}
+ }
+};
+
+MortimerCastle::MortimerCastle() : MortimerFlight(false), _random("mortimer-castle"),
+ _nextVoice(0), _flightDifficulty(0), _riddleDifficulty(0), _puzzle(0), _step(0), _damage(0), _checkpointDamage(0),
+ _wrongChoices(0), _crashes(0), _frame(0), _choiceFrame(0), _hintFrames(0), _holdFrames(0), _answer(0), _selection(-1),
+ _stage(kEntry), _moviePending(false), _choosing(false), _leaving(false), _correct(false), _resourcesLoaded(false) {
+ memset(_rooms, 0, sizeof(_rooms));
+ memset(_sides, 0, sizeof(_sides));
+}
+
+bool MortimerCastle::loadText() {
+ Common::File file;
+ if (!file.open("RESOURCE/DATA/RIDDLE_F.DAT"))
+ return false;
+ int answer = -1, difficulty = -1;
+ while (!file.eos() && !file.err()) {
+ Common::String line = file.readLine();
+ line.trim();
+ if (line.empty())
+ continue;
+ if (line[0] == '#') {
+ const Common::String heading = line.substr(1);
+ if (heading == "Medium" || heading == "Hard" || heading == "Easy") {
+ difficulty = heading == "Medium" ? 0 : heading == "Hard" ? 1 : -1;
+ } else {
+ answer = heading == "Mortimer" ? 0 : heading == "Lodius" ? 1 : -1;
+ difficulty = -1;
+ }
+ } else if (answer >= 0 && difficulty >= 0 && line[0] != 0x1a) {
+ Common::String &text = _riddles[answer][difficulty];
+ if (!text.empty())
+ text += '\n';
+ text += line;
+ }
+ }
+ if (file.err())
+ return false;
+ file.close();
+ if (!file.open("RESOURCE/DATA/CASTLE.DAT"))
+ return false;
+ bool hint = false;
+ while (!file.eos() && !file.err()) {
+ Common::String line = file.readLine();
+ line.trim();
+ if (line.empty())
+ continue;
+ if (line[0] == '#') {
+ const Common::String heading = line.substr(1);
+ hint = heading == (context().girl ? "In Common g" : "In Common b");
+ } else if (hint && line[0] != 0x1a) {
+ if (!_hint.empty())
+ _hint += '\n';
+ _hint += line;
+ }
+ }
+ for (uint i = 0; i < 2; ++i)
+ for (uint j = 0; j < 2; ++j)
+ if (_riddles[i][j].empty())
+ return false;
+ return !file.err() && !_hint.empty();
+}
+
+bool MortimerCastle::loadResources() {
+ if (_resourcesLoaded)
+ return true;
+ struct Bank {
+ const char *name;
+ Common::Array<MortimerSprite> *sprites;
+ uint count;
+ };
+ const Bank banks[] = {
+ {"RESOURCE/CASTLE/GOGGLES.NUT", &_goggles, 11},
+ {"RESOURCE/CASTLE/C_ICON_H.NUT", &_icons, 28},
+ {"RESOURCE/CASTLE/SPIN.NUT", &_spin, 4},
+ {"RESOURCE/CASTLE/FLAME1.NUT", &_flames[0], 5},
+ {"RESOURCE/CASTLE/FLAME2.NUT", &_flames[1], 5},
+ {"RESOURCE/CASTLE/FLAME3.NUT", &_flames[2], 5},
+ {"RESOURCE/CASTLE/FLAME4.NUT", &_flames[3], 5},
+ {"RESOURCE/CASTLE/CG_MOUTH.NUT", &_mouth, 7},
+ {"RESOURCE/GENERAL/ARROWS.NUT", &_cursor, 4}
+ };
+ Common::File file;
+ for (uint i = 0; i < ARRAYSIZE(banks); ++i) {
+ if (!file.open(Common::Path(banks[i].name)) ||
+ !loadMortimerSprites(file, *banks[i].sprites) || banks[i].sprites->size() < banks[i].count) {
+ _error = Common::String::format("Missing or invalid %s", banks[i].name);
+ return false;
+ }
+ file.close();
+ }
+ for (uint region = 0; region < 4; ++region) {
+ for (uint i = 0; i < 7; ++i) {
+ const Common::String name = Common::String::format("RESOURCE/CASTLE/PLAQ_%s.NUT", mortimerRegion(region).animals[i]);
+ if (!file.open(Common::Path(name)) || !loadMortimerSprites(file, _plaques[region * 7 + i]) || _plaques[region * 7 + i].size() < 2) {
+ _error = Common::String::format("Missing or invalid %s", name.c_str());
+ return false;
+ }
+ file.close();
+ }
+ }
+ if (!_font.load("RESOURCE/GENERAL/MORTFNTH.NUT") || !_captionFont.load("RESOURCE/GENERAL/ESTRO20.NUT")) {
+ _error = "Missing or invalid Mortimer castle fonts";
+ return false;
+ }
+ _resourcesLoaded = true;
+ return true;
+}
+
+bool MortimerCastle::start(int scene, uint flightDifficulty, uint riddleDifficulty, bool girl) {
+ _error.clear();
+ context().girl = girl;
+ _flightDifficulty = MIN(flightDifficulty, 2U);
+ _riddleDifficulty = MIN(riddleDifficulty, 2U);
+ setCollisionRadius(20 * (_flightDifficulty + 1));
+ for (uint i = 0; i < 2; ++i)
+ for (uint j = 0; j < 2; ++j)
+ _riddles[i][j].clear();
+ _hint.clear();
+ if (!loadResources())
+ return false;
+ if (!loadText()) {
+ _error = "Missing or invalid Mortimer castle text";
+ return false;
+ }
+ const Common::String name = Common::String::format("RESOURCE/CASTLE/CAS_A_H%c.SAD", girl ? 'G' : 'B');
+ Common::File file;
+ if (!file.open(Common::Path(name)) || !loadMortimerSound(file, _hintSound)) {
+ _error = Common::String::format("Missing or invalid %s", name.c_str());
+ return false;
+ }
+ _frame = _hintFrames = 0;
+ _voices.clear();
+ _nextVoice = 0;
+ _answer = _random.getRandomBit();
+ context().riddle = _answer * 10 + (_riddleDifficulty != 0);
+ if (scene == 21)
+ return newPuzzle();
+ enter(scene == 16 ? kGate : scene == 22 ? kSong : kIdle);
+ return true;
+}
+
+bool MortimerCastle::newPuzzle() {
+ _puzzle = _random.getRandomNumber(7);
+ _step = _damage = _checkpointDamage = _wrongChoices = _crashes = 0;
+ uint rooms[] = {0, 1, 2, 3, 4};
+ for (uint i = 0; i < 5; ++i) {
+ const uint choice = _random.getRandomNumber(4 - i);
+ _rooms[i] = rooms[choice];
+ for (uint j = choice; j < 4 - i; ++j)
+ rooms[j] = rooms[j + 1];
+ _sides[i] = _random.getRandomBit();
+ }
+ // SHELL includes the entry in its side slots, leaving the last pair on the right.
+ _sides[5] = 0;
+ const Common::String name = Common::String::format("RESOURCE/CASTLE/C_CUZ%02u%c.SAD",
+ kCastlePuzzles[_riddleDifficulty][_puzzle][0], context().girl ? 'G' : 'B');
+ Common::File file;
+ if (!file.open(Common::Path(name)) || !loadMortimerSound(file, _explanation)) {
+ _error = Common::String::format("Missing or invalid %s", name.c_str());
+ return false;
+ }
+ enter(kEntry);
+ debug(1, "Mortimer castle: puzzle %u, flight difficulty %u, riddle difficulty %u", _puzzle, _flightDifficulty, _riddleDifficulty);
+ return true;
+}
+
+void MortimerCastle::enter(Stage stage) {
+ _stage = stage;
+ _choosing = _leaving = false;
+ _selection = -1;
+ _choiceFrame = 0;
+ _holdFrames = 0;
+ _voices.clear();
+ _nextVoice = 0;
+ const char *name = nullptr;
+ switch (stage) {
+ case kEntry: name = "CS_ENTRY.SAN"; break;
+ case kRoom:
+ _movieName = Common::String::format("RESOURCE/CASTLE/CS_%c.SAN", 'A' + _rooms[_step ? _step - 1 : 0]);
+ break;
+ case kLeft: name = "CS_LEFT.SAN"; break;
+ case kDrop: name = "CS_DROP.SAN"; break;
+ case kExit: name = "CS_EXIT.SAN"; _voices.push_back(3); break;
+ case kGate:
+ _answer = _random.getRandomBit();
+ context().riddle = _answer * 10 + (_riddleDifficulty != 0);
+ name = "CS_GATE.SAN";
+ break;
+ case kSong: name = "C_SONG_1.SAN"; break;
+ case kIdle: name = "CASTLE_I.NUT"; break;
+ case kRiddle: name = "C_RIDD_1.SAN"; break;
+ case kWrong: name = "C_WRONGH.SAN"; break;
+ case kCorrect: name = "CGYESH.SAN"; break;
+ case kFinished: break;
+ }
+ if (name)
+ _movieName = Common::String("RESOURCE/CASTLE/") + name;
+ _moviePending = stage != kFinished;
+}
+
+bool MortimerCastle::loadMovie() {
+ const MortimerMovieContext savedContext = context();
+ clearProgress();
+ context() = savedContext;
+ for (uint i = 0; i < 2; ++i) {
+ _speechBlocks[i] = SpeechBlock();
+ _speech[i] = MortimerSound();
+ }
+ Common::File file;
+ if (!file.open(Common::Path(_movieName))) {
+ _error = Common::String::format("Missing %s", _movieName.c_str());
+ return false;
+ }
+ if (!MortimerFlight::load(file, _stage <= kExit && _stage != kEntry)) {
+ _error = Common::String::format("%s: %s", _movieName.c_str(), MortimerMovie::errorMessage().c_str());
+ return false;
+ }
+ setWeapon(kSlimeGun);
+ _moviePending = false;
+ debug(1, "Mortimer castle: %s, step %u, damage %u, wrong choices %u, crashes %u",
+ _movieName.c_str(), _step, _damage, _wrongChoices, _crashes);
+ return true;
+}
+
+int MortimerCastle::animal(uint side) const {
+ const byte *puzzle = kCastlePuzzles[_riddleDifficulty][_puzzle];
+ if (!_step)
+ return side ? -1 : puzzle[1];
+ return _step <= 5 ? puzzle[_step * 2 + (_sides[_step] != side)] : -1;
+}
+
+void MortimerCastle::handleAction(const byte *data, uint32 size) {
+ MortimerFlight::handleAction(data, size);
+ const uint code = READ_LE_UINT16(data), flags = READ_LE_UINT16(data + 2);
+ if (code == 6 && flags == 39 && (_stage == kEntry || _stage == kRoom)) {
+ showChoices();
+ } else if (code == 8 && flags == 46 && size >= 18) {
+ const uint id = READ_LE_UINT16(data + 6);
+ if (id / 1000 != 7 || id / 100 - 70 != uint(context().girl))
+ return;
+ const uint block = READ_LE_UINT16(data + 10), count = READ_LE_UINT16(data + 12);
+ const uint remaining = READ_LE_UINT32(data + 14);
+ const uint length = size - 18;
+ if (block >= count || length > remaining || remaining > 25000) {
+ _error = Common::String::format("Invalid castle speech %u block %u/%u in %s at frame %u",
+ id, block, count, _movieName.c_str(), frame());
+ return;
+ }
+ for (uint side = 0; side < 2; ++side) {
+ if (int(id % 100) != animal(side))
+ continue;
+ SpeechBlock &speech = _speechBlocks[side];
+ if (!block) {
+ speech = SpeechBlock();
+ speech.count = count;
+ speech.size = remaining;
+ }
+ if (speech.next != block || speech.count != count || speech.size - speech.data.size() != remaining) {
+ _error = Common::String::format("Incomplete castle speech %u block %u/%u in %s at frame %u",
+ id, block, count, _movieName.c_str(), frame());
+ return;
+ }
+ const uint offset = speech.data.size();
+ speech.data.resize(offset + length);
+ memcpy(speech.data.begin() + offset, data + 18, length);
+ ++speech.next;
+ }
+ }
+}
+
+void MortimerCastle::showChoices() {
+ _choosing = true;
+ _choiceFrame = 0;
+ _selection = -1;
+ _voices.clear();
+ _nextVoice = 0;
+ // The left name is spoken first; the entry has only a right plaque.
+ for (int side = 1; side >= 0; --side) {
+ if (animal(side) < 0)
+ continue;
+ const SpeechBlock &speech = _speechBlocks[side];
+ const uint size = speech.data.size() >= 8 ? READ_BE_UINT32(speech.data.begin() + 4) : 0;
+ if (!speech.count || speech.next != speech.count || speech.data.size() != speech.size ||
+ speech.data.size() < 8 || size != speech.data.size() - 8) {
+ _error = Common::String::format("Incomplete castle plaque speech in %s for animal %d", _movieName.c_str(), animal(side));
+ return;
+ }
+ Common::MemoryReadStream stream(speech.data.begin(), size + 8);
+ if (!loadMortimerSound(stream, _speech[side])) {
+ _error = Common::String::format("Missing or invalid castle plaque speech in %s for animal %d", _movieName.c_str(), animal(side));
+ return;
+ }
+ _voices.push_back(side);
+ }
+ if (_step == 1)
+ _voices.push_back(2);
+}
+
+const MortimerSound *MortimerCastle::takeSpeech(bool playing) {
+ if (playing || !hasSpeech())
+ return nullptr;
+ const uint voice = _voices[_nextVoice++];
+ if (voice == 2) {
+ _hintFrames = 60;
+ return &_hintSound;
+ }
+ return voice == 3 ? &_explanation : &_speech[voice];
+}
+
+void MortimerCastle::update() {
+ ++_frame;
+ if (_holdFrames)
+ --_holdFrames;
+ if (_hintFrames)
+ --_hintFrames;
+ if (!_choosing)
+ return;
+ ++_choiceFrame;
+ if (_selection < 0 || _choiceFrame < 2)
+ return;
+ _choosing = false;
+ _leaving = true;
+ _hintFrames = 0;
+ if (_selection == 1)
+ enter(kLeft);
+}
+
+void MortimerCastle::click(const Common::Point &point) {
+ if (_choosing && _selection < 0) {
+ _selection = _step ? (point.x <= 310 ? 1 : 0) : 0;
+ _correct = !_step || uint(_selection) == _sides[_step];
+ _choiceFrame = 0;
+ _voices.clear();
+ _nextVoice = 0;
+ } else if (_stage == kIdle) {
+ if (Common::Rect(184, 341, 459, 447).contains(point)) {
+ enter(kRiddle);
+ } else if (Common::Rect(0, 70, 85, 372).contains(point)) {
+ enter(_answer == 0 ? kCorrect : kWrong);
+ } else if (Common::Rect(558, 70, 640, 371).contains(point)) {
+ enter(_answer == 1 ? kCorrect : kWrong);
+ } else if (Common::Rect(295, 201, 353, 245).contains(point) ||
+ Common::Rect(385, 205, 422, 247).contains(point) || Common::Rect(225, 208, 260, 247).contains(point)) {
+ enter(kWrong);
+ }
+ }
+}
+
+void MortimerCastle::resolveChoice() {
+ if (!_correct) {
+ enter(kDrop);
+ return;
+ }
+ _damage -= _damage / (2 + _flightDifficulty);
+ _checkpointDamage = _damage;
+ ++_step;
+ enter(_step < 6 ? kRoom : kExit);
+}
+
+void MortimerCastle::retry(bool crash) {
+ const uint allowances[] = {5, 3, 3};
+ const bool resetPuzzle = crash ? ++_crashes >= 4 : ++_wrongChoices >= allowances[_flightDifficulty];
+ if (resetPuzzle) {
+ newPuzzle();
+ } else {
+ _damage = _checkpointDamage;
+ enter(kRoom);
+ if (crash)
+ _holdFrames = 7;
+ }
+}
+
+void MortimerCastle::hit() {
+ if (_damage < 14)
+ ++_damage;
+ else
+ retry(true);
+}
+
+void MortimerCastle::movieFinished() {
+ switch (_stage) {
+ case kEntry:
+ case kRoom:
+ if (_leaving)
+ resolveChoice();
+ else
+ _error = Common::String::format("Castle movie %s ended without a plaque choice", _movieName.c_str());
+ break;
+ case kLeft: resolveChoice(); break;
+ case kDrop: retry(false); break;
+ case kExit: enter(kGate); break;
+ case kGate: enter(kSong); break;
+ case kSong:
+ case kRiddle: enter(kIdle); break;
+ case kWrong: newPuzzle(); break;
+ case kCorrect: enter(kFinished); break;
+ case kIdle:
+ case kFinished: break;
+ }
+}
+
+bool MortimerCastle::skip() {
+ if (_choosing || _stage == kIdle)
+ return true;
+ if ((_stage == kEntry || _stage == kRoom) && !_leaving) {
+ while (!_choosing) {
+ if (!advance()) {
+ if (errorMessage().empty())
+ movieFinished();
+ break;
+ }
+ if (!_error.empty())
+ break;
+ }
+ } else {
+ movieFinished();
+ }
+ return errorMessage().empty();
+}
+
+int MortimerCastle::sceneId() const {
+ return _stage <= kExit ? 21 : _stage == kGate ? 16 : _stage == kSong ? 22 : 23;
+}
+
+const char *MortimerCastle::subtitleName() const {
+ return _stage == kGate || _stage == kCorrect ? "RESOURCE/DATA/ENDTEXT.TRS" : "RESOURCE/DATA/CAS_SONG.TRS";
+}
+
+void MortimerCastle::drawText(Graphics::Surface &screen, const Common::String &text, int x, int y, int width, byte color) const {
+ Common::Array<Common::String> lines;
+ _font.wordWrapText(text, width, lines);
+ y -= lines.size() * _font.getFontHeight() / 2;
+ for (uint i = 0; i < lines.size(); ++i)
+ _font.drawString(&screen, lines[i], x, y + i * _font.getFontHeight(), width, color, Graphics::kTextAlignCenter);
+}
+
+void MortimerCastle::drawOverlay(Graphics::Surface &screen, const Common::Point &mouse, const Common::Point &origin, byte mouth) const {
+ if (courseView()) {
+ _goggles[0].draw(screen, 0, 0);
+ const uint gauge = _damage >= 13 ? 9 + _frame % 2 : _damage >= 10 ? _damage - 4 : 1 + _damage / 2;
+ _goggles[gauge].draw(screen, 0, 0);
+ const Common::Point origins[] = {{355, 85}, {413, 86}, {470, 87}, {527, 88}, {584, 90}};
+ const byte *puzzle = kCastlePuzzles[_riddleDifficulty][_puzzle];
+ for (uint i = 0; i < MIN(_step, 5U); ++i) {
+ const uint code = puzzle[i ? i * 2 : 1];
+ _icons[code / 10 * 7 + code % 10].draw(screen, origins[i].x, origins[i].y, true);
+ }
+ if (_choosing) {
+ const Common::Rect clip(8, 104, 632, 448);
+ for (uint side = 0; side < 2; ++side) {
+ const int code = animal(side);
+ if (code < 0)
+ continue;
+ const Common::Array<MortimerSprite> &plaque = _plaques[code / 10 * 7 + code % 10];
+ const MortimerSprite &sprite = _selection >= 0 ? _spin[2 + MIN(_choiceFrame, 1U)] :
+ _choiceFrame < 2 ? _spin[_choiceFrame] : plaque[(_choiceFrame / 2) % 2];
+ sprite.draw(screen, origin.x + (side ? 248 : 404), origin.y + 132, false, &clip, 2);
+ drawText(screen, mortimerRegion(code / 10).names[code % 10], side ? 26 : 416, 35, 208, 5);
+ }
+ const Common::Point flames[] = {{43, 131}, {101, 68}, {258, 68}, {325, 131}};
+ for (uint i = 0; i < 4; ++i)
+ if (_step || (i != 0 && i != 3))
+ _flames[i][_frame % 5].draw(screen, origin.x + flames[i].x * 2, origin.y + flames[i].y * 2, false, &clip, 2);
+ }
+ if (_hintFrames) {
+ Common::Array<Common::String> lines;
+ _captionFont.wordWrapText(_hint, 600, lines);
+ for (uint i = 0; i < lines.size(); ++i)
+ _captionFont.drawString(&screen, lines[i], 20, 480 - (lines.size() - i) * _captionFont.getFontHeight(), 600,
+ context().girl ? 253 : 5, Graphics::kTextAlignCenter);
+ }
+ } else if (_stage == kIdle || _stage == kRiddle || _stage == kSong) {
+ _mouth[MIN<uint>(mouth, 6)].draw(screen, 199, _stage == kSong ? 263 : 264);
+ if (_stage == kIdle) {
+ drawText(screen, _riddles[_answer][_riddleDifficulty != 0], 184, 394, 274, 5);
+ _cursor[0].draw(screen, mouse.x, mouse.y);
+ }
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/castle.h b/engines/scumm/insane/mortimer/castle.h
new file mode 100644
index 00000000000..00cfc5d5b75
--- /dev/null
+++ b/engines/scumm/insane/mortimer/castle.h
@@ -0,0 +1,95 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_CASTLE_H
+#define SCUMM_INSANE_MORTIMER_CASTLE_H
+
+#include "common/random.h"
+#include "scumm/insane/mortimer/flight.h"
+#include "scumm/insane/mortimer/text.h"
+
+namespace Scumm {
+
+class MortimerCastle : public MortimerFlight {
+public:
+ MortimerCastle();
+ bool start(int scene, uint flightDifficulty, uint riddleDifficulty, bool girl);
+ bool loadMovie();
+ void update();
+ void click(const Common::Point &point);
+ bool skip();
+ void movieFinished();
+ void hit();
+ void drawOverlay(Graphics::Surface &screen, const Common::Point &mouse, const Common::Point &origin, byte mouth) const;
+ const MortimerSound *takeSpeech(bool playing);
+
+ bool needsMovie() const { return _moviePending; }
+ bool courseView() const { return _stage <= kExit && interactive(); }
+ bool acceptsShots() const { return (_stage == kEntry || _stage == kRoom) && !_choosing && !_leaving && !_holdFrames; }
+ bool choosing() const { return _choosing; }
+ bool paused() const { return _choosing || _stage == kIdle || _holdFrames; }
+ bool finished() const { return _stage == kFinished; }
+ bool hasSpeech() const { return _nextVoice < _voices.size(); }
+ bool forceSubtitles() const { return _stage == kSong; }
+ uint damage() const { return _damage; }
+ int sceneId() const;
+ const char *subtitleName() const;
+ const Common::String &movieName() const { return _movieName; }
+ const Common::String &errorMessage() const { return _error.empty() ? MortimerMovie::errorMessage() : _error; }
+
+protected:
+ void handleAction(const byte *data, uint32 size) override;
+
+private:
+ enum Stage { kEntry, kRoom, kLeft, kDrop, kExit, kGate, kSong, kIdle, kRiddle, kWrong, kCorrect, kFinished };
+ struct SpeechBlock {
+ Common::Array<byte> data;
+ uint next = 0, count = 0, size = 0;
+ };
+
+ bool loadResources();
+ bool loadText();
+ bool newPuzzle();
+ void enter(Stage stage);
+ void retry(bool crash);
+ void resolveChoice();
+ void showChoices();
+ int animal(uint side) const;
+ void drawText(Graphics::Surface &screen, const Common::String &text, int x, int y, int width, byte color) const;
+
+ Common::RandomSource _random;
+ Common::Array<MortimerSprite> _goggles, _icons, _spin, _flames[4], _plaques[28], _mouth, _cursor;
+ MortimerFont _font, _captionFont;
+ Common::String _riddles[2][2], _hint;
+ Common::String _movieName, _error;
+ SpeechBlock _speechBlocks[2];
+ MortimerSound _speech[2], _hintSound, _explanation;
+ Common::Array<uint> _voices;
+ uint _nextVoice, _flightDifficulty, _riddleDifficulty, _puzzle, _step, _damage, _checkpointDamage;
+ uint _wrongChoices, _crashes, _frame, _choiceFrame, _hintFrames, _holdFrames;
+ uint _rooms[5], _sides[6];
+ int _answer, _selection;
+ Stage _stage;
+ bool _moviePending, _choosing, _leaving, _correct, _resourcesLoaded;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index 57b3728fa55..489ea5d5135 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -61,9 +61,11 @@ void MortimerFlight::reset() {
_targets.clear();
_obstacles.clear();
_inBranch = false;
+ _ignoreCollisions = false;
}
bool MortimerFlight::advance(bool renderFrame) {
+ _ignoreCollisions = false;
_obstacles.clear();
for (uint i = 0; i < _targets.size(); ++i)
_targets[i].active = false;
@@ -172,6 +174,8 @@ void MortimerFlight::handleAction(const byte *data, uint32 size) {
_weapon = flags == 9 ? kSlimeGun : kRevitascope;
} else if (code == 6 && (flags == 11 || flags == 25)) {
_interactive = flags == 11;
+ } else if (code == 6 && flags == 40) {
+ _ignoreCollisions = true;
} else if (code == 8 && flags == 46 && size >= 21 && id / 1000 != 7) {
Common::String text((const char *)data + 18, size - 21);
if (id / 1000 == 11) {
@@ -227,6 +231,8 @@ MortimerFlight::ShotResult MortimerFlight::shoot(const Common::Point &point) {
}
bool MortimerFlight::hitsObstacle(const Common::Point &point) const {
+ if (_ignoreCollisions)
+ return false;
const int radius = _collisionRadius;
const Common::Rect footprint(point.x - radius, point.y - radius, point.x + radius, point.y + radius);
for (uint i = 0; i < _targets.size(); ++i) {
diff --git a/engines/scumm/insane/mortimer/flight.h b/engines/scumm/insane/mortimer/flight.h
index 84f6c9c1a94..d14e23b00ca 100644
--- a/engines/scumm/insane/mortimer/flight.h
+++ b/engines/scumm/insane/mortimer/flight.h
@@ -51,19 +51,22 @@ public:
uint rescuedSpecies() const { return _rescuedSpecies; }
Weapon weapon() const { return _weapon; }
void resetWeapon() { _weapon = kRevitascope; }
+ void setWeapon(Weapon weapon) { _weapon = weapon; }
const Common::String &nextFlight() const { return _nextFlight; }
const Common::Array<Target> &targets() const { return _targets; }
bool interactive() const { return _interactive; }
void setSteering(const Common::Point &point) { _steering = point; }
void setCollisionRadius(uint radius) { _collisionRadius = radius; }
+protected:
+ void handleAction(const byte *data, uint32 size) override;
+
private:
struct Obstacle {
Common::Point corners[4];
bool outside;
};
- void handleAction(const byte *data, uint32 size) override;
int objectScale(byte codec) const override;
Target *findTarget(uint16 id);
bool targetHit(uint16 id) const;
@@ -75,7 +78,7 @@ private:
Common::String _nextFlight;
const bool _isDemo, _keepProgress;
bool _hitTargets[1400];
- bool _initialInteractive, _interactive, _inBranch;
+ bool _initialInteractive, _interactive, _inBranch, _ignoreCollisions;
Common::Point _steering;
Weapon _weapon;
uint _rescuedCount, _rescuedSpecies, _collisionRadius;
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 9dc2e35efed..9abda778787 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -118,7 +118,7 @@ MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
_segment(0), _carriedStones(0), _completedSpecies(0), _shellShocks(0), _shockFrame(0), _crashCount(0),
_uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0), _pendingGlowStones(0), _finished(false),
- _controllerMode(false), _interviewActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false) {
+ _controllerMode(false), _interviewActive(false), _castleActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
}
@@ -209,12 +209,16 @@ bool MortimerEngine::loadResources() {
}
MortimerMovie &MortimerEngine::currentMovie() {
+ if (_castleActive)
+ return _castle;
if (_crashStage != kNoCrash || _interviewActive || _introIndex >= 0)
return _intro;
return _flight;
}
const MortimerMovie &MortimerEngine::currentMovie() const {
+ if (_castleActive)
+ return _castle;
if (_crashStage != kNoCrash || _interviewActive || _introIndex >= 0)
return _intro;
return _flight;
@@ -342,6 +346,12 @@ bool MortimerEngine::followFlightBranch() {
}
void MortimerEngine::updateMovieContext() {
+ if (_castleActive) {
+ _castle.setSteering(moviePoint(_aim));
+ _castle.context().conditions[MortimerMovieContext::kDamaged] = _castle.damage() != 0;
+ _intro.context() = _castle.context();
+ return;
+ }
_flight.setSteering(moviePoint(_aim));
if (_isDemo)
return;
@@ -358,6 +368,8 @@ bool MortimerEngine::enterCampaignScene() {
stopSounds();
const MortimerCampaign::Scene &scene = _campaign.scene();
_interviewActive = false;
+ _castleActive = false;
+ _viewport = Common::Rect(24, 20, 616, 360);
_pendingFlight.clear();
_playingCached = false;
if (!loadSubtitles(scene.subtitles))
@@ -378,8 +390,18 @@ bool MortimerEngine::enterCampaignScene() {
return processInterview();
}
if (scene.kind == MortimerCampaign::kCastle || scene.kind == MortimerCampaign::kCastleRiddle) {
- _loadError = "Mortimer castle gameplay is not implemented yet";
- return false;
+ if (!_resourcesLoaded) {
+ if (!loadResources())
+ return false;
+ _resourcesLoaded = true;
+ }
+ _castleActive = true;
+ _viewport = Common::Rect(8, 104, 632, 448);
+ if (!_castle.start(scene.id, _flightDifficulty, _riddleDifficulty, _intro.context().girl)) {
+ _loadError = _castle.errorMessage();
+ return false;
+ }
+ return processCastle();
}
_movieHandles.clear();
const Common::String name = _campaign.movieName(_intro.context().girl);
@@ -440,6 +462,7 @@ void MortimerEngine::stopSounds() {
_mixer->stopHandle(_scopeHandle);
_mixer->stopHandle(_rescueHandle);
_mixer->stopHandle(_glowHandle);
+ _mixer->stopHandle(_castleHandle);
_soundState.reset();
}
@@ -554,6 +577,8 @@ bool MortimerEngine::restart(bool all) {
_campaign.start();
return enterCampaignScene();
}
+ if (_castleActive)
+ return _campaign.start(21) && enterCampaignScene();
if (!_interviewActive && _introIndex >= 0)
return startMovie();
const uint region = _campaign.scene().region;
@@ -581,6 +606,17 @@ bool MortimerEngine::restart(bool all) {
bool MortimerEngine::nextScene(bool skip) {
stopSounds();
+ if (_castleActive) {
+ if (!_castle.skip()) {
+ _loadError = _castle.errorMessage();
+ return false;
+ }
+ if (!_castle.needsMovie() && !_castle.finished()) {
+ _system->getPaletteManager()->setPalette(_castle.palette(), 0, 256);
+ playMovieSounds(true);
+ }
+ return processCastle();
+ }
if (_crashStage != kNoCrash) {
if (_crashStage == kCrashMovie && _needsRepair)
return loadCrashMovie(true);
@@ -722,7 +758,88 @@ bool MortimerEngine::processInterview() {
return true;
}
+bool MortimerEngine::processCastle() {
+ if (!_castle.errorMessage().empty()) {
+ _loadError = _castle.errorMessage();
+ return false;
+ }
+ if (_castle.finished()) {
+ stopSounds();
+ _castleActive = false;
+ return _campaign.start(36) && enterCampaignScene();
+ }
+ _campaign.start(_castle.sceneId());
+ if (!loadSubtitles(_castle.subtitleName()))
+ return false;
+ if (_castle.needsMovie()) {
+ stopSounds();
+ _movieHandles.clear();
+ if (!_castle.loadMovie()) {
+ _loadError = _castle.errorMessage();
+ return false;
+ }
+ _introIndex = -1;
+ _movieHandles.resize(_castle.sounds().size());
+ _hitJolt = false;
+ _shotFrame = 3;
+ for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
+ _steeringHistory[i] = Common::Point();
+ setMouse(Common::Point(320, 240));
+ updateSteering();
+ _system->warpMouse(_mouse.x, _mouse.y);
+ if (!startMovie())
+ return false;
+ }
+ const MortimerSound *speech = _castle.takeSpeech(_mixer->isSoundHandleActive(_castleHandle));
+ if (speech)
+ playSound(*speech, _castleHandle, Audio::Mixer::kSpeechSoundType);
+ return true;
+}
+
+bool MortimerEngine::updateCastle() {
+ if (_controllerMode && (_castle.choosing() || !_castle.courseView()))
+ setMouse(Common::Point(_mouse.x + (_steering[1] - _steering[0]) * 18 / Common::JOYAXIS_MAX,
+ _mouse.y + (_steering[3] - _steering[2]) * 18 / Common::JOYAXIS_MAX));
+ _castle.update();
+ updateInterface();
+ if (_castle.needsMovie() || _castle.finished())
+ return processCastle();
+ if (_castle.courseView()) {
+ if (_hitJolt) {
+ const int amount = 64 * (_flightDifficulty + 1);
+ updateView(_random.getRandomBit() ? amount : -amount, _random.getRandomBit() ? amount : -amount);
+ _hitJolt = false;
+ }
+ updateSteering(!_castle.acceptsShots());
+ }
+ updateMovieContext();
+ if (_castle.paused())
+ return processCastle();
+ if (!_castle.advance()) {
+ if (!_castle.errorMessage().empty()) {
+ _loadError = _castle.errorMessage();
+ return false;
+ }
+ if (_castle.hasSpeech() || _mixer->isSoundHandleActive(_castleHandle))
+ return processCastle();
+ _castle.movieFinished();
+ return processCastle();
+ }
+ playMovieSounds();
+ _system->getPaletteManager()->setPalette(_castle.palette(), 0, 256);
+ if (_castle.courseView() && _castle.acceptsShots() && _castle.hitsObstacle(moviePoint(_aim))) {
+ _castle.hit();
+ _hitJolt = true;
+ if (!processCastle())
+ return false;
+ playCollisionSound();
+ }
+ return processCastle();
+}
+
bool MortimerEngine::updateScene() {
+ if (_castleActive)
+ return updateCastle();
if (_interviewActive) {
if (_controllerMode) {
setMouse(Common::Point(_mouse.x + (_steering[1] - _steering[0]) * 18 / Common::JOYAXIS_MAX,
@@ -785,19 +902,20 @@ void MortimerEngine::setMouse(const Common::Point &position) {
_mouse.y = CLIP<int>(position.y, 0, 479);
}
-void MortimerEngine::updateSteering() {
+void MortimerEngine::updateSteering(bool centerView) {
int x = (_mouse.x - 320) / 2, y = 240 - _mouse.y;
if (_controllerMode) {
x = (_steering[1] - _steering[0]) * 128 / Common::JOYAXIS_MAX;
y = (_steering[2] - _steering[3]) * 128 / Common::JOYAXIS_MAX;
}
- updateView(x, y);
+ updateView(centerView ? 0 : x, centerView ? 0 : y);
if (ABS(x) < 3)
x = 0;
if (ABS(y) < 3)
y = 0;
- _aim.x = 320 + CLIP(x * 296 / 109, -296, 295);
- _aim.y = 190 - CLIP(y * 170 / 128, -169, 170);
+ const int halfWidth = _viewport.width() / 2, halfHeight = _viewport.height() / 2;
+ _aim.x = _viewport.left + halfWidth + CLIP(x * halfWidth / 109, -halfWidth, halfWidth - 1);
+ _aim.y = _viewport.top + halfHeight - CLIP(y * halfHeight / 128, 1 - halfHeight, halfHeight);
}
void MortimerEngine::updateView(int x, int y) {
@@ -812,8 +930,9 @@ void MortimerEngine::updateView(int x, int y) {
averageX /= 84;
averageY /= 84;
// The movie is doubled, and its translation is aligned to four pixels.
- _view.x = ((88 + CLIP(averageX * 88 / 128, -88, 88) + 3) & ~3) / 2;
- _view.y = ((118 - CLIP(averageY * 118 / 128, -118, 118) + 3) & ~3) / 2;
+ const int horizontal = _castleActive ? 72 : 88, vertical = _castleActive ? 116 : 118;
+ _view.x = ((horizontal + CLIP(averageX * horizontal / 128, -horizontal, horizontal) + 3) & ~3) / 2;
+ _view.y = ((vertical - CLIP(averageY * vertical / 128, -vertical, vertical) + 3) & ~3) / 2;
}
Common::Point MortimerEngine::moviePoint(const Common::Point &point) const {
@@ -825,14 +944,15 @@ Common::Point MortimerEngine::screenPoint(const Common::Point &point) const {
}
void MortimerEngine::fire() {
+ MortimerFlight &flight = currentFlight();
if (_shotFrame >= 3) {
_shot = _aim;
- _shotWeapon = _flight.weapon();
+ _shotWeapon = flight.weapon();
_shotFrame = 0;
playSound(_shotWeapon == MortimerFlight::kSlimeGun ? _slimeSound : _scopeSound,
_scopeHandle, Audio::Mixer::kSFXSoundType);
}
- const MortimerFlight::ShotResult result = _flight.shoot(moviePoint(_aim));
+ const MortimerFlight::ShotResult result = flight.shoot(moviePoint(_aim));
if (result == MortimerFlight::kAnimalRescued) {
playSound(_rescueSound, _rescueHandle, Audio::Mixer::kSFXSoundType);
++_pendingGlowStones;
@@ -840,9 +960,9 @@ void MortimerEngine::fire() {
_glowFrame = 0;
playSound(_glowSound, _glowHandle, Audio::Mixer::kSFXSoundType);
}
- for (uint i = 0; i < _flight.targets().size(); ++i) {
- const MortimerFlight::Target &target = _flight.targets()[i];
- if (target.hit && target.rescuedAt == _flight.frame() && target.id / 100 < 7)
+ for (uint i = 0; i < flight.targets().size(); ++i) {
+ const MortimerFlight::Target &target = flight.targets()[i];
+ if (target.hit && target.rescuedAt == flight.frame() && target.id / 100 < 7)
_iconFlash[target.id / 100] = 6;
}
} else if (result == MortimerFlight::kHornetHit) {
@@ -937,7 +1057,13 @@ Common::Error MortimerEngine::run() {
switch (event.customType) {
case kActionFire:
if (!isPaused() && !_finished) {
- if (_interviewActive) {
+ if (_castleActive) {
+ if (_castle.courseView() && _castle.acceptsShots())
+ fire();
+ else
+ _castle.click(_mouse);
+ exit = !processCastle();
+ } else if (_interviewActive) {
const bool playing = _interview.playingMovie();
_interview.click(_mouse);
if (playing && !_interview.playingMovie())
@@ -1013,21 +1139,22 @@ void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y, bool
}
void MortimerEngine::drawTargets() {
- for (uint i = 0; i < _flight.targets().size(); ++i) {
- const MortimerFlight::Target &target = _flight.targets()[i];
+ MortimerFlight &flight = currentFlight();
+ for (uint i = 0; i < flight.targets().size(); ++i) {
+ const MortimerFlight::Target &target = flight.targets()[i];
const bool hostile = (target.id >= 700 && target.id < 800) || (target.id >= 9000 && target.id < 9400);
if (!target.active || !target.id || (target.id >= 800 && !hostile))
continue;
if (target.hit) {
const Common::Array<MortimerSprite> &effect = target.hitWeapon == MortimerFlight::kSlimeGun ? _slimeHit : _scopeHit;
- const uint frame = _flight.frame() - target.rescuedAt;
+ const uint frame = flight.frame() - target.rescuedAt;
if (frame < effect.size()) {
const Common::Point center = screenPoint(Common::Point((target.rect.left + target.rect.right) / 2,
(target.rect.top + target.rect.bottom) / 2));
drawSprite(effect[frame], center.x, center.y, true, &_viewport);
}
- } else if ((target.id < 700 && _flight.weapon() == MortimerFlight::kRevitascope) ||
- (hostile && _flight.weapon() == MortimerFlight::kSlimeGun)) {
+ } else if ((target.id < 700 && flight.weapon() == MortimerFlight::kRevitascope) ||
+ (hostile && flight.weapon() == MortimerFlight::kSlimeGun)) {
const Common::Point top = screenPoint(Common::Point(target.rect.left, target.rect.top));
const Common::Point bottom = screenPoint(Common::Point(target.rect.right, target.rect.bottom));
drawSprite(_corners[0], top.x, top.y, true, &_viewport);
@@ -1108,25 +1235,33 @@ void MortimerEngine::draw() {
} else if (interactive()) {
_screen.fillRect(Common::Rect(640, 480), 10);
for (int y = _viewport.top; y < _viewport.bottom; ++y) {
- const byte *src = _flight.pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerMovie::kScreenWidth + _view.x;
+ const byte *src = currentFlight().pixels() + (_view.y + (y - _viewport.top) / 2) * MortimerMovie::kScreenWidth + _view.x;
byte *dst = (byte *)_screen.getBasePtr(_viewport.left, y);
for (int x = 0; x < _viewport.width(); ++x)
dst[x] = src[x / 2];
}
drawTargets();
- drawSprite(_head[_uiFrame % 4], 270, 200);
- drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
- drawSprite(_cockpit[0], 0, 0);
- drawSprite(_cockpit[1], 567, 0);
- drawSprite(_cockpit[2], 0, 0);
- drawDashboard();
+ if (_castleActive) {
+ _castle.drawOverlay(_screen, _mouse, screenPoint(Common::Point()), _soundState.variable(1));
+ drawSprite(_reticles[0], _castle.choosing() ? _mouse.x : _aim.x,
+ _castle.choosing() ? _mouse.y : _aim.y, true, &_viewport);
+ } else {
+ drawSprite(_head[_uiFrame % 4], 270, 200);
+ drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
+ drawSprite(_cockpit[0], 0, 0);
+ drawSprite(_cockpit[1], 567, 0);
+ drawSprite(_cockpit[2], 0, 0);
+ drawDashboard();
+ }
drawProjectile();
} else {
_screen.copyRectToSurface(currentMovie().pixels(), MortimerMovie::kScreenWidth, 0, 0, 640, 480);
+ if (_castleActive)
+ _castle.drawOverlay(_screen, _mouse, Common::Point(), _soundState.variable(1));
}
if (!_isDemo)
_subtitles.draw(_screen, currentMovie().captions(), ConfMan.getBool("subtitles") ||
- (_interviewActive && _interview.forceSubtitles()), &_intro.context());
+ (_interviewActive && _interview.forceSubtitles()) || (_castleActive && _castle.forceSubtitles()), &_intro.context());
if (isPaused()) {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 24401d946b5..0a8e917009c 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -29,6 +29,7 @@
#include "scumm/insane/mortimer/flight.h"
#include "scumm/insane/mortimer/interview.h"
#include "scumm/insane/mortimer/campaign.h"
+#include "scumm/insane/mortimer/castle.h"
#include "scumm/smush/saud_stream.h"
namespace Scumm {
@@ -68,14 +69,18 @@ private:
bool finishCrash();
void creditRescues();
bool processInterview();
+ bool processCastle();
+ bool updateCastle();
bool updateScene();
bool nextScene(bool skip = false);
bool restart(bool all = false);
MortimerMovie ¤tMovie();
const MortimerMovie ¤tMovie() const;
- bool interactive() const { return _crashStage == kNoCrash && !_interviewActive && _introIndex < 0 && _flight.interactive(); }
+ MortimerFlight ¤tFlight() { return _castleActive ? _castle : _flight; }
+ bool interactive() const { return _castleActive ? _castle.courseView() :
+ _crashStage == kNoCrash && !_interviewActive && _introIndex < 0 && _flight.interactive(); }
void setMouse(const Common::Point &position);
- void updateSteering();
+ void updateSteering(bool centerView = false);
void updateView(int x, int y);
void updateInterface();
void fire();
@@ -98,6 +103,7 @@ private:
MortimerCutscene _intro;
MortimerFlight _flight;
MortimerInterview _interview;
+ MortimerCastle _castle;
MortimerSubtitles _subtitles;
MortimerCampaign _campaign;
Graphics::Surface _screen;
@@ -108,7 +114,7 @@ private:
SaudStreamState _soundState;
Common::Array<Audio::SoundHandle> _movieHandles;
Common::Array<Audio::SoundHandle> _collisionHandles;
- Audio::SoundHandle _scopeHandle, _rescueHandle, _glowHandle;
+ Audio::SoundHandle _scopeHandle, _rescueHandle, _glowHandle, _castleHandle;
Common::String _loadError;
Common::String _flightName, _pendingFlight, _subtitleName;
Common::Rect _viewport;
@@ -124,7 +130,7 @@ private:
uint _segment, _carriedStones, _completedSpecies, _shellShocks, _shockFrame, _crashCount;
uint _uiFrame, _shotFrame, _glowFrame, _glowStones, _pendingGlowStones;
uint _iconFlash[7];
- bool _finished, _controllerMode, _interviewActive;
+ bool _finished, _controllerMode, _interviewActive, _castleActive;
bool _resourcesLoaded, _playingCached;
bool _needsRepair, _hitJolt;
};
diff --git a/engines/scumm/insane/mortimer/movie.cpp b/engines/scumm/insane/mortimer/movie.cpp
index 7b374fc7214..6bcb084d3cd 100644
--- a/engines/scumm/insane/mortimer/movie.cpp
+++ b/engines/scumm/insane/mortimer/movie.cpp
@@ -392,6 +392,9 @@ bool MortimerMovie::load(Common::SeekableReadStream &stream, bool doubled, bool
return fail("Unsupported Mortimer movie header");
expectedFrames = READ_LE_UINT16(src + 2);
_frameRate = READ_LE_UINT32(src + 774);
+ // Single-frame NUT backgrounds have no playback clock.
+ if (!_frameRate && expectedFrames == 1)
+ _frameRate = 10;
if (!expectedFrames || _frameRate != 10)
return fail("Unsupported Mortimer movie timing");
memcpy(_initialPalette, src + 6, sizeof(_initialPalette));
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index faabd6bb7c3..4ae124ae527 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -148,6 +148,7 @@ MODULE_OBJS += \
insane/insane_scenes.o \
insane/insane_iact.o \
insane/mortimer/campaign.o \
+ insane/mortimer/castle.o \
insane/mortimer/dialogs.o \
insane/mortimer/flight.o \
insane/mortimer/interview.o \
Commit: e5c3219592ac48768d45cdd7c3b884951a9af65f
https://github.com/scummvm/scummvm/commit/e5c3219592ac48768d45cdd7c3b884951a9af65f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: implemented cheats and easter eggs
Changed paths:
engines/scumm/detection.h
engines/scumm/detection_tables.h
engines/scumm/insane/mortimer/castle.cpp
engines/scumm/insane/mortimer/castle.h
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/metaengine.cpp
diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index c0df2e1ab35..4f1be4f71d6 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -47,6 +47,8 @@ namespace Scumm {
#define GAMEOPTION_REBEL1_RESTORED_CONTENT GUIO_GAMEOPTIONS16
#define GAMEOPTION_REBEL2_RESTORED_CONTENT GUIO_GAMEOPTIONS17
#define GAMEOPTION_MORTIMER_RESTORED_CONTENT GUIO_GAMEOPTIONS18
+#define GAMEOPTION_MORTIMER_CHEATS GUIO_GAMEOPTIONS19
+#define GAMEOPTION_MORTIMER_PROGRAMMER GUIO_GAMEOPTIONS20
/**
* Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 6ba62a46092..78ad699908e 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -251,8 +251,8 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
- {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO3(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT)},
- {"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO5(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT, GAMEOPTION_MORTIMER_RESTORED_CONTENT)},
+ {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO5(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT, GAMEOPTION_MORTIMER_CHEATS, GAMEOPTION_MORTIMER_PROGRAMMER)},
+ {"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO6(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT, GAMEOPTION_MORTIMER_RESTORED_CONTENT, GAMEOPTION_MORTIMER_CHEATS)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
{"dig", "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/mortimer/castle.cpp b/engines/scumm/insane/mortimer/castle.cpp
index 81d14d23e22..f6220dc8db6 100644
--- a/engines/scumm/insane/mortimer/castle.cpp
+++ b/engines/scumm/insane/mortimer/castle.cpp
@@ -66,8 +66,8 @@ const byte kCastlePuzzles[3][8][12] = {
MortimerCastle::MortimerCastle() : MortimerFlight(false), _random("mortimer-castle"),
_nextVoice(0), _flightDifficulty(0), _riddleDifficulty(0), _puzzle(0), _step(0), _damage(0), _checkpointDamage(0),
- _wrongChoices(0), _crashes(0), _frame(0), _choiceFrame(0), _hintFrames(0), _holdFrames(0), _answer(0), _selection(-1),
- _stage(kEntry), _moviePending(false), _choosing(false), _leaving(false), _correct(false), _resourcesLoaded(false) {
+ _wrongChoices(0), _crashes(0), _frame(0), _choiceFrame(0), _hintFrames(0), _holdFrames(0), _answer(0), _selection(-1), _forcedChoice(0),
+ _stage(kEntry), _moviePending(false), _choosing(false), _leaving(false), _correct(false), _resourcesLoaded(false), _programmer(false) {
memset(_rooms, 0, sizeof(_rooms));
memset(_sides, 0, sizeof(_sides));
}
@@ -172,6 +172,7 @@ bool MortimerCastle::loadResources() {
bool MortimerCastle::start(int scene, uint flightDifficulty, uint riddleDifficulty, bool girl) {
_error.clear();
+ _forcedChoice = 0;
context().girl = girl;
_flightDifficulty = MIN(flightDifficulty, 2U);
_riddleDifficulty = MIN(riddleDifficulty, 2U);
@@ -379,6 +380,10 @@ void MortimerCastle::update() {
--_hintFrames;
if (!_choosing)
return;
+ if (_forcedChoice && _selection < 0) {
+ selectSide(!_step ? 0 : _sides[_step] ^ (_forcedChoice == 2));
+ _forcedChoice = 0;
+ }
++_choiceFrame;
if (_selection < 0 || _choiceFrame < 2)
return;
@@ -389,13 +394,17 @@ void MortimerCastle::update() {
enter(kLeft);
}
+void MortimerCastle::selectSide(uint side) {
+ _selection = side;
+ _correct = !_step || side == _sides[_step];
+ _choiceFrame = 0;
+ _voices.clear();
+ _nextVoice = 0;
+}
+
void MortimerCastle::click(const Common::Point &point) {
if (_choosing && _selection < 0) {
- _selection = _step ? (point.x <= 310 ? 1 : 0) : 0;
- _correct = !_step || uint(_selection) == _sides[_step];
- _choiceFrame = 0;
- _voices.clear();
- _nextVoice = 0;
+ selectSide(_step && point.x <= 310 ? 1 : 0);
} else if (_stage == kIdle) {
if (Common::Rect(184, 341, 459, 447).contains(point)) {
enter(kRiddle);
@@ -519,7 +528,8 @@ void MortimerCastle::drawOverlay(Graphics::Surface &screen, const Common::Point
const MortimerSprite &sprite = _selection >= 0 ? _spin[2 + MIN(_choiceFrame, 1U)] :
_choiceFrame < 2 ? _spin[_choiceFrame] : plaque[(_choiceFrame / 2) % 2];
sprite.draw(screen, origin.x + (side ? 248 : 404), origin.y + 132, false, &clip, 2);
- drawText(screen, mortimerRegion(code / 10).names[code % 10], side ? 26 : 416, 35, 208, 5);
+ const char *name = _programmer && !code ? "Programmer" : mortimerRegion(code / 10).names[code % 10];
+ drawText(screen, name, side ? 26 : 416, 35, 208, 5);
}
const Common::Point flames[] = {{43, 131}, {101, 68}, {258, 68}, {325, 131}};
for (uint i = 0; i < 4; ++i)
diff --git a/engines/scumm/insane/mortimer/castle.h b/engines/scumm/insane/mortimer/castle.h
index 00cfc5d5b75..b3706b01e0d 100644
--- a/engines/scumm/insane/mortimer/castle.h
+++ b/engines/scumm/insane/mortimer/castle.h
@@ -37,6 +37,9 @@ public:
bool skip();
void movieFinished();
void hit();
+ void clearDamage() { _damage = 0; }
+ void forceChoice(bool correct) { _forcedChoice = correct ? 1 : 2; }
+ void setProgrammer(bool enabled) { _programmer = enabled; }
void drawOverlay(Graphics::Surface &screen, const Common::Point &mouse, const Common::Point &origin, byte mouth) const;
const MortimerSound *takeSpeech(bool playing);
@@ -71,6 +74,7 @@ private:
void retry(bool crash);
void resolveChoice();
void showChoices();
+ void selectSide(uint side);
int animal(uint side) const;
void drawText(Graphics::Surface &screen, const Common::String &text, int x, int y, int width, byte color) const;
@@ -85,9 +89,9 @@ private:
uint _nextVoice, _flightDifficulty, _riddleDifficulty, _puzzle, _step, _damage, _checkpointDamage;
uint _wrongChoices, _crashes, _frame, _choiceFrame, _hintFrames, _holdFrames;
uint _rooms[5], _sides[6];
- int _answer, _selection;
+ int _answer, _selection, _forcedChoice;
Stage _stage;
- bool _moviePending, _choosing, _leaving, _correct, _resourcesLoaded;
+ bool _moviePending, _choosing, _leaving, _correct, _resourcesLoaded, _programmer;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 5a832cf4284..605fed8fea7 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -68,7 +68,7 @@ MortimerInterview::MortimerInterview(bool isDemo) : _random("mortimer"), _isDemo
_region(0), _difficulty(0), _textDifficulty(0), _tried(0), _stage(kIdle), _effect(kNoEffect),
_answer(-1), _previousAnswer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0), _guesses(3),
_species(0), _stones(0), _frame(0), _switchFrame(6), _payment(0),
- _moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false), _restartRegion(false) {
+ _moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false), _restartRegion(false), _programmer(false) {
memset(_palette, 0, sizeof(_palette));
memset(_gatePalette, 0, sizeof(_gatePalette));
}
@@ -93,9 +93,12 @@ bool MortimerInterview::loadText(const char *path, bool riddles) {
answer = heading == "Easy" ? 0 : heading == "Medium" ? 1 : 2;
} else {
animal = answer = -1;
- for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i)
- if (heading.equalsIgnoreCase(_isDemo ? kAnimals[i] : mortimerRegion(_region).names[i]))
+ for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i) {
+ const char *name = _programmer && !i ? "Programmer" :
+ _isDemo ? kAnimals[i] : mortimerRegion(_region).names[i];
+ if (heading.equalsIgnoreCase(name))
animal = i;
+ }
}
} else if (animal >= 0 && answer >= 0 && answer < 3) {
Common::String &text = riddles ? _riddles[animal][answer] : _dialogue[animal][answer];
@@ -105,6 +108,9 @@ bool MortimerInterview::loadText(const char *path, bool riddles) {
}
}
for (uint i = 0; i < ARRAYSIZE(kAnimals); ++i) {
+ // The alternate resource table has dialogue, but no Programmer riddle.
+ if (riddles && _programmer && !i)
+ continue;
for (uint j = 0; j < (riddles && _isDemo ? 1U : 3U); ++j)
if ((riddles ? _riddles[i][j] : _dialogue[i][j]).empty())
return false;
@@ -112,8 +118,9 @@ bool MortimerInterview::loadText(const char *path, bool riddles) {
return !file.err();
}
-bool MortimerInterview::loadResources(uint region) {
+bool MortimerInterview::loadResources(uint region, bool programmer) {
_region = region;
+ _programmer = programmer && !_isDemo && !region;
for (uint i = 0; i < 7; ++i)
for (uint j = 0; j < 3; ++j) {
_dialogue[i][j].clear();
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index b1e75453a0d..3393cf97fa7 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -37,11 +37,12 @@ public:
enum Effect { kNoEffect, kButtonEffect, kPayoutEffect, kSwitchEffect };
explicit MortimerInterview(bool isDemo = true);
- bool loadResources(uint region = 0);
+ bool loadResources(uint region = 0, bool programmer = false);
void setDifficulty(uint difficulty) { _difficulty = difficulty; }
void resetRiddle();
void resetAttempts();
void start(uint species, uint stones, int scene = 10);
+ void rescueAll() { _species = 0x7f; _selected = 0; }
void click(const Common::Point &point);
void scroll(int direction);
void update();
@@ -97,7 +98,7 @@ private:
Effect _effect;
int _answer, _previousAnswer, _selected, _question, _guess, _lastAction, _textScroll, _guesses;
uint _species, _stones, _frame, _switchFrame, _payment;
- bool _moviePending, _riddleView, _solving, _help, _helpExplained, _restartRegion;
+ bool _moviePending, _riddleView, _solving, _help, _helpExplained, _restartRegion, _programmer;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 9abda778787..5576f16b3e0 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -61,6 +61,8 @@ enum MortimerAction {
kActionMenu, kActionMouse, kActionJoystick
};
+enum { kBootProgrammer = 1000 };
+
Common::KeymapArray MortimerEngine::initKeymaps() {
Common::Keymap *keymap = new Common::Keymap(Common::Keymap::kKeymapTypeGame, "mortimer", "Mortimer");
keymap->setPartialMatchAllowed(false);
@@ -73,7 +75,7 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
{"FIRE", _s("Fire"), kActionFire, {"MOUSE_LEFT", "MOUSE_RIGHT", "JOY_A", "JOY_B"}},
{"PAUSE", _s("Pause"), kActionPause, {"SPACE", "JOY_START", nullptr, nullptr}},
{"SKIP", _s("Skip"), kActionSkip, {"ESCAPE", "JOY_Y", nullptr, nullptr}},
- {"RESTART", _s("Restart"), kActionRestart, {"r", "JOY_X", nullptr, nullptr}},
+ {"RESTART", _s("Restart"), kActionRestart, {"C+r", "JOY_X", nullptr, nullptr}},
{"QUIT", _s("Quit"), kActionQuit, {"q", "A+q", nullptr, nullptr}},
{"MENU", _s("Game menu"), kActionMenu, {"F5", "JOY_LEFT_SHOULDER", nullptr, nullptr}},
{"MOUSE", _s("Mouse control"), kActionMouse, {"A+m", nullptr, nullptr, nullptr}},
@@ -118,7 +120,8 @@ MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
_segment(0), _carriedStones(0), _completedSpecies(0), _shellShocks(0), _shockFrame(0), _crashCount(0),
_uiFrame(0), _shotFrame(3), _glowFrame(10), _glowStones(0), _pendingGlowStones(0), _finished(false),
- _controllerMode(false), _interviewActive(false), _castleActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false) {
+ _controllerMode(false), _interviewActive(false), _castleActive(false), _resourcesLoaded(false), _playingCached(false), _needsRepair(false), _hitJolt(false),
+ _cheats(ConfMan.getBool("mortimer_cheats")), _programmer(!isDemo && ConfMan.getBool("mortimer_programmer_easter_egg")), _invulnerable(false) {
memset(_steering, 0, sizeof(_steering));
memset(_iconFlash, 0, sizeof(_iconFlash));
}
@@ -294,7 +297,7 @@ bool MortimerEngine::loadRegion(uint region) {
_loadError = Common::String::format("Missing or invalid %s", name.c_str());
return false;
}
- if (!_interview.loadResources(region)) {
+ if (!_interview.loadResources(region, _programmer)) {
_loadError = _interview.errorMessage();
return false;
}
@@ -397,6 +400,7 @@ bool MortimerEngine::enterCampaignScene() {
}
_castleActive = true;
_viewport = Common::Rect(8, 104, 632, 448);
+ _castle.setProgrammer(_programmer);
if (!_castle.start(scene.id, _flightDifficulty, _riddleDifficulty, _intro.context().girl)) {
_loadError = _castle.errorMessage();
return false;
@@ -827,13 +831,8 @@ bool MortimerEngine::updateCastle() {
}
playMovieSounds();
_system->getPaletteManager()->setPalette(_castle.palette(), 0, 256);
- if (_castle.courseView() && _castle.acceptsShots() && _castle.hitsObstacle(moviePoint(_aim))) {
- _castle.hit();
- _hitJolt = true;
- if (!processCastle())
- return false;
- playCollisionSound();
- }
+ if (_castle.courseView() && _castle.acceptsShots() && _castle.hitsObstacle(moviePoint(_aim)) && !addShellShock())
+ return false;
return processCastle();
}
@@ -881,18 +880,100 @@ bool MortimerEngine::updateScene() {
return followFlightBranch();
playMovieSounds();
_system->getPaletteManager()->setPalette(_interviewActive ? _interview.palette(movie) : movie.palette(), 0, 256);
- if (interactive() && _flight.hitsObstacle(moviePoint(_aim))) {
+ if (interactive() && _flight.hitsObstacle(moviePoint(_aim)) && !addShellShock())
+ return false;
+ return true;
+}
+
+bool MortimerEngine::addShellShock() {
+ if (_invulnerable || isPaused() || !interactive())
+ return true;
+ if (_castleActive) {
+ if (!_castle.acceptsShots())
+ return true;
+ _castle.hit();
+ _hitJolt = true;
+ if (!processCastle())
+ return false;
+ } else if (_shellShocks + 1 >= (_isDemo ? 18U : 15U)) {
// Retail crashes on the fifteenth hit; the demo allows eighteen.
- if (_shellShocks + 1 >= (_isDemo ? 18U : 15U)) {
- if (!(_isDemo && !_restoredContent ? restart() : startCrash()))
- return false;
- } else {
- ++_shellShocks;
- _shockFrame = 0;
- _hitJolt = !_isDemo || _restoredContent;
+ if (!(_isDemo && !_restoredContent ? restart() : startCrash()))
+ return false;
+ } else {
+ ++_shellShocks;
+ _shockFrame = 0;
+ _hitJolt = !_isDemo || _restoredContent;
+ }
+ // Movie changes stop audio, so enqueue the fatal impact afterwards.
+ playCollisionSound();
+ return true;
+}
+
+void MortimerEngine::clearShellShock() {
+ if (_castleActive)
+ _castle.clearDamage();
+ else
+ _shellShocks = _shockFrame = 0;
+ _hitJolt = false;
+ updateMovieContext();
+}
+
+bool MortimerEngine::skipRegion() {
+ const uint region = _campaign.scene().region;
+ const uint next = region < 5 ? (region + 1) % 5 : 0;
+ _loadedRegion = -1;
+ _crashStage = kNoCrash;
+ _needsRepair = _hitJolt = false;
+ _flight.clearProgress();
+ _interview.resetRiddle();
+ _carriedStones = _completedSpecies = _creditedRescues = _shellShocks = 0;
+ _shockFrame = _crashCount = 0;
+ return _campaign.start(next == 4 ? 34 : next) && enterCampaignScene();
+}
+
+bool MortimerEngine::handleCheatKey(const Common::KeyState &key) {
+ if (!_cheats || _finished || (key.flags & (Common::KBD_CTRL | Common::KBD_META)))
+ return false;
+ const bool alt = key.flags & Common::KBD_ALT;
+ const bool plus = key.keycode == Common::KEYCODE_EQUALS || key.keycode == Common::KEYCODE_PLUS ||
+ key.keycode == Common::KEYCODE_KP_PLUS;
+ const bool minus = key.keycode == Common::KEYCODE_MINUS || key.keycode == Common::KEYCODE_UNDERSCORE ||
+ key.keycode == Common::KEYCODE_KP_MINUS;
+ if ((plus || minus) && alt == !_isDemo) {
+ if (interactive()) {
+ if (plus)
+ clearShellShock();
+ else
+ addShellShock();
}
- // Movie changes stop audio, so enqueue the fatal impact afterwards.
- playCollisionSound();
+ return true;
+ }
+ if (alt)
+ return false;
+ if (key.keycode == Common::KEYCODE_a) {
+ _completedSpecies = 0x7f;
+ if (_interviewActive)
+ _interview.rescueAll();
+ return true;
+ }
+ if (key.keycode == Common::KEYCODE_i) {
+ _invulnerable = !_invulnerable;
+ return true;
+ }
+ if (_isDemo)
+ return false;
+ switch (key.keycode) {
+ case Common::KEYCODE_s:
+ if (!isPaused())
+ skipRegion();
+ break;
+ case Common::KEYCODE_r:
+ case Common::KEYCODE_w:
+ if (_castleActive && _castle.sceneId() == 21)
+ _castle.forceChoice(key.keycode == Common::KEYCODE_r);
+ break;
+ default:
+ return false;
}
return true;
}
@@ -1005,6 +1086,9 @@ void MortimerEngine::pauseEngineIntern(bool pause) {
}
Common::Error MortimerEngine::run() {
+ const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : -1;
+ if (_isDemo && boot == kBootProgrammer)
+ return Common::Error(Common::kUnsupportedGameidError, "The Programmer interview requires the full Mortimer game data");
if (_isDemo && !(_resourcesLoaded = loadResources()))
return Common::Error(Common::kReadingFailed, _loadError);
if (!_isDemo && !_subtitles.load("RESOURCE/DATA/BEGIN.TRS", "RESOURCE/GENERAL/ESTRO20.NUT", "RESOURCE/GENERAL/MORTFNTH.NUT"))
@@ -1018,10 +1102,17 @@ Common::Error MortimerEngine::run() {
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
bool exit;
- // Use the original scene IDs for ScummVM's developer startup shortcut.
- // Interview entry points supply rescued animals and glowstones.
- const int boot = ConfMan.hasKey("boot_param") ? ConfMan.getInt("boot_param") : -1;
- if (!_isDemo) {
+ // 1000 opens the hidden interview; other values select original scene IDs.
+ if (boot == kBootProgrammer) {
+ _programmer = true;
+ exit = !loadRegion(0);
+ if (!exit) {
+ _completedSpecies = 0x7f;
+ _carriedStones = 30;
+ _campaign.start(8);
+ exit = !enterCampaignScene();
+ }
+ } else if (!_isDemo) {
_subtitleName = "RESOURCE/DATA/BEGIN.TRS";
if (!_campaign.start(boot < 0 ? 24 : boot))
_campaign.start();
@@ -1041,6 +1132,17 @@ Common::Error MortimerEngine::run() {
while (!shouldQuit() && !exit) {
Common::Event event;
while (_eventMan->pollEvent(event)) {
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbdRepeat)
+ continue;
+ if (handleCheatKey(event.kbd)) {
+ if (!_loadError.empty()) {
+ exit = true;
+ break;
+ }
+ continue;
+ }
+ }
if (event.type == Common::EVENT_MOUSEMOVE) {
if (!isPaused()) {
setMouse(event.mouse);
@@ -1077,7 +1179,7 @@ Common::Error MortimerEngine::run() {
case kActionSkip:
if (pause.isActive())
pause.clear();
- else
+ else if (!interactive() || _cheats)
exit = !nextScene(true);
break;
case kActionQuit:
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 0a8e917009c..8e2618ecd58 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -96,6 +96,10 @@ private:
void playMovieSounds(bool seek = false);
void playCollisionSound();
void stopSounds();
+ bool handleCheatKey(const Common::KeyState &key);
+ bool skipRegion();
+ bool addShellShock();
+ void clearShellShock();
const bool _isDemo, _restoredContent;
uint _flightDifficulty, _riddleDifficulty;
@@ -133,6 +137,7 @@ private:
bool _finished, _controllerMode, _interviewActive, _castleActive;
bool _resourcesLoaded, _playingCached;
bool _needsRepair, _hitJolt;
+ bool _cheats, _programmer, _invulnerable;
};
} // End of namespace Scumm
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 422b8e9d353..53fc9986041 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -995,6 +995,24 @@ const ExtraGuiOption enableMortimerRestoredContent = {
0
};
+const ExtraGuiOption enableMortimerCheats = {
+ _s("Enable cheats"),
+ _s("Enable the original game's cheat shortcuts for rescuing animals, changing shellshock and skipping flights"),
+ "mortimer_cheats",
+ false,
+ 0,
+ 0
+};
+
+const ExtraGuiOption enableMortimerProgrammer = {
+ _s("Programmer's Easter egg"),
+ _s("Replace the rhinoceros interview text with the hidden programmer dialogue"),
+ "mortimer_programmer_easter_egg",
+ false,
+ 0,
+ 0
+};
+
const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
ExtraGuiOptions options;
// Query the GUI options
@@ -1064,6 +1082,13 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
(gameid == "mortimer" && extra == "Demo")) {
options.push_back(enableMortimerRestoredContent);
}
+ if (target.empty() || guiOptions.contains(GAMEOPTION_MORTIMER_CHEATS) || gameid == "mortimer") {
+ options.push_back(enableMortimerCheats);
+ }
+ if (target.empty() || guiOptions.contains(GAMEOPTION_MORTIMER_PROGRAMMER) ||
+ (gameid == "mortimer" && extra != "Demo" && !guiOptions.contains(GAMEOPTION_MORTIMER_RESTORED_CONTENT))) {
+ options.push_back(enableMortimerProgrammer);
+ }
if (target.empty() || gameid == "comi") {
options.push_back(comiObjectLabelsOption);
Commit: 30cdd215a4af0855c125ea7d07dffee6c4aa06eb
https://github.com/scummvm/scummvm/commit/30cdd215a4af0855c125ea7d07dffee6c4aa06eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: adjusted the position of some elements in the full game
Changed paths:
engines/scumm/insane/mortimer/mortimer.cpp
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 5576f16b3e0..820db146f5d 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -1247,18 +1247,18 @@ void MortimerEngine::drawTargets() {
const bool hostile = (target.id >= 700 && target.id < 800) || (target.id >= 9000 && target.id < 9400);
if (!target.active || !target.id || (target.id >= 800 && !hostile))
continue;
+ const Common::Point top = screenPoint(Common::Point(target.rect.left, target.rect.top));
+ const Common::Point bottom = screenPoint(Common::Point(target.rect.right, target.rect.bottom));
if (target.hit) {
const Common::Array<MortimerSprite> &effect = target.hitWeapon == MortimerFlight::kSlimeGun ? _slimeHit : _scopeHit;
const uint frame = flight.frame() - target.rescuedAt;
if (frame < effect.size()) {
- const Common::Point center = screenPoint(Common::Point((target.rect.left + target.rect.right) / 2,
- (target.rect.top + target.rect.bottom) / 2));
+ // Average after scaling to retain the midpoint of odd-sized targets.
+ const Common::Point center((top.x + bottom.x) / 2, (top.y + bottom.y) / 2);
drawSprite(effect[frame], center.x, center.y, true, &_viewport);
}
} else if ((target.id < 700 && flight.weapon() == MortimerFlight::kRevitascope) ||
(hostile && flight.weapon() == MortimerFlight::kSlimeGun)) {
- const Common::Point top = screenPoint(Common::Point(target.rect.left, target.rect.top));
- const Common::Point bottom = screenPoint(Common::Point(target.rect.right, target.rect.bottom));
drawSprite(_corners[0], top.x, top.y, true, &_viewport);
drawSprite(_corners[1], bottom.x, bottom.y, true, &_viewport);
}
@@ -1303,14 +1303,17 @@ void MortimerEngine::drawDashboard() {
const uint level = _isDemo || _shellShocks <= 10 ? _shellShocks / 2 : _shellShocks - 5;
const uint gauge = level < 8 ? 19 + level : 27 + _shockFrame;
drawSprite(_dashboard[gauge], 180, 442);
- const Common::Point iconOrigins[] = {{158, 364}, {203, 358}, {248, 358}, {293, 354}, {339, 355}, {383, 360}, {428, 361}};
+ const Common::Point demoIconOrigins[] = {{158, 364}, {203, 358}, {248, 358}, {293, 354}, {339, 355}, {383, 360}, {428, 361}};
+ // Retail uses centered icons (SHELL 43f12c, 44304c); the demo uses sprite origins.
+ const Common::Point iconCenters[] = {{176, 384}, {222, 378}, {265, 378}, {310, 376}, {355, 379}, {399, 380}, {444, 384}};
const Common::Point flashOrigins[] = {{154, 345}, {198, 338}, {243, 334}, {288, 332}, {333, 334}, {378, 338}, {422, 345}};
for (uint species = 0; species < 7; ++species) {
if ((_iconFlash[species] & 1) || (_interviewActive && int(species) == _interview.highlightedAnimal()))
drawSprite(_dashboard[species], flashOrigins[species].x, flashOrigins[species].y);
const uint available = _interviewActive ? _interview.species() : (_completedSpecies | _flight.rescuedSpecies());
const bool rescued = available & (1 << species);
- drawSprite(_icons[species * 2 + (rescued ? 0 : 1)], iconOrigins[species].x, iconOrigins[species].y);
+ const Common::Point &origin = _isDemo ? demoIconOrigins[species] : iconCenters[species];
+ drawSprite(_icons[species * 2 + (rescued ? 0 : 1)], origin.x, origin.y, !_isDemo);
}
const uint thresholds[] = {1, 2, 3, 4, 7, 20, 30, 40, 50};
for (int i = ARRAYSIZE(thresholds) - 1; i >= 0; --i) {
@@ -1348,8 +1351,8 @@ void MortimerEngine::draw() {
drawSprite(_reticles[0], _castle.choosing() ? _mouse.x : _aim.x,
_castle.choosing() ? _mouse.y : _aim.y, true, &_viewport);
} else {
- drawSprite(_head[_uiFrame % 4], 270, 200);
drawSprite(_reticles[_flight.weapon() == MortimerFlight::kSlimeGun ? 0 : 1], _aim.x, _aim.y, true, &_viewport);
+ drawSprite(_head[_uiFrame % 4], 270, 200);
drawSprite(_cockpit[0], 0, 0);
drawSprite(_cockpit[1], 567, 0);
drawSprite(_cockpit[2], 0, 0);
Commit: bc691b52cadef313fe437f368486f68676342096
https://github.com/scummvm/scummvm/commit/bc691b52cadef313fe437f368486f68676342096
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: automatic saving per profile
Changed paths:
A engines/scumm/insane/mortimer/saveload.cpp
A engines/scumm/insane/saveload.cpp
A engines/scumm/insane/saveload.h
engines/scumm/detection_tables.h
engines/scumm/insane/mortimer/castle.h
engines/scumm/insane/mortimer/dialogs.cpp
engines/scumm/insane/mortimer/dialogs.h
engines/scumm/insane/mortimer/flight.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/rebel1/saveload.cpp
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 78ad699908e..3eac7ed12e5 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -251,7 +251,7 @@ static const GameSettings gameVariantsTable[] = {
{"rebel2", "Demo v1.0", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO | GF_TESTING, Common::kPlatformMacintosh, GUIO6(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE, GAMEOPTION_REBEL2_RESTORED_CONTENT)},
{"rebel2", "PlayStation", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_UNSTABLE | GF_16BIT_COLOR, Common::kPlatformPSX, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
- {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO5(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT, GAMEOPTION_MORTIMER_CHEATS, GAMEOPTION_MORTIMER_PROGRAMMER)},
+ {"mortimer", "", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_UNSTABLE, Common::kPlatformWindows, GUIO4(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_MORTIMER_CHEATS, GAMEOPTION_MORTIMER_PROGRAMMER)},
{"mortimer", "Demo", 0, GID_MORTIMER, 7, 0, MDT_NONE, GF_DEMO | GF_UNSTABLE, Common::kPlatformWindows, GUIO6(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOSUBTITLES, GUIO_NOASPECT, GAMEOPTION_MORTIMER_RESTORED_CONTENT, GAMEOPTION_MORTIMER_CHEATS)},
{"dig", "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/mortimer/castle.h b/engines/scumm/insane/mortimer/castle.h
index b3706b01e0d..ddae2b0fd74 100644
--- a/engines/scumm/insane/mortimer/castle.h
+++ b/engines/scumm/insane/mortimer/castle.h
@@ -49,6 +49,7 @@ public:
bool choosing() const { return _choosing; }
bool paused() const { return _choosing || _stage == kIdle || _holdFrames; }
bool finished() const { return _stage == kFinished; }
+ bool solved() const { return _stage == kCorrect || _stage == kFinished; }
bool hasSpeech() const { return _nextVoice < _voices.size(); }
bool forceSubtitles() const { return _stage == kSong; }
uint damage() const { return _damage; }
diff --git a/engines/scumm/insane/mortimer/dialogs.cpp b/engines/scumm/insane/mortimer/dialogs.cpp
index c9ffa44b701..7390371caff 100644
--- a/engines/scumm/insane/mortimer/dialogs.cpp
+++ b/engines/scumm/insane/mortimer/dialogs.cpp
@@ -24,13 +24,26 @@
#include "common/util.h"
#include "gui/gui-manager.h"
#include "gui/ThemeEval.h"
+#include "gui/message.h"
+#include "gui/widgets/edittext.h"
#include "gui/widgets/popup.h"
namespace Scumm {
-MortimerDifficultyDialog::MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty)
- : GUI::Dialog("MortimerDifficultyDialog") {
- new GUI::StaticTextWidget(this, _name + ".Title", _("Select difficulty"));
+MortimerDifficultyDialog::MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty, bool girl, bool newPilot)
+ : GUI::Dialog("MortimerDifficultyDialog"), _character(nullptr), _pilotName(nullptr) {
+ new GUI::StaticTextWidget(this, _name + ".Title", newPilot ? _("New pilot") : _("Select difficulty"));
+ if (newPilot) {
+ new GUI::StaticTextWidget(this, _name + ".NameLabel", _("Pilot name:"));
+ _pilotName = new GUI::EditTextWidget(this, _name + ".Name", Common::U32String());
+ }
+ if (!isDemo) {
+ new GUI::StaticTextWidget(this, _name + ".CharacterLabel", _("Character:"));
+ _character = new GUI::PopUpWidget(this, _name + ".Character");
+ _character->appendEntry(_("Sid"), 0);
+ _character->appendEntry(_("Sally"), 1);
+ _character->setSelectedTag(girl ? 1 : 0);
+ }
new GUI::StaticTextWidget(this, _name + ".FlightLabel", _("Flight difficulty:"));
new GUI::StaticTextWidget(this, _name + ".RiddleLabel", _("Riddle difficulty:"));
_flight = new GUI::PopUpWidget(this, _name + ".Flight", _("Choose how difficult flying and recovering from crashes should be."));
@@ -60,6 +73,16 @@ uint MortimerDifficultyDialog::riddleDifficulty() const {
return _riddle->getSelectedTag();
}
+bool MortimerDifficultyDialog::girl() const {
+ return _character && _character->getSelectedTag() == 1;
+}
+
+Common::String MortimerDifficultyDialog::pilotName() const {
+ Common::String name = _pilotName ? _pilotName->getEditString().encode() : Common::String();
+ name.trim();
+ return name;
+}
+
void MortimerDifficultyDialog::reflowLayout() {
GUI::ThemeEval &layouts = *g_gui.xmlEval();
const int lineHeight = layouts.getVar("Globals.Line.Height");
@@ -67,8 +90,12 @@ void MortimerDifficultyDialog::reflowLayout() {
layouts.addDialog(_name, "screen_center")
.addLayout(GUI::ThemeLayout::kLayoutVertical, 8)
.addPadding(8, 8, 8, 8)
- .addWidget("Title", "", width, lineHeight, Graphics::kTextAlignCenter)
- .addWidget("FlightLabel", "", -1, lineHeight)
+ .addWidget("Title", "", width, lineHeight, Graphics::kTextAlignCenter);
+ if (_pilotName)
+ layouts.addWidget("NameLabel", "", -1, lineHeight).addWidget("Name", "EditTextWidget");
+ if (_character)
+ layouts.addWidget("CharacterLabel", "", -1, lineHeight).addWidget("Character", "PopUp");
+ layouts.addWidget("FlightLabel", "", -1, lineHeight)
.addWidget("Flight", "PopUp")
.addWidget("RiddleLabel", "", -1, lineHeight)
.addWidget("Riddle", "PopUp")
@@ -84,6 +111,57 @@ void MortimerDifficultyDialog::reflowLayout() {
}
void MortimerDifficultyDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
+ if (cmd == GUI::kOKCmd) {
+ if (_pilotName && (pilotName().empty() || pilotName().size() > 31)) {
+ GUI::MessageDialog message(pilotName().empty() ? _("Please enter a pilot name.") : _("Please enter a shorter pilot name."));
+ message.runModal();
+ return;
+ }
+ setResult(1);
+ close();
+ } else {
+ GUI::Dialog::handleCommand(sender, cmd, data);
+ }
+}
+
+MortimerPilotDialog::MortimerPilotDialog(const SaveStateList &pilots) : GUI::Dialog("MortimerPilotDialog") {
+ new GUI::StaticTextWidget(this, _name + ".Title", _("Select pilot"));
+ new GUI::StaticTextWidget(this, _name + ".Hint", _("Progress is saved after each riddle is solved."));
+ _pilot = new GUI::PopUpWidget(this, _name + ".Pilot");
+ _pilot->appendEntry(_("New pilot"), kNewPilot);
+ for (uint i = 0; i < pilots.size(); ++i)
+ _pilot->appendEntry(pilots[i].getDescription(), pilots[i].getSaveSlot());
+ _pilot->setSelected(pilots.empty() ? 0 : 1);
+ new GUI::ButtonWidget(this, _name + ".Cancel", _("~C~ancel"), Common::U32String(), GUI::kCloseCmd);
+ new GUI::ButtonWidget(this, _name + ".Start", _("Start"), Common::U32String(), GUI::kOKCmd, Common::ASCII_RETURN);
+}
+
+int MortimerPilotDialog::selectedPilot() const {
+ return _pilot->getSelectedTag();
+}
+
+void MortimerPilotDialog::reflowLayout() {
+ GUI::ThemeEval &layouts = *g_gui.xmlEval();
+ const int lineHeight = layouts.getVar("Globals.Line.Height");
+ const int width = 280 * g_gui.getScaleFactor();
+ layouts.addDialog(_name, "screen_center")
+ .addLayout(GUI::ThemeLayout::kLayoutVertical, 8)
+ .addPadding(8, 8, 8, 8)
+ .addWidget("Title", "", width, lineHeight, Graphics::kTextAlignCenter)
+ .addWidget("Pilot", "PopUp")
+ .addWidget("Hint", "", -1, lineHeight, Graphics::kTextAlignCenter)
+ .addLayout(GUI::ThemeLayout::kLayoutHorizontal, 8, GUI::ThemeLayout::kItemAlignEnd)
+ .addPadding(0, 0, 0, 0)
+ .addSpace()
+ .addWidget("Cancel", "Button")
+ .addWidget("Start", "Button")
+ .closeLayout()
+ .closeLayout()
+ .closeDialog();
+ GUI::Dialog::reflowLayout();
+}
+
+void MortimerPilotDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
if (cmd == GUI::kOKCmd) {
setResult(1);
close();
diff --git a/engines/scumm/insane/mortimer/dialogs.h b/engines/scumm/insane/mortimer/dialogs.h
index b69188539e5..b7233e1950f 100644
--- a/engines/scumm/insane/mortimer/dialogs.h
+++ b/engines/scumm/insane/mortimer/dialogs.h
@@ -22,18 +22,22 @@
#define SCUMM_INSANE_MORTIMER_DIALOGS_H
#include "gui/dialog.h"
+#include "engines/savestate.h"
namespace GUI {
class PopUpWidget;
+class EditTextWidget;
}
namespace Scumm {
class MortimerDifficultyDialog : public GUI::Dialog {
public:
- MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty);
+ MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty, bool girl = false, bool newPilot = false);
uint flightDifficulty() const;
uint riddleDifficulty() const;
+ bool girl() const;
+ Common::String pilotName() const;
void reflowLayout() override;
protected:
@@ -41,6 +45,22 @@ protected:
private:
GUI::PopUpWidget *_flight, *_riddle;
+ GUI::PopUpWidget *_character;
+ GUI::EditTextWidget *_pilotName;
+};
+
+class MortimerPilotDialog : public GUI::Dialog {
+public:
+ enum { kNewPilot = 100 };
+ explicit MortimerPilotDialog(const SaveStateList &pilots);
+ int selectedPilot() const;
+ void reflowLayout() override;
+
+protected:
+ void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
+
+private:
+ GUI::PopUpWidget *_pilot;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/flight.cpp b/engines/scumm/insane/mortimer/flight.cpp
index 489ea5d5135..97a35f47011 100644
--- a/engines/scumm/insane/mortimer/flight.cpp
+++ b/engines/scumm/insane/mortimer/flight.cpp
@@ -41,7 +41,9 @@ void MortimerFlight::clearProgress() {
_rescuedCount = _rescuedSpecies = 0;
_weapon = kRevitascope;
memset(_hitTargets, 0, sizeof(_hitTargets));
+ const bool girl = context().girl;
context() = MortimerMovieContext();
+ context().girl = girl;
}
void MortimerFlight::clearHostiles() {
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index 3393cf97fa7..a3a87c37fd6 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -60,6 +60,7 @@ public:
bool movieDoubled() const;
bool wantsFlight() const { return _stage == kFlyAway; }
bool finished() const { return _stage == kFinished; }
+ bool solved() const { return _stage == kCorrect || _stage == kOpenGate || _stage == kFinished; }
bool restartsRegion() const { return _restartRegion; }
int sceneId() const;
int riddleId() const { return _answer * 10 + _textDifficulty; }
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index 820db146f5d..f0afc121021 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -115,6 +115,7 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
_restoredContent(isDemo && ConfMan.getBool("mortimer_restored_content")), _flightDifficulty(0), _riddleDifficulty(0),
+ _pilotSlot(-1), _checkpoint(0), _girl(false), _pilotLoadRequested(false),
_random("mortimer-flight"), _flight(isDemo, _restoredContent), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
@@ -132,16 +133,18 @@ MortimerEngine::~MortimerEngine() {
}
bool MortimerEngine::hasFeature(EngineFeature feature) const {
- return feature == kSupportsReturnToLauncher;
+ return feature == kSupportsReturnToLauncher || (!_isDemo && feature == kSupportsLoadingDuringRuntime);
}
bool MortimerEngine::selectDifficulty() {
if (_isDemo && !_restoredContent)
return true;
- MortimerDifficultyDialog dialog(_isDemo, _flightDifficulty, _riddleDifficulty);
+ MortimerDifficultyDialog dialog(_isDemo, _flightDifficulty, _riddleDifficulty, _girl);
if (runDialog(dialog) != 1 || shouldQuit())
return false;
setDifficulty(dialog.flightDifficulty(), dialog.riddleDifficulty());
+ _girl = dialog.girl();
+ _intro.context().girl = _flight.context().girl = _girl;
return true;
}
@@ -401,14 +404,14 @@ bool MortimerEngine::enterCampaignScene() {
_castleActive = true;
_viewport = Common::Rect(8, 104, 632, 448);
_castle.setProgrammer(_programmer);
- if (!_castle.start(scene.id, _flightDifficulty, _riddleDifficulty, _intro.context().girl)) {
+ if (!_castle.start(scene.id, _flightDifficulty, _riddleDifficulty, _girl)) {
_loadError = _castle.errorMessage();
return false;
}
return processCastle();
}
_movieHandles.clear();
- const Common::String name = _campaign.movieName(_intro.context().girl);
+ const Common::String name = _campaign.movieName(_girl);
Common::File file;
if (!file.open(Common::Path(name))) {
_loadError = Common::String::format("Missing %s", name.c_str());
@@ -544,9 +547,7 @@ bool MortimerEngine::finishCrash() {
_needsRepair = false;
// Easy flight keeps rescues and stones even after a full repair.
if (_flightDifficulty != 0) {
- const bool girl = _flight.context().girl;
_flight.clearProgress();
- _flight.context().girl = girl;
_carriedStones = _completedSpecies = _creditedRescues = 0;
_interview.resetAttempts();
}
@@ -696,6 +697,8 @@ bool MortimerEngine::nextScene(bool skip) {
}
bool MortimerEngine::processInterview() {
+ if (!_isDemo && _interview.solved())
+ saveCheckpoint(_loadedRegion + 1);
if (_interview.wantsFlight()) {
_carriedStones = _interview.stones();
_completedSpecies = _interview.species();
@@ -767,6 +770,8 @@ bool MortimerEngine::processCastle() {
_loadError = _castle.errorMessage();
return false;
}
+ if (_castle.solved())
+ saveCheckpoint(5);
if (_castle.finished()) {
stopSounds();
_castleActive = false;
@@ -1097,13 +1102,23 @@ Common::Error MortimerEngine::run() {
_screen.create(640, 480, Graphics::PixelFormat::createFormatCLUT8());
_screen.fillRect(Common::Rect(640, 480), 10);
syncSoundSettings();
- if (!selectDifficulty())
+ if (!_isDemo && ConfMan.hasKey("save_slot") && ConfMan.getInt("save_slot") >= 0) {
+ const Common::Error result = loadGameState(ConfMan.getInt("save_slot"));
+ if (result.getCode() != Common::kNoError)
+ return result;
+ } else if (!_isDemo && boot < 0) {
+ if (!selectPilot())
+ return Common::kNoError;
+ } else if (!selectDifficulty()) {
return Common::kNoError;
+ }
const bool oldCursor = CursorMan.showMouse(false);
PauseToken pause;
bool exit;
// 1000 opens the hidden interview; other values select original scene IDs.
- if (boot == kBootProgrammer) {
+ if (_pilotLoadRequested) {
+ exit = !startPilot();
+ } else if (boot == kBootProgrammer) {
_programmer = true;
exit = !loadRegion(0);
if (!exit) {
@@ -1130,6 +1145,12 @@ Common::Error MortimerEngine::run() {
exit = !(loadIntro(boot >= 19 && boot <= 23 ? boot - 19 : 0) && startMovie());
}
while (!shouldQuit() && !exit) {
+ if (_pilotLoadRequested) {
+ if (pause.isActive())
+ pause.clear();
+ if (!startPilot())
+ break;
+ }
Common::Event event;
while (_eventMan->pollEvent(event)) {
if (event.type == Common::EVENT_KEYDOWN) {
@@ -1210,9 +1231,13 @@ Common::Error MortimerEngine::run() {
break;
}
}
+ if (_pilotLoadRequested)
+ break;
}
if (exit)
break;
+ if (_pilotLoadRequested)
+ continue;
if (!isPaused() && !_finished && int32(_system->getMillis() - _nextFrameTime) >= 0) {
_nextFrameTime += 1000 / currentMovie().frameRate();
if (!updateScene())
@@ -1224,8 +1249,14 @@ Common::Error MortimerEngine::run() {
draw();
if (_finished) {
GUI::MessageDialog dialog(_("Play again?"), _("Restart"), _("Quit"));
- if (dialog.runModal() != GUI::kMessageOK || !selectDifficulty() || !restart(true))
+ if (runDialog(dialog) != GUI::kMessageOK)
+ break;
+ if (_isDemo) {
+ if (!selectDifficulty() || !restart(true))
+ break;
+ } else if (!selectPilot()) {
break;
+ }
}
_system->delayMillis(10);
}
@@ -1233,7 +1264,7 @@ Common::Error MortimerEngine::run() {
CursorMan.showMouse(oldCursor);
if (!_loadError.empty())
warning("Mortimer: %s", _loadError.c_str());
- return _loadError.empty() ? Common::kNoError : Common::Error(Common::kReadingFailed, _loadError);
+ return _loadError.empty() ? savePilot() : Common::Error(Common::kReadingFailed, _loadError);
}
void MortimerEngine::drawSprite(const MortimerSprite &sprite, int x, int y, bool centered, const Common::Rect *bounds) {
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 8e2618ecd58..792966ac085 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -42,6 +42,10 @@ public:
~MortimerEngine() override;
Common::Error run() override;
bool hasFeature(EngineFeature feature) const override;
+ Common::String getSaveStateName(int slot) const override;
+ Common::Error loadGameState(int slot) override;
+ bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
+ bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
static Common::KeymapArray initKeymaps();
protected:
@@ -53,6 +57,10 @@ private:
bool loadResources();
bool selectDifficulty();
void setDifficulty(uint flight, uint riddle);
+ bool selectPilot();
+ bool startPilot();
+ Common::Error savePilot();
+ void saveCheckpoint(uint checkpoint);
uint introCount() const;
bool loadIntro(uint index);
bool loadFlight(uint segment);
@@ -103,6 +111,10 @@ private:
const bool _isDemo, _restoredContent;
uint _flightDifficulty, _riddleDifficulty;
+ Common::String _pilotName;
+ int _pilotSlot;
+ uint _checkpoint;
+ bool _girl, _pilotLoadRequested;
Common::RandomSource _random;
MortimerCutscene _intro;
MortimerFlight _flight;
diff --git a/engines/scumm/insane/mortimer/saveload.cpp b/engines/scumm/insane/mortimer/saveload.cpp
new file mode 100644
index 00000000000..5e436eb22f5
--- /dev/null
+++ b/engines/scumm/insane/mortimer/saveload.cpp
@@ -0,0 +1,185 @@
+/* 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 "scumm/insane/mortimer/mortimer.h"
+#include "scumm/insane/mortimer/dialogs.h"
+#include "scumm/insane/saveload.h"
+#include "scumm/scumm.h"
+
+#include "common/config-manager.h"
+#include "common/endian.h"
+#include "common/ptr.h"
+#include "common/savefile.h"
+#include "common/translation.h"
+#include "engines/metaengine.h"
+#include "gui/message.h"
+
+namespace Scumm {
+
+const uint32 kMortimerPilotMagic = MKTAG('M', 'O', 'R', 'P');
+const uint16 kMortimerPilotVersion = 1;
+
+Common::String MortimerEngine::getSaveStateName(int slot) const {
+ return getMetaEngine()->getSavegameFile(slot, _targetName.c_str());
+}
+
+bool MortimerEngine::canLoadGameStateCurrently(Common::U32String *msg) {
+ return !_isDemo && _screen.getPixels();
+}
+
+bool MortimerEngine::canSaveGameStateCurrently(Common::U32String *msg) {
+ if (msg && !_isDemo)
+ *msg = _("Progress is saved automatically when a riddle is solved.");
+ return false;
+}
+
+Common::Error MortimerEngine::savePilot() {
+ if (_isDemo || _pilotSlot < 0)
+ return Common::kNoError;
+ Common::ScopedPtr<Common::OutSaveFile> out(_saveFileMan->openForSaving(getSaveStateName(_pilotSlot), false));
+ if (!out || !writeInsaneSavegameHeader(out.get(), _pilotName, getTotalPlayTime()))
+ return Common::kWritingFailed;
+ out->writeUint32BE(kMortimerPilotMagic);
+ out->writeUint16LE(kMortimerPilotVersion);
+ out->writeByte(_girl);
+ out->writeByte(_flightDifficulty);
+ out->writeByte(_riddleDifficulty);
+ out->writeByte(_checkpoint);
+ out->writeByte(ConfMan.getBool("subtitles"));
+ out->writeByte(_controllerMode);
+ out->finalize();
+ return out->err() ? Common::kWritingFailed : Common::kNoError;
+}
+
+Common::Error MortimerEngine::loadGameState(int slot) {
+ if (_isDemo || slot < 0 || slot > getMetaEngine()->getMaximumSaveSlot())
+ return Common::kReadingFailed;
+ Common::ScopedPtr<Common::InSaveFile> in(_saveFileMan->openForLoading(getSaveStateName(slot)));
+ Common::String name;
+ SaveStateMetaInfos infos;
+ if (!in || !readInsaneSavegameHeader(in.get(), &name, infos) ||
+ in->readUint32BE() != kMortimerPilotMagic || in->readUint16LE() != kMortimerPilotVersion)
+ return Common::kReadingFailed;
+ const byte girl = in->readByte();
+ const byte flight = in->readByte(), riddle = in->readByte(), checkpoint = in->readByte();
+ const byte subtitles = in->readByte(), controller = in->readByte();
+ if (in->err() || in->eos() || name.empty() || girl > 1 || flight > 2 || riddle > 2 ||
+ checkpoint > 5 || subtitles > 1 || controller > 1)
+ return Common::kReadingFailed;
+
+ _pilotName = name;
+ _pilotSlot = slot;
+ _checkpoint = checkpoint;
+ _girl = girl;
+ _controllerMode = controller;
+ ConfMan.setBool("subtitles", subtitles);
+ setDifficulty(flight, riddle);
+ setTotalPlayTime(infos.playtime * 1000);
+ // Leave the menu before switching movies, as in RA2's pilot loading.
+ _pilotLoadRequested = true;
+ return Common::kNoError;
+}
+
+void MortimerEngine::saveCheckpoint(uint checkpoint) {
+ if (_isDemo || _pilotSlot < 0 || checkpoint <= _checkpoint)
+ return;
+ _checkpoint = checkpoint;
+ if (savePilot().getCode() != Common::kNoError) {
+ GUI::MessageDialog message(_("Could not save your pilot. Progress will be saved again when you finish another riddle or quit."));
+ runDialog(message);
+ }
+}
+
+bool MortimerEngine::selectPilot() {
+ while (!shouldQuit()) {
+ const SaveStateList pilots = getMetaEngine()->listSaves(_targetName.c_str());
+ MortimerPilotDialog dialog(pilots);
+ if (runDialog(dialog) != 1 || shouldQuit())
+ return false;
+ if (dialog.selectedPilot() != MortimerPilotDialog::kNewPilot) {
+ const Common::Error result = loadGameState(dialog.selectedPilot());
+ if (result.getCode() == Common::kNoError)
+ return true;
+ GUI::MessageDialog message(result.getTranslatedDesc());
+ runDialog(message);
+ continue;
+ }
+ int slot = 1;
+ while (slot <= getMetaEngine()->getMaximumSaveSlot() && _saveFileMan->exists(getSaveStateName(slot)))
+ ++slot;
+ if (slot > getMetaEngine()->getMaximumSaveSlot()) {
+ GUI::MessageDialog message(_("There are no free pilot slots. Delete a saved pilot from the launcher before creating another."));
+ runDialog(message);
+ continue;
+ }
+ MortimerDifficultyDialog options(false, 0, 0, false, true);
+ if (runDialog(options) != 1 || shouldQuit())
+ continue;
+ _pilotName = options.pilotName();
+ _pilotSlot = slot;
+ _checkpoint = 0;
+ _girl = options.girl();
+ _controllerMode = false;
+ setDifficulty(options.flightDifficulty(), options.riddleDifficulty());
+ setTotalPlayTime();
+ const Common::Error result = savePilot();
+ if (result.getCode() == Common::kNoError) {
+ _pilotLoadRequested = true;
+ return true;
+ }
+ _saveFileMan->removeSavefile(getSaveStateName(slot));
+ _pilotSlot = -1;
+ GUI::MessageDialog message(result.getTranslatedDesc());
+ runDialog(message);
+ }
+ return false;
+}
+
+bool MortimerEngine::startPilot() {
+ _pilotLoadRequested = false;
+ stopSounds();
+ _loadError.clear();
+ _loadedRegion = -1;
+ _finished = _interviewActive = _castleActive = _playingCached = false;
+ _crashStage = kNoCrash;
+ _needsRepair = _hitJolt = _invulnerable = false;
+ _shockFrame = _crashCount = _shellShocks = 0;
+ _carriedStones = _completedSpecies = _creditedRescues = 0;
+ _uiFrame = _glowStones = _pendingGlowStones = 0;
+ _shotFrame = 3;
+ _glowFrame = 10;
+ _pendingFlight.clear();
+ _flightName.clear();
+ memset(_steering, 0, sizeof(_steering));
+ memset(_iconFlash, 0, sizeof(_iconFlash));
+ for (uint i = 0; i < ARRAYSIZE(_steeringHistory); ++i)
+ _steeringHistory[i] = Common::Point();
+ _flight.clearProgress();
+ _castle.clearProgress();
+ _intro.context() = MortimerMovieContext();
+ _intro.context().girl = _flight.context().girl = _girl;
+ _interview.resetRiddle();
+ _programmer = ConfMan.getBool("mortimer_programmer_easter_egg");
+ // SHELL's region entry table resumes the castle at END_1. Completed pilots show credits.
+ const int scenes[] = {24, 1, 2, 3, 34, 39};
+ return _campaign.start(scenes[_checkpoint]) && enterCampaignScene();
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel1/saveload.cpp b/engines/scumm/insane/rebel1/saveload.cpp
index 4bc3cac8c42..6645baaac48 100644
--- a/engines/scumm/insane/rebel1/saveload.cpp
+++ b/engines/scumm/insane/rebel1/saveload.cpp
@@ -23,61 +23,15 @@
#include "common/stream.h"
#include "common/endian.h"
-#include "graphics/thumbnail.h"
-
#include "scumm/scumm.h"
#include "scumm/scumm_v7.h"
+#include "scumm/insane/saveload.h"
#include "scumm/insane/rebel1/rebel.h"
namespace Scumm {
const uint32 kRA1SaveTag = MKTAG('R', 'A', '1', 'S');
const uint32 kRA1SaveVersion = 1;
-const uint32 kRA1ScummSaveVersion = 124;
-
-struct Rebel1SavegameHeader {
- uint32 type;
- uint32 size;
- uint32 version;
- char name[32];
-};
-
-bool writeRebel1SavegameHeader(Common::WriteStream *out, const Common::String &desc) {
- Rebel1SavegameHeader hdr;
- hdr.type = MKTAG('S', 'C', 'V', 'M');
- hdr.size = 0;
- hdr.version = kRA1ScummSaveVersion;
- memset(hdr.name, 0, sizeof(hdr.name));
- Common::strlcpy(hdr.name, desc.c_str(), sizeof(hdr.name));
-
- out->writeUint32BE(hdr.type);
- out->writeUint32LE(hdr.size);
- out->writeUint32LE(hdr.version);
- out->write(hdr.name, sizeof(hdr.name));
- return !out->err();
-}
-
-bool readRebel1SavegameHeader(Common::SeekableReadStream *in, Common::String *desc, uint32 *version) {
- Rebel1SavegameHeader hdr;
- hdr.type = in->readUint32BE();
- hdr.size = in->readUint32LE();
- hdr.version = in->readUint32LE();
- in->read(hdr.name, sizeof(hdr.name));
- if (in->err() || hdr.type != MKTAG('S', 'C', 'V', 'M'))
- return false;
-
- if (hdr.version > 0xFFFFFF)
- hdr.version = SWAP_BYTES_32(hdr.version);
- if (hdr.version < VER(52) || hdr.version > kRA1ScummSaveVersion)
- return false;
-
- hdr.name[sizeof(hdr.name) - 1] = 0;
- if (desc)
- *desc = hdr.name;
- if (version)
- *version = hdr.version;
- return true;
-}
int InsaneRebel1::getCurrentSaveLevel() const {
return CLIP<int>(_resumeLevel, 1, kNumLevels);
@@ -93,9 +47,10 @@ Common::Error InsaneRebel1::writeSaveState(int slot, const Common::String &desc,
if (!out)
return Common::kWritingFailed;
- writeRebel1SavegameHeader(out, desc);
- Graphics::saveThumbnail(*out);
- _vm->saveInfos(out);
+ if (!writeInsaneSavegameHeader(out, desc, _vm->getTotalPlayTime())) {
+ delete out;
+ return Common::kWritingFailed;
+ }
out->writeUint32BE(kRA1SaveTag);
out->writeUint32LE(kRA1SaveVersion);
@@ -126,27 +81,13 @@ bool InsaneRebel1::readSaveState(int slot, SaveState &state, Common::String *des
if (!in)
return false;
- uint32 headerVersion = 0;
- if (!readRebel1SavegameHeader(in.get(), desc, &headerVersion)) {
+ SaveStateMetaInfos infos;
+ if (!readInsaneSavegameHeader(in.get(), desc, infos)) {
warning("RA1: invalid save header in '%s'", filename.c_str());
return false;
}
- if (headerVersion >= VER(52) && !Graphics::skipThumbnail(*in)) {
- warning("RA1: save thumbnail could not be skipped in '%s'", filename.c_str());
- return false;
- }
-
- if (headerVersion >= VER(56)) {
- SaveStateMetaInfos infos;
- if (!_vm->loadInfos(in.get(), &infos)) {
- warning("RA1: save info section could not be found in '%s'", filename.c_str());
- return false;
- }
- _vm->setTotalPlayTime(infos.playtime * 1000);
- } else {
- _vm->setTotalPlayTime();
- }
+ _vm->setTotalPlayTime(infos.playtime * 1000);
if (in->readUint32BE() != kRA1SaveTag) {
warning("RA1: missing RA1 save data in '%s'", filename.c_str());
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 9e0e2ec0b06..c70d29c3646 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -36,6 +36,7 @@
#include "scumm/actor.h"
#include "scumm/file.h"
#include "scumm/resource.h"
+#include "scumm/insane/saveload.h"
#include "scumm/scumm_v7.h"
#include "scumm/sound.h"
@@ -1407,51 +1408,6 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
const uint16 kPilotSaveVersion = 2;
-// Pilots occupy the regular ScummVM save slots, so they need the standard SCUMM
-// header too: without it every slot reads as an invalid savegame.
-const uint32 kPilotScummSaveVersion = 124;
-
-bool writeRebel2SavegameHeader(Common::WriteStream *out, const Common::String &desc) {
- char name[32];
- memset(name, 0, sizeof(name));
- Common::strlcpy(name, desc.c_str(), sizeof(name));
-
- out->writeUint32BE(MKTAG('S', 'C', 'V', 'M'));
- out->writeUint32LE(0);
- out->writeUint32LE(kPilotScummSaveVersion);
- out->write(name, sizeof(name));
- return !out->err();
-}
-
-// A stream starting with the pilot magic is a pre-header file; rewind it.
-bool readRebel2SavegameHeader(Common::SeekableReadStream *in, uint32 *version) {
- const uint32 tag = in->readUint32BE();
- if (tag == kPilotSaveMagic) {
- in->seek(0);
- if (version)
- *version = 0;
- return true;
- }
- if (tag != MKTAG('S', 'C', 'V', 'M'))
- return false;
-
- in->readUint32LE(); // size, unused
- uint32 hdrVersion = in->readUint32LE();
- if (hdrVersion > 0xFFFFFF)
- hdrVersion = SWAP_BYTES_32(hdrVersion);
- if (hdrVersion < VER(52) || hdrVersion > kPilotScummSaveVersion)
- return false;
-
- char name[32];
- in->read(name, sizeof(name));
- if (in->err())
- return false;
-
- if (version)
- *version = hdrVersion;
- return true;
-}
-
bool InsaneRebel2::loadPilots() {
_numPilots = 0;
@@ -1461,25 +1417,15 @@ bool InsaneRebel2::loadPilots() {
if (!sf)
break; // Slots are contiguous
- uint32 scummVersion = 0;
- if (!readRebel2SavegameHeader(sf, &scummVersion)) {
+ SaveStateMetaInfos infos;
+ // Early pilot files have no SCUMM envelope.
+ const bool legacy = sf->readUint32BE() == kPilotSaveMagic;
+ sf->seek(0);
+ if (!legacy && !readInsaneSavegameHeader(sf, nullptr, infos)) {
delete sf;
break;
}
- // Only present once the slot carries a SCUMM header.
- if (scummVersion >= VER(52) && !Graphics::skipThumbnail(*sf)) {
- delete sf;
- break;
- }
- if (scummVersion >= VER(56)) {
- SaveStateMetaInfos infos;
- if (!_vm->loadInfos(sf, &infos)) {
- delete sf;
- break;
- }
- }
-
uint32 magic = sf->readUint32BE();
if (magic != kPilotSaveMagic) {
delete sf;
@@ -1527,9 +1473,11 @@ bool InsaneRebel2::savePilots() {
Common::String desc(_pilots[i].name);
if (desc.empty())
desc = Common::String::format("Pilot %d", i + 1);
- writeRebel2SavegameHeader(sf, desc);
- Graphics::saveThumbnail(*sf);
- _vm->saveInfos(sf);
+ if (!writeInsaneSavegameHeader(sf, desc, _vm->getTotalPlayTime())) {
+ delete sf;
+ ok = false;
+ continue;
+ }
sf->writeUint32BE(kPilotSaveMagic);
sf->writeUint16LE(kPilotSaveVersion);
@@ -1547,6 +1495,7 @@ bool InsaneRebel2::savePilots() {
sf->writeSint16LE(p.rating[j]);
sf->finalize();
+ ok = !sf->err() && ok;
delete sf;
}
diff --git a/engines/scumm/insane/saveload.cpp b/engines/scumm/insane/saveload.cpp
new file mode 100644
index 00000000000..67be54bb9db
--- /dev/null
+++ b/engines/scumm/insane/saveload.cpp
@@ -0,0 +1,92 @@
+/* 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 "scumm/insane/saveload.h"
+
+#include "common/endian.h"
+#include "common/stream.h"
+#include "common/system.h"
+#include "graphics/thumbnail.h"
+#include "scumm/scumm.h"
+
+namespace Scumm {
+
+const uint32 kInsaneScummSaveVersion = 124;
+
+bool writeInsaneSavegameHeader(Common::WriteStream *out, const Common::String &desc, uint32 playTime) {
+ char name[32] = {};
+ Common::strlcpy(name, desc.c_str(), sizeof(name));
+ out->writeUint32BE(MKTAG('S', 'C', 'V', 'M'));
+ out->writeUint32LE(0);
+ out->writeUint32LE(kInsaneScummSaveVersion);
+ out->write(name, sizeof(name));
+ if (!Graphics::saveThumbnail(*out))
+ return false;
+
+ TimeDate now;
+ g_system->getTimeAndDate(now);
+ out->writeUint32BE(MKTAG('I', 'N', 'F', 'O'));
+ out->writeUint32BE(2);
+ out->writeUint32BE(26);
+ out->writeUint32BE(0);
+ out->writeUint32BE(playTime / 1000);
+ out->writeUint32BE(((now.tm_mday & 0xff) << 24) | (((now.tm_mon + 1) & 0xff) << 16) | ((now.tm_year + 1900) & 0xffff));
+ out->writeUint16BE(((now.tm_hour & 0xff) << 8) | (now.tm_min & 0xff));
+ return !out->err();
+}
+
+bool readInsaneSavegameHeader(Common::SeekableReadStream *in, Common::String *desc, SaveStateMetaInfos &infos) {
+ memset(&infos, 0, sizeof(infos));
+ if (in->readUint32BE() != MKTAG('S', 'C', 'V', 'M'))
+ return false;
+ in->readUint32LE();
+ uint32 version = in->readUint32LE();
+ if (version > 0xffffff)
+ version = SWAP_BYTES_32(version);
+ if (version < 52 || version > kInsaneScummSaveVersion)
+ return false;
+ char name[32];
+ if (in->read(name, sizeof(name)) != sizeof(name))
+ return false;
+ name[sizeof(name) - 1] = 0;
+ if (desc)
+ *desc = name;
+ if (!Graphics::skipThumbnail(*in))
+ return false;
+ if (version >= 56) {
+ const int64 infoStart = in->pos();
+ if (in->readUint32BE() != MKTAG('I', 'N', 'F', 'O'))
+ return false;
+ const uint32 infoVersion = in->readUint32BE(), size = in->readUint32BE();
+ if (!infoVersion || size < (infoVersion == 1 ? 20U : 26U) || size > in->size() - infoStart)
+ return false;
+ in->readUint32BE();
+ infos.playtime = in->readUint32BE();
+ if (infoVersion >= 2) {
+ infos.date = in->readUint32BE();
+ infos.time = in->readUint16BE();
+ }
+ if (!in->seek(infoStart + size))
+ return false;
+ }
+ return !in->err() && !in->eos();
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/saveload.h b/engines/scumm/insane/saveload.h
new file mode 100644
index 00000000000..ec63cc842a0
--- /dev/null
+++ b/engines/scumm/insane/saveload.h
@@ -0,0 +1,41 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_SAVELOAD_H
+#define SCUMM_INSANE_SAVELOAD_H
+
+#include "common/str.h"
+
+namespace Common {
+class SeekableReadStream;
+class WriteStream;
+}
+
+namespace Scumm {
+
+struct SaveStateMetaInfos;
+
+// The movie games share the SCUMM save envelope and supply their own payloads.
+bool writeInsaneSavegameHeader(Common::WriteStream *out, const Common::String &desc, uint32 playTime);
+bool readInsaneSavegameHeader(Common::SeekableReadStream *in, Common::String *desc, SaveStateMetaInfos &infos);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 4ae124ae527..55e4d6decc4 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -147,6 +147,7 @@ MODULE_OBJS += \
insane/insane_enemy.o \
insane/insane_scenes.o \
insane/insane_iact.o \
+ insane/saveload.o \
insane/mortimer/campaign.o \
insane/mortimer/castle.o \
insane/mortimer/dialogs.o \
@@ -154,6 +155,7 @@ MODULE_OBJS += \
insane/mortimer/interview.o \
insane/mortimer/movie.o \
insane/mortimer/mortimer.o \
+ insane/mortimer/saveload.o \
insane/mortimer/text.o \
insane/rebel/rebel_audio.o \
insane/rebel/rebel_gamepad.o \
Commit: 81d3f76d302b9c28be438dfab4f76a93b2e9c662
https://github.com/scummvm/scummvm/commit/81d3f76d302b9c28be438dfab4f76a93b2e9c662
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-10-14T20:54:43+02:00
Commit Message:
SCUMM: MORTIMER: added support for high score save/load
Changed paths:
A engines/scumm/insane/highscores.h
A engines/scumm/insane/mortimer/scores.cpp
A engines/scumm/insane/mortimer/scores.h
engines/scumm/insane/mortimer/dialogs.cpp
engines/scumm/insane/mortimer/dialogs.h
engines/scumm/insane/mortimer/interview.cpp
engines/scumm/insane/mortimer/interview.h
engines/scumm/insane/mortimer/mortimer.cpp
engines/scumm/insane/mortimer/mortimer.h
engines/scumm/insane/mortimer/saveload.cpp
engines/scumm/insane/mortimer/text.cpp
engines/scumm/insane/mortimer/text.h
engines/scumm/insane/rebel1/menu.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/module.mk
diff --git a/engines/scumm/insane/highscores.h b/engines/scumm/insane/highscores.h
new file mode 100644
index 00000000000..3061d25ffd5
--- /dev/null
+++ b/engines/scumm/insane/highscores.h
@@ -0,0 +1,50 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_HIGHSCORES_H
+#define SCUMM_INSANE_HIGHSCORES_H
+
+#include "common/util.h"
+
+namespace Scumm {
+
+template<class Entry, class Score>
+int findHighScorePosition(const Entry *entries, int count, Score score, bool beforeEqual = false) {
+ int position = 0;
+ while (position < count && (entries[position].score > score ||
+ (!beforeEqual && entries[position].score == score)))
+ ++position;
+ return position;
+}
+
+template<class Entry>
+int insertHighScoreEntry(Entry *entries, int count, int capacity, int position, const Entry &entry) {
+ assert(count >= 0 && count <= capacity && position >= 0 && position <= count);
+ if (position >= capacity)
+ return count;
+ for (int i = MIN(count, capacity - 1); i > position; --i)
+ entries[i] = entries[i - 1];
+ entries[position] = entry;
+ return MIN(count + 1, capacity);
+}
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/dialogs.cpp b/engines/scumm/insane/mortimer/dialogs.cpp
index 7390371caff..6eb4c104e15 100644
--- a/engines/scumm/insane/mortimer/dialogs.cpp
+++ b/engines/scumm/insane/mortimer/dialogs.cpp
@@ -30,6 +30,8 @@
namespace Scumm {
+const uint32 kMortimerHighScoresCmd = 'MHSC';
+
MortimerDifficultyDialog::MortimerDifficultyDialog(bool isDemo, uint flightDifficulty, uint riddleDifficulty, bool girl, bool newPilot)
: GUI::Dialog("MortimerDifficultyDialog"), _character(nullptr), _pilotName(nullptr) {
new GUI::StaticTextWidget(this, _name + ".Title", newPilot ? _("New pilot") : _("Select difficulty"));
@@ -132,6 +134,7 @@ MortimerPilotDialog::MortimerPilotDialog(const SaveStateList &pilots) : GUI::Dia
for (uint i = 0; i < pilots.size(); ++i)
_pilot->appendEntry(pilots[i].getDescription(), pilots[i].getSaveSlot());
_pilot->setSelected(pilots.empty() ? 0 : 1);
+ new GUI::ButtonWidget(this, _name + ".HighScores", _("High Scores"), Common::U32String(), kMortimerHighScoresCmd);
new GUI::ButtonWidget(this, _name + ".Cancel", _("~C~ancel"), Common::U32String(), GUI::kCloseCmd);
new GUI::ButtonWidget(this, _name + ".Start", _("Start"), Common::U32String(), GUI::kOKCmd, Common::ASCII_RETURN);
}
@@ -150,6 +153,7 @@ void MortimerPilotDialog::reflowLayout() {
.addWidget("Title", "", width, lineHeight, Graphics::kTextAlignCenter)
.addWidget("Pilot", "PopUp")
.addWidget("Hint", "", -1, lineHeight, Graphics::kTextAlignCenter)
+ .addWidget("HighScores", "Button")
.addLayout(GUI::ThemeLayout::kLayoutHorizontal, 8, GUI::ThemeLayout::kItemAlignEnd)
.addPadding(0, 0, 0, 0)
.addSpace()
@@ -162,8 +166,8 @@ void MortimerPilotDialog::reflowLayout() {
}
void MortimerPilotDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
- if (cmd == GUI::kOKCmd) {
- setResult(1);
+ if (cmd == GUI::kOKCmd || cmd == kMortimerHighScoresCmd) {
+ setResult(cmd == GUI::kOKCmd ? kStartPilot : kViewHighScores);
close();
} else {
GUI::Dialog::handleCommand(sender, cmd, data);
diff --git a/engines/scumm/insane/mortimer/dialogs.h b/engines/scumm/insane/mortimer/dialogs.h
index b7233e1950f..29bcd5d2048 100644
--- a/engines/scumm/insane/mortimer/dialogs.h
+++ b/engines/scumm/insane/mortimer/dialogs.h
@@ -51,7 +51,7 @@ private:
class MortimerPilotDialog : public GUI::Dialog {
public:
- enum { kNewPilot = 100 };
+ enum { kStartPilot = 1, kViewHighScores = 2, kNewPilot = 100 };
explicit MortimerPilotDialog(const SaveStateList &pilots);
int selectedPilot() const;
void reflowLayout() override;
diff --git a/engines/scumm/insane/mortimer/interview.cpp b/engines/scumm/insane/mortimer/interview.cpp
index 605fed8fea7..c3b6fe40c9a 100644
--- a/engines/scumm/insane/mortimer/interview.cpp
+++ b/engines/scumm/insane/mortimer/interview.cpp
@@ -67,7 +67,7 @@ const Common::Point kQuestionOrigins[] = {{122, 191}, {120, 234}, {124, 278}};
MortimerInterview::MortimerInterview(bool isDemo) : _random("mortimer"), _isDemo(isDemo),
_region(0), _difficulty(0), _textDifficulty(0), _tried(0), _stage(kIdle), _effect(kNoEffect),
_answer(-1), _previousAnswer(-1), _selected(-1), _question(0), _guess(-1), _lastAction(46), _textScroll(0), _guesses(3),
- _species(0), _stones(0), _frame(0), _switchFrame(6), _payment(0),
+ _species(0), _stones(0), _frame(0), _switchFrame(6), _payment(0), _bonusStones(0),
_moviePending(false), _riddleView(false), _solving(false), _help(false), _helpExplained(false), _restartRegion(false), _programmer(false) {
memset(_palette, 0, sizeof(_palette));
memset(_gatePalette, 0, sizeof(_gatePalette));
@@ -206,7 +206,7 @@ void MortimerInterview::start(uint species, uint stones, int scene) {
if (_isDemo || _answer < 0)
_guesses = _isDemo ? 3 : mortimerGuessAllowance(_difficulty);
_restartRegion = false;
- _frame = _payment = 0;
+ _frame = _payment = _bonusStones = 0;
_switchFrame = 6;
_selected = -1;
_question = _textScroll = 0;
@@ -341,7 +341,7 @@ void MortimerInterview::selectAnimal(int animal) {
}
void MortimerInterview::click(const Common::Point &point) {
- if (fullScreen() || _stage == kPay || _stage == kGuess || _stage == kCorrect || _stage == kWrong || _stage == kSolve || _stage == kNoStones || _stage == kCost || _stage == kNoAnimals)
+ if (fullScreen() || _stage == kPay || _stage == kBonus || _stage == kGuess || _stage == kCorrect || _stage == kWrong || _stage == kSolve || _stage == kNoStones || _stage == kCost || _stage == kNoAnimals)
return;
int action = hitControl(point);
if (action < 0)
@@ -428,6 +428,12 @@ MortimerInterview::Effect MortimerInterview::takeEffect() {
return result;
}
+uint MortimerInterview::takeBonusStones() {
+ const uint result = _bonusStones;
+ _bonusStones = 0;
+ return result;
+}
+
void MortimerInterview::update() {
++_frame;
if (_switchFrame < 6)
@@ -439,6 +445,19 @@ void MortimerInterview::update() {
if (!_payment)
enter(kGuess);
}
+ if (_stage == kBonus && (_frame % 3) == 0) {
+ if (_stones) {
+ const uint count = MIN(3U, _stones);
+ _stones -= count;
+ _bonusStones += count;
+ _effect = kPayoutEffect;
+ _payment = 15;
+ } else if (_payment > 3) {
+ _payment -= 3;
+ } else {
+ enter(kOpenGate);
+ }
+ }
}
void MortimerInterview::movieFinished() {
@@ -460,7 +479,11 @@ void MortimerInterview::movieFinished() {
}
break;
case kGuess: enter(_guess == _answer ? kCorrect : kWrong); break;
- case kCorrect: enter(kOpenGate); break;
+ case kCorrect:
+ enter(_isDemo ? kOpenGate : kBonus);
+ _frame = 0;
+ _payment = 15;
+ break;
case kWrong:
if (!_guesses) {
_restartRegion = !_isDemo;
@@ -479,7 +502,7 @@ void MortimerInterview::movieFinished() {
void MortimerInterview::skip() {
if (playingMovie())
movieFinished();
- else if (_stage != kPay)
+ else if (_stage != kPay && _stage != kBonus)
enter(kFlyAway);
}
@@ -538,7 +561,7 @@ void MortimerInterview::draw(Graphics::Surface &screen, const MortimerMovie &mov
void MortimerInterview::drawCursor(Graphics::Surface &screen, const Common::Point &point) const {
if (!fullScreen()) {
- if (_stage == kPay)
+ if (_stage == kPay || (_stage == kBonus && _stones))
_panel[19 + _frame % 2].draw(screen, 508, 451);
const int cursor = (_help ? 2 : 0) + (hitControl(point) < 0 ? 1 : 0);
_cursor[cursor].draw(screen, point.x, point.y);
diff --git a/engines/scumm/insane/mortimer/interview.h b/engines/scumm/insane/mortimer/interview.h
index a3a87c37fd6..7288f3f3e90 100644
--- a/engines/scumm/insane/mortimer/interview.h
+++ b/engines/scumm/insane/mortimer/interview.h
@@ -60,7 +60,10 @@ public:
bool movieDoubled() const;
bool wantsFlight() const { return _stage == kFlyAway; }
bool finished() const { return _stage == kFinished; }
- bool solved() const { return _stage == kCorrect || _stage == kOpenGate || _stage == kFinished; }
+ bool solved() const { return _stage == kCorrect || _stage == kBonus || _stage == kOpenGate || _stage == kFinished; }
+ bool payingBonus() const { return _stage == kBonus; }
+ bool bonusFinished() const { return _stage == kOpenGate || _stage == kFinished; }
+ uint takeBonusStones();
bool restartsRegion() const { return _restartRegion; }
int sceneId() const;
int riddleId() const { return _answer * 10 + _textDifficulty; }
@@ -76,7 +79,7 @@ public:
private:
enum Stage {
kGate, kSong, kPopup, kIdle, kAnswer, kRiddle, kSolve,
- kNoStones, kPay, kGuess, kCorrect, kWrong, kOpenGate,
+ kNoStones, kPay, kGuess, kCorrect, kBonus, kWrong, kOpenGate,
kEnding, kHelp, kFlyAway, kFinished, kCost, kNoAnimals, kTried
};
@@ -98,7 +101,7 @@ private:
Stage _stage;
Effect _effect;
int _answer, _previousAnswer, _selected, _question, _guess, _lastAction, _textScroll, _guesses;
- uint _species, _stones, _frame, _switchFrame, _payment;
+ uint _species, _stones, _frame, _switchFrame, _payment, _bonusStones;
bool _moviePending, _riddleView, _solving, _help, _helpExplained, _restartRegion, _programmer;
};
diff --git a/engines/scumm/insane/mortimer/mortimer.cpp b/engines/scumm/insane/mortimer/mortimer.cpp
index f0afc121021..2fac84cc856 100644
--- a/engines/scumm/insane/mortimer/mortimer.cpp
+++ b/engines/scumm/insane/mortimer/mortimer.cpp
@@ -23,6 +23,7 @@
#include "audio/audiostream.h"
#include "backends/keymapper/action.h"
+#include "backends/keymapper/keymapper.h"
#include "common/config-manager.h"
#include "common/endian.h"
#include "common/events.h"
@@ -115,7 +116,8 @@ Common::KeymapArray MortimerEngine::initKeymaps() {
MortimerEngine::MortimerEngine(OSystem *system, bool isDemo) : Engine(system), _isDemo(isDemo),
_restoredContent(isDemo && ConfMan.getBool("mortimer_restored_content")), _flightDifficulty(0), _riddleDifficulty(0),
- _pilotSlot(-1), _checkpoint(0), _girl(false), _pilotLoadRequested(false),
+ _pilotSlot(-1), _checkpoint(0), _score(0), _checkpointScore(0), _girl(false), _pilotLoadRequested(false),
+ _highScoresActive(false), _highScoreEligible(false), _viewingHighScores(false),
_random("mortimer-flight"), _flight(isDemo, _restoredContent), _interview(isDemo),
_viewport(24, 20, 616, 360), _mouse(320, 240), _view(44, 60), _aim(320, 190), _shotWeapon(MortimerFlight::kRevitascope),
_crashStage(kNoCrash), _nextFrameTime(0), _pauseTime(0), _introIndex(-1), _loadedRegion(-1), _creditedRescues(0),
@@ -372,6 +374,10 @@ void MortimerEngine::updateMovieContext() {
bool MortimerEngine::enterCampaignScene() {
stopSounds();
+ if (_highScoresActive) {
+ setScoreNameInput(false);
+ _highScoresActive = false;
+ }
const MortimerCampaign::Scene &scene = _campaign.scene();
_interviewActive = false;
_castleActive = false;
@@ -423,9 +429,36 @@ bool MortimerEngine::enterCampaignScene() {
}
_introIndex = scene.id;
_movieHandles.resize(_intro.sounds().size());
+ if (scene.kind == MortimerCampaign::kCredits) {
+ if (!_highScores.load(*_saveFileMan, _targetName + ".hsf")) {
+ _loadError = "Missing or invalid Mortimer high-score resources";
+ return false;
+ }
+ _highScores.start(_score, _highScoreEligible);
+ _highScoreEligible = false;
+ _highScoresActive = true;
+ setScoreNameInput(_highScores.enteringName());
+ }
return startMovie();
}
+void MortimerEngine::setScoreNameInput(bool enabled) {
+ _eventMan->getKeymapper()->setGameKeymapState("mortimer", !enabled);
+ if (_system->hasFeature(OSystem::kFeatureVirtualKeyboard))
+ _system->setFeatureState(OSystem::kFeatureVirtualKeyboard, enabled);
+}
+
+bool MortimerEngine::finishHighScores() {
+ setScoreNameInput(false);
+ _highScoresActive = false;
+ if (_viewingHighScores) {
+ _finished = true;
+ return true;
+ }
+ _nextFrameTime = _system->getMillis() + 1000 / currentMovie().frameRate();
+ return loadSubtitles("RESOURCE/DATA/CREDITS.TRS");
+}
+
void MortimerEngine::playSound(const MortimerSound &sound, Audio::SoundHandle &handle, Audio::Mixer::SoundType type, uint64 skipFrames) {
_mixer->stopHandle(handle);
SaudStream *stream = new SaudStream(sound.samples, sound.script, sound.rate, _soundState, sound.volume, sound.balance);
@@ -577,7 +610,10 @@ bool MortimerEngine::restart(bool all) {
_shockFrame = _crashCount = 0;
if (!_isDemo) {
_finished = false;
+ _score = _checkpointScore;
if (all) {
+ _score = _checkpointScore = _checkpoint = 0;
+ _highScoreEligible = false;
_loadedRegion = -1;
_campaign.start();
return enterCampaignScene();
@@ -697,8 +733,15 @@ bool MortimerEngine::nextScene(bool skip) {
}
bool MortimerEngine::processInterview() {
- if (!_isDemo && _interview.solved())
- saveCheckpoint(_loadedRegion + 1);
+ const uint bonusStones = _interview.takeBonusStones();
+ if (!_isDemo && _interview.solved() && (int)_checkpoint <= _loadedRegion) {
+ const MortimerScoring &scoring = mortimerScoring(_flightDifficulty, _riddleDifficulty);
+ if (!_score)
+ _score = scoring.initial;
+ _score += bonusStones * scoring.stone;
+ if (_interview.bonusFinished())
+ saveCheckpoint(_loadedRegion + 1);
+ }
if (_interview.wantsFlight()) {
_carriedStones = _interview.stones();
_completedSpecies = _interview.species();
@@ -770,8 +813,11 @@ bool MortimerEngine::processCastle() {
_loadError = _castle.errorMessage();
return false;
}
- if (_castle.solved())
+ if (_castle.solved() && _checkpoint < 5) {
+ _score += mortimerScoring(_flightDifficulty, _riddleDifficulty).castle;
saveCheckpoint(5);
+ _highScoreEligible = true;
+ }
if (_castle.finished()) {
stopSounds();
_castleActive = false;
@@ -842,6 +888,8 @@ bool MortimerEngine::updateCastle() {
}
bool MortimerEngine::updateScene() {
+ if (_highScoresActive)
+ return true;
if (_castleActive)
return updateCastle();
if (_interviewActive) {
@@ -1153,6 +1201,23 @@ Common::Error MortimerEngine::run() {
}
Common::Event event;
while (_eventMan->pollEvent(event)) {
+ if (_highScoresActive && !isPaused()) {
+ if (_highScores.enteringName()) {
+ if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat && _highScores.handleKey(event.kbd)) {
+ if (_highScores.save(*_saveFileMan, _targetName + ".hsf")) {
+ setScoreNameInput(false);
+ } else {
+ GUI::MessageDialog message(_("Could not save the high scores. Please try again."));
+ runDialog(message);
+ }
+ }
+ } else if ((event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat) ||
+ event.type == Common::EVENT_LBUTTONDOWN || event.type == Common::EVENT_RBUTTONDOWN ||
+ event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START) {
+ exit = !finishHighScores();
+ }
+ continue;
+ }
if (event.type == Common::EVENT_KEYDOWN) {
if (event.kbdRepeat)
continue;
@@ -1248,9 +1313,12 @@ Common::Error MortimerEngine::run() {
}
draw();
if (_finished) {
- GUI::MessageDialog dialog(_("Play again?"), _("Restart"), _("Quit"));
- if (runDialog(dialog) != GUI::kMessageOK)
- break;
+ if (!_viewingHighScores) {
+ GUI::MessageDialog dialog(_("Play again?"), _("Restart"), _("Quit"));
+ if (runDialog(dialog) != GUI::kMessageOK)
+ break;
+ }
+ _viewingHighScores = false;
if (_isDemo) {
if (!selectDifficulty() || !restart(true))
break;
@@ -1261,6 +1329,8 @@ Common::Error MortimerEngine::run() {
_system->delayMillis(10);
}
stopSounds();
+ if (_highScoresActive)
+ setScoreNameInput(false);
CursorMan.showMouse(oldCursor);
if (!_loadError.empty())
warning("Mortimer: %s", _loadError.c_str());
@@ -1395,9 +1465,14 @@ void MortimerEngine::draw() {
if (_castleActive)
_castle.drawOverlay(_screen, _mouse, Common::Point(), _soundState.variable(1));
}
- if (!_isDemo)
+ if (_highScoresActive) {
+ _highScores.draw(_screen, _system->getMillis());
+ } else if (!_isDemo) {
_subtitles.draw(_screen, currentMovie().captions(), ConfMan.getBool("subtitles") ||
(_interviewActive && _interview.forceSubtitles()) || (_castleActive && _castle.forceSubtitles()), &_intro.context());
+ if (_interviewActive && _interview.payingBonus())
+ _subtitles.drawScore(_screen, _score);
+ }
if (isPaused()) {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
_screen.fillRect(Common::Rect(90, 142, 550, 182), 10);
diff --git a/engines/scumm/insane/mortimer/mortimer.h b/engines/scumm/insane/mortimer/mortimer.h
index 792966ac085..0e51e33d958 100644
--- a/engines/scumm/insane/mortimer/mortimer.h
+++ b/engines/scumm/insane/mortimer/mortimer.h
@@ -30,6 +30,7 @@
#include "scumm/insane/mortimer/interview.h"
#include "scumm/insane/mortimer/campaign.h"
#include "scumm/insane/mortimer/castle.h"
+#include "scumm/insane/mortimer/scores.h"
#include "scumm/smush/saud_stream.h"
namespace Scumm {
@@ -61,6 +62,8 @@ private:
bool startPilot();
Common::Error savePilot();
void saveCheckpoint(uint checkpoint);
+ void setScoreNameInput(bool enabled);
+ bool finishHighScores();
uint introCount() const;
bool loadIntro(uint index);
bool loadFlight(uint segment);
@@ -114,7 +117,10 @@ private:
Common::String _pilotName;
int _pilotSlot;
uint _checkpoint;
+ uint32 _score, _checkpointScore;
bool _girl, _pilotLoadRequested;
+ bool _highScoresActive, _highScoreEligible, _viewingHighScores;
+ MortimerHighScores _highScores;
Common::RandomSource _random;
MortimerCutscene _intro;
MortimerFlight _flight;
diff --git a/engines/scumm/insane/mortimer/saveload.cpp b/engines/scumm/insane/mortimer/saveload.cpp
index 5e436eb22f5..e44ce2ea127 100644
--- a/engines/scumm/insane/mortimer/saveload.cpp
+++ b/engines/scumm/insane/mortimer/saveload.cpp
@@ -34,7 +34,7 @@
namespace Scumm {
const uint32 kMortimerPilotMagic = MKTAG('M', 'O', 'R', 'P');
-const uint16 kMortimerPilotVersion = 1;
+const uint16 kMortimerPilotVersion = 2;
Common::String MortimerEngine::getSaveStateName(int slot) const {
return getMetaEngine()->getSavegameFile(slot, _targetName.c_str());
@@ -64,6 +64,8 @@ Common::Error MortimerEngine::savePilot() {
out->writeByte(_checkpoint);
out->writeByte(ConfMan.getBool("subtitles"));
out->writeByte(_controllerMode);
+ // A quit during the glowstone payout must not save uncompleted-region points.
+ out->writeUint32LE(_checkpointScore);
out->finalize();
return out->err() ? Common::kWritingFailed : Common::kNoError;
}
@@ -80,13 +82,16 @@ Common::Error MortimerEngine::loadGameState(int slot) {
const byte girl = in->readByte();
const byte flight = in->readByte(), riddle = in->readByte(), checkpoint = in->readByte();
const byte subtitles = in->readByte(), controller = in->readByte();
+ const uint32 score = in->readUint32LE();
if (in->err() || in->eos() || name.empty() || girl > 1 || flight > 2 || riddle > 2 ||
checkpoint > 5 || subtitles > 1 || controller > 1)
return Common::kReadingFailed;
_pilotName = name;
_pilotSlot = slot;
+ _viewingHighScores = false;
_checkpoint = checkpoint;
+ _score = _checkpointScore = score;
_girl = girl;
_controllerMode = controller;
ConfMan.setBool("subtitles", subtitles);
@@ -98,9 +103,10 @@ Common::Error MortimerEngine::loadGameState(int slot) {
}
void MortimerEngine::saveCheckpoint(uint checkpoint) {
- if (_isDemo || _pilotSlot < 0 || checkpoint <= _checkpoint)
+ if (_isDemo || checkpoint <= _checkpoint)
return;
_checkpoint = checkpoint;
+ _checkpointScore = _score;
if (savePilot().getCode() != Common::kNoError) {
GUI::MessageDialog message(_("Could not save your pilot. Progress will be saved again when you finish another riddle or quit."));
runDialog(message);
@@ -111,7 +117,14 @@ bool MortimerEngine::selectPilot() {
while (!shouldQuit()) {
const SaveStateList pilots = getMetaEngine()->listSaves(_targetName.c_str());
MortimerPilotDialog dialog(pilots);
- if (runDialog(dialog) != 1 || shouldQuit())
+ const int choice = runDialog(dialog);
+ if (shouldQuit())
+ return false;
+ if (choice == MortimerPilotDialog::kViewHighScores) {
+ _viewingHighScores = _pilotLoadRequested = true;
+ return true;
+ }
+ if (choice != MortimerPilotDialog::kStartPilot)
return false;
if (dialog.selectedPilot() != MortimerPilotDialog::kNewPilot) {
const Common::Error result = loadGameState(dialog.selectedPilot());
@@ -134,7 +147,9 @@ bool MortimerEngine::selectPilot() {
continue;
_pilotName = options.pilotName();
_pilotSlot = slot;
+ _viewingHighScores = false;
_checkpoint = 0;
+ _score = _checkpointScore = 0;
_girl = options.girl();
_controllerMode = false;
setDifficulty(options.flightDifficulty(), options.riddleDifficulty());
@@ -154,6 +169,10 @@ bool MortimerEngine::selectPilot() {
bool MortimerEngine::startPilot() {
_pilotLoadRequested = false;
+ _score = _checkpointScore;
+ _highScoreEligible = false;
+ if (_viewingHighScores)
+ return _campaign.start(39) && enterCampaignScene();
stopSounds();
_loadError.clear();
_loadedRegion = -1;
diff --git a/engines/scumm/insane/mortimer/scores.cpp b/engines/scumm/insane/mortimer/scores.cpp
new file mode 100644
index 00000000000..9192801fedb
--- /dev/null
+++ b/engines/scumm/insane/mortimer/scores.cpp
@@ -0,0 +1,168 @@
+/* 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 "scumm/insane/mortimer/scores.h"
+#include "scumm/insane/highscores.h"
+
+#include "common/file.h"
+#include "common/ptr.h"
+#include "common/savefile.h"
+#include "graphics/surface.h"
+
+namespace Scumm {
+
+const MortimerScoring kMortimerScoring[] = {
+ {100, 0, 2000}, {1000, 3000, 12000}, {3500, 75000, 75000}
+};
+
+const MortimerScoring &mortimerScoring(uint flightDifficulty, uint riddleDifficulty) {
+ // Scoring uses the integer mean of the two difficulty settings.
+ return kMortimerScoring[(MIN(flightDifficulty, 2U) + MIN(riddleDifficulty, 2U)) / 2];
+}
+
+bool MortimerHighScores::read(Common::SeekableReadStream &stream) {
+ if (stream.size() != 240)
+ return false;
+ for (uint i = 0; i < ARRAYSIZE(_entries); ++i) {
+ Entry &entry = _entries[i];
+ if (stream.read(entry.name, sizeof(entry.name)) != sizeof(entry.name) ||
+ !memchr(entry.name, 0, sizeof(entry.name)))
+ return false;
+ entry.score = stream.readUint32LE();
+ if (stream.err() || stream.eos() || (i && entry.score > _entries[i - 1].score))
+ return false;
+ }
+ return true;
+}
+
+bool MortimerHighScores::load(Common::SaveFileManager &saveFileMan, const Common::String &fileName) {
+ Common::File text;
+ if (!text.open("RESOURCE/DATA/HISCORE.TRS") || !_text.loadText(text) ||
+ !_font.load("RESOURCE/GENERAL/ESTRO20.NUT"))
+ return false;
+ Common::ScopedPtr<Common::InSaveFile> saved(saveFileMan.openForLoading(fileName));
+ if (saved && read(*saved))
+ return true;
+ Common::File defaults;
+ return defaults.open("RESOURCE/DATA/MORTIMER.HSF") && read(defaults);
+}
+
+void MortimerHighScores::start(uint32 score, bool allowEntry) {
+ _score = score;
+ _selected = allowEntry ? findHighScorePosition(_entries, ARRAYSIZE(_entries), score, true) : -1;
+ _enteringName = _selected >= 0 && _selected < (int)ARRAYSIZE(_entries);
+ _didNotQualify = allowEntry && !_enteringName;
+ if (_enteringName) {
+ Entry entry = {};
+ entry.score = score;
+ insertHighScoreEntry(_entries, ARRAYSIZE(_entries), ARRAYSIZE(_entries), _selected, entry);
+ } else {
+ _selected = -1;
+ }
+}
+
+bool MortimerHighScores::handleKey(const Common::KeyState &key) {
+ if (!_enteringName || (key.flags & (Common::KBD_CTRL | Common::KBD_ALT | Common::KBD_META)))
+ return false;
+ char *name = _entries[_selected].name;
+ const uint length = strlen(name);
+ if (key.keycode == Common::KEYCODE_RETURN || key.keycode == Common::KEYCODE_KP_ENTER)
+ return true;
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ name[0] = 0;
+ } else if (key.keycode == Common::KEYCODE_BACKSPACE) {
+ if (length)
+ name[length - 1] = 0;
+ } else if (key.ascii >= 32 && key.ascii < 127 && key.ascii != '%' && length < 10) {
+ name[length] = key.ascii;
+ name[length + 1] = 0;
+ }
+ return false;
+}
+
+bool MortimerHighScores::save(Common::SaveFileManager &saveFileMan, const Common::String &fileName) {
+ Common::ScopedPtr<Common::OutSaveFile> out(saveFileMan.openForSaving(fileName, false));
+ if (!out)
+ return false;
+ for (uint i = 0; i < ARRAYSIZE(_entries); ++i) {
+ out->write(_entries[i].name, sizeof(_entries[i].name));
+ out->writeUint32LE(_entries[i].score);
+ }
+ out->finalize();
+ if (out->err())
+ return false;
+ _enteringName = false;
+ return true;
+}
+
+Common::String MortimerHighScores::ranking(uint32 score) const {
+ const uint32 limits[] = {14999, 34999, 54999, 74999, 134999, 197999, 279999, 607499, 999999};
+ uint rank = 0;
+ while (rank < ARRAYSIZE(limits) && score > limits[rank])
+ ++rank;
+ return _text.text(rank);
+}
+
+void MortimerHighScores::drawText(Graphics::Surface &screen, const Common::String &text, int x, int y,
+ byte color, Graphics::TextAlign align) const {
+ const int width = _font.getStringWidth(text);
+ if (align == Graphics::kTextAlignCenter)
+ x -= width / 2;
+ else if (align == Graphics::kTextAlignRight)
+ x -= width;
+ // SHELL centers these text objects vertically, including the table rows.
+ _font.drawString(&screen, text, x, y - (_font.getFontHeight() + 1) / 2, width, color, Graphics::kTextAlignLeft, 0, false);
+}
+
+void MortimerHighScores::draw(Graphics::Surface &screen, uint32 time) const {
+ const int height = _font.getFontHeight() + 1;
+ drawText(screen, _text.text(10), 320, 140, 253, Graphics::kTextAlignCenter);
+ drawText(screen, _text.text(11), 30, 175, 1, Graphics::kTextAlignLeft);
+ drawText(screen, _text.text(12), 350, 175, 1, Graphics::kTextAlignRight);
+ drawText(screen, _text.text(13), 610, 175, 1, Graphics::kTextAlignRight);
+ for (uint i = 0; i < ARRAYSIZE(_entries); ++i) {
+ const Entry &entry = _entries[i];
+ const byte color = (int)i == _selected ? 1 : 253;
+ const int y = 210 + i * height;
+ drawText(screen, entry.name, 30, y, color, Graphics::kTextAlignLeft);
+ drawText(screen, Common::String::format("%u", entry.score), 350, y, color, Graphics::kTextAlignRight);
+ drawText(screen, ranking(entry.score), 610, y, color, Graphics::kTextAlignRight);
+ }
+ if (_enteringName) {
+ drawMortimerMessage(screen, _font, _text.text(14), 253);
+ const Common::String name(_entries[_selected].name);
+ const int y = 240 + height + height / 2 + height / 4;
+ drawText(screen, name, 320, y, 253, Graphics::kTextAlignCenter);
+ if ((time / 300) % 2 == 0) {
+ const int x = 322 + _font.getStringWidth(name) / 2;
+ screen.fillRect(Common::Rect(x, y - height / 2, x + 2, y + height / 2), 253);
+ }
+ } else {
+ if (_didNotQualify)
+ drawMortimerMessage(screen, _font, Common::String::format(_text.text(16).c_str(), (int)_score), 253);
+ Common::Array<Common::String> lines;
+ _font.wordWrapText(_text.text(15), 640, lines, 0, Graphics::kWordWrapOnExplicitNewLines);
+ const int top = 457 - (lines.size() * _font.getFontHeight() + 1) / 2;
+ for (uint i = 0; i < lines.size(); ++i)
+ drawText(screen, lines[i], 320, top + height / 2 + i * height, 1, Graphics::kTextAlignCenter);
+ }
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/mortimer/scores.h b/engines/scumm/insane/mortimer/scores.h
new file mode 100644
index 00000000000..d09a32a52dd
--- /dev/null
+++ b/engines/scumm/insane/mortimer/scores.h
@@ -0,0 +1,69 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SCUMM_INSANE_MORTIMER_SCORES_H
+#define SCUMM_INSANE_MORTIMER_SCORES_H
+
+#include "common/keyboard.h"
+#include "scumm/insane/mortimer/text.h"
+
+namespace Common {
+class SaveFileManager;
+}
+
+namespace Scumm {
+
+struct MortimerScoring {
+ uint32 stone, initial, castle;
+};
+
+const MortimerScoring &mortimerScoring(uint flightDifficulty, uint riddleDifficulty);
+
+class MortimerHighScores {
+public:
+ bool load(Common::SaveFileManager &saveFileMan, const Common::String &fileName);
+ void start(uint32 score, bool allowEntry);
+ bool handleKey(const Common::KeyState &key);
+ bool save(Common::SaveFileManager &saveFileMan, const Common::String &fileName);
+ bool enteringName() const { return _enteringName; }
+ void draw(Graphics::Surface &screen, uint32 time) const;
+
+private:
+ struct Entry {
+ char name[20];
+ uint32 score;
+ };
+
+ bool read(Common::SeekableReadStream &stream);
+ Common::String ranking(uint32 score) const;
+ void drawText(Graphics::Surface &screen, const Common::String &text, int x, int y, byte color,
+ Graphics::TextAlign align) const;
+
+ Entry _entries[10];
+ MortimerFont _font;
+ MortimerSubtitles _text;
+ uint32 _score = 0;
+ int _selected = -1;
+ bool _enteringName = false, _didNotQualify = false;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/mortimer/text.cpp b/engines/scumm/insane/mortimer/text.cpp
index 9b07ee0ae5f..2ce99a8265f 100644
--- a/engines/scumm/insane/mortimer/text.cpp
+++ b/engines/scumm/insane/mortimer/text.cpp
@@ -124,6 +124,34 @@ bool MortimerSubtitles::loadText(Common::SeekableReadStream &stream) {
return !stream.err() && !_entries.empty();
}
+Common::String MortimerSubtitles::text(uint16 id) const {
+ const auto found = _entries.find(id);
+ return found == _entries.end() ? Common::String() : found->_value.text;
+}
+
+void drawMortimerMessage(Graphics::Surface &screen, const MortimerFont &font, const Common::String &text, byte color) {
+ Common::Array<Common::String> lines;
+ const int width = font.wordWrapText(text, screen.w - 40, lines, 0, Graphics::kWordWrapOnExplicitNewLines);
+ const int lineHeight = font.getFontHeight() + 1;
+ const int height = lines.size() * font.getFontHeight() + 1;
+ const int x = (screen.w - width) / 2, y = (screen.h - height) / 2;
+ Common::Rect box(x - 10, y - 10, x + width + 10, y + height + 10);
+ screen.fillRect(box, 251);
+ for (int i = 0; i < 3; ++i) {
+ screen.hLine(box.left, box.top, box.right - 1, 250);
+ screen.vLine(box.left, box.top, box.bottom - 1, 250);
+ screen.hLine(box.left, box.bottom - 1, box.right - 1, 252);
+ screen.vLine(box.right - 1, box.top, box.bottom - 1, 252);
+ box.grow(-1);
+ }
+ for (uint i = 0; i < lines.size(); ++i)
+ font.drawString(&screen, lines[i], x, y + i * lineHeight, width, color, Graphics::kTextAlignCenter, 0, false);
+}
+
+void MortimerSubtitles::drawScore(Graphics::Surface &screen, uint32 score) const {
+ drawMortimerMessage(screen, _infoFont, Common::String::format(text(3).c_str(), (long)score), 231);
+}
+
void MortimerSubtitles::draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled,
const MortimerMovieContext *context) const {
for (uint i = 0; i < captions.size(); ++i) {
diff --git a/engines/scumm/insane/mortimer/text.h b/engines/scumm/insane/mortimer/text.h
index e4abc3c8354..fc59b923f13 100644
--- a/engines/scumm/insane/mortimer/text.h
+++ b/engines/scumm/insane/mortimer/text.h
@@ -45,6 +45,8 @@ class MortimerSubtitles {
public:
bool load(const char *textPath, const char *fontPath, const char *infoFontPath);
bool loadText(Common::SeekableReadStream &stream);
+ Common::String text(uint16 id) const;
+ void drawScore(Graphics::Surface &screen, uint32 score) const;
void draw(Graphics::Surface &screen, const Common::Array<MortimerMovie::Caption> &captions, bool enabled,
const MortimerMovieContext *context = nullptr) const;
@@ -60,6 +62,8 @@ private:
Common::HashMap<uint16, Entry> _entries;
};
+void drawMortimerMessage(Graphics::Surface &screen, const MortimerFont &font, const Common::String &text, byte color);
+
} // End of namespace Scumm
#endif
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 157d4d37c48..aff8c90b06d 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -31,6 +31,7 @@
#include "scumm/scumm_v7.h"
#include "scumm/smush/smush_player.h"
#include "scumm/insane/rebel1/rebel.h"
+#include "scumm/insane/highscores.h"
namespace Scumm {
@@ -1301,18 +1302,15 @@ int InsaneRebel1::runPasscodeEntryDialog() {
}
bool InsaneRebel1::runHighScoreNameEntry() {
- int slot = 0;
- while (slot < kHighScoreCount && _highScores[slot].score >= _score)
- slot++;
+ const int slot = findHighScorePosition(_highScores, kHighScoreCount, _score);
if (slot >= kHighScoreCount)
return false;
- for (int i = kHighScoreCount - 1; i > slot; i--)
- _highScores[i] = _highScores[i - 1];
-
- _highScores[slot].score = _score;
- _highScores[slot].difficulty = _difficulty;
- Common::strlcpy(_highScores[slot].name, "<", sizeof(_highScores[slot].name));
+ HighScoreEntry entry = {};
+ entry.score = _score;
+ entry.difficulty = _difficulty;
+ Common::strlcpy(entry.name, "<", sizeof(entry.name));
+ insertHighScoreEntry(_highScores, kHighScoreCount, kHighScoreCount, slot, entry);
_highScoreEntryIndex = slot;
beginTextEntry(false);
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index 92f064b51e1..53192318e36 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -39,6 +39,7 @@
#include "scumm/insane/rebel2/rebel.h"
#include "scumm/insane/rebel2/shared.h"
+#include "scumm/insane/highscores.h"
namespace Scumm {
@@ -1447,10 +1448,7 @@ void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
if (score == 0)
return;
- int insertPos = 0;
- while (insertPos < _numRankings && score <= _rankings[insertPos].score) {
- insertPos++;
- }
+ int insertPos = findHighScorePosition(_rankings, _numRankings, score);
if (insertPos > kMaxRankings - 1)
return;
@@ -1467,19 +1465,14 @@ void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
}
}
- int lastIdx = MIN(_numRankings, kMaxRankings - 1);
- for (int i = lastIdx; i > insertPos; i--)
- _rankings[i] = _rankings[i - 1];
-
- RankingEntry &r = _rankings[insertPos];
+ RankingEntry r = {};
Common::strlcpy(r.name, name, sizeof(r.name));
r.score = score;
r.rating = rating;
r.difficulty = difficulty;
r.chapter = chapter;
- if (_numRankings < kMaxRankings)
- _numRankings++;
+ _numRankings = insertHighScoreEntry(_rankings, _numRankings, kMaxRankings, insertPos, r);
}
void InsaneRebel2::showTopPilots() {
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 55e4d6decc4..5095cddc8dc 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -156,6 +156,7 @@ MODULE_OBJS += \
insane/mortimer/movie.o \
insane/mortimer/mortimer.o \
insane/mortimer/saveload.o \
+ insane/mortimer/scores.o \
insane/mortimer/text.o \
insane/rebel/rebel_audio.o \
insane/rebel/rebel_gamepad.o \
More information about the Scummvm-git-logs
mailing list