[Scummvm-git-logs] scummvm master -> d3ddaec0ea030078f4be31aeeafcb7e681f4e59c
neuromancer
noreply at scummvm.org
Wed Jun 24 12:14:04 UTC 2026
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
cf378b61f1 FREESCAPE: improved OPL music rendition for Driller
015a24a173 FREESCAPE: improved culling support in Driller
d54c958df5 FREESCAPE: improved culling support in Total Eclipse and Castle Master
d3ddaec0ea EEM: allow to play the Mac release from the installer
Commit: cf378b61f17f7fbcf51d4415754940231ed14cd4
https://github.com/scummvm/scummvm/commit/cf378b61f17f7fbcf51d4415754940231ed14cd4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-24T14:13:48+02:00
Commit Message:
FREESCAPE: improved OPL music rendition for Driller
Changed paths:
engines/freescape/games/driller/opl.music.cpp
diff --git a/engines/freescape/games/driller/opl.music.cpp b/engines/freescape/games/driller/opl.music.cpp
index a6f95b2fb22..6fd3c41886f 100644
--- a/engines/freescape/games/driller/opl.music.cpp
+++ b/engines/freescape/games/driller/opl.music.cpp
@@ -72,9 +72,9 @@ const DrillerOPLBasePatch kDrillerOPLBasePatches[] = {
{ 0x22, 0x21, 0x18, 0x00, 0xF4, 0xF2, 0x55, 0x36, 0x00, 0x00, 0x08 } // default lead
};
-const byte kDrillerMusicAttenuation = 12;
+const byte kDrillerMusicAttenuation = 4;
const byte kDrillerNoiseAttenuation = 0;
-const byte kDrillerNarrowPulseAttenuation = 6;
+const byte kDrillerNarrowPulseAttenuation = 2;
const uint16 kDrillerNarrowPulseEdgeDistance = 0x0300;
const byte kDrillerPulseBrightnessBoostMax = 24;
const int kDrillerArpeggioSize = 3;
@@ -117,6 +117,44 @@ byte getDrillerInstrumentControl(const uint8_t *instA0, const uint8_t *instA1, b
return instA0[1];
}
+// SID sustain level (0-15, linear) -> OPL2 sustain-level nibble (attenuation).
+const byte kDrillerSidSustainToOPL[16] = {
+ 15, 8, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0
+};
+
+// SID rate (0=fast..15=slow) -> OPL rate (15=fast..0=never), inverted and
+// compressed into [5,15] so even the slowest SID rate stays audible.
+byte sidRateToOPL(byte sidRate) {
+ return 15 - (sidRate * 10) / 15;
+}
+
+// Convert a SID instrument's AD/SR bytes to OPL2 carrier envelope registers.
+void deriveDrillerOPLEnvelope(byte sidAD, byte sidSR, byte &oplAD, byte &oplSR, bool &sustaining) {
+ byte sidAttack = sidAD >> 4;
+ byte sidSustain = sidSR >> 4;
+ byte attack = sidRateToOPL(sidAttack);
+ byte decay = sidRateToOPL(sidAD & 0x0F);
+ byte release = sidRateToOPL(sidSR & 0x0F);
+
+ byte sustainLevel;
+ if (sidSustain != 0) {
+ sustaining = true;
+ sustainLevel = kDrillerSidSustainToOPL[sidSustain];
+ } else if (sidAttack >= 10) {
+ // Slow-attack swell, no SID sustain: hold it so the note does not
+ // collapse into a short puff (instruments 1, 2, 14).
+ sustaining = true;
+ sustainLevel = 0;
+ } else {
+ // Fast attack, no sustain: plucked voice that decays away.
+ sustaining = false;
+ sustainLevel = 15;
+ }
+
+ oplAD = (attack << 4) | decay;
+ oplSR = (sustainLevel << 4) | release;
+}
+
void DrillerOPLMusicPlayer::VoiceState::reset() {
trackDataPtr = nullptr;
trackIndex = 0;
@@ -650,12 +688,22 @@ void DrillerOPLMusicPlayer::setOPLInstrument(int channel, VoiceState &v) {
byte mod = kOPLModOffset[channel];
byte car = kOPLCarOffset[channel];
+ // Give the carrier the instrument's own envelope (from its SID AD/SR) so the
+ // 22 instruments stay distinct and sustained voices hold instead of fading.
+ int instBase = v.instrumentIndex;
+ if (instBase < 0 || instBase >= NUM_INSTRUMENTS * 8)
+ instBase = 0;
+ byte carAD, carSR;
+ bool sustaining;
+ deriveDrillerOPLEnvelope(instrumentDataA0[instBase + 2], instrumentDataA0[instBase + 3], carAD, carSR, sustaining);
+ byte carChar = (patch.carChar & ~0x20) | (sustaining ? 0x20 : 0x00);
+
_opl->writeReg(0x20 + mod, patch.modChar);
- _opl->writeReg(0x20 + car, patch.carChar);
+ _opl->writeReg(0x20 + car, carChar);
_opl->writeReg(0x60 + mod, patch.modAD);
- _opl->writeReg(0x60 + car, patch.carAD);
+ _opl->writeReg(0x60 + car, carAD);
_opl->writeReg(0x80 + mod, patch.modSR);
- _opl->writeReg(0x80 + car, patch.carSR);
+ _opl->writeReg(0x80 + car, carSR);
_opl->writeReg(0xE0 + mod, patch.modWave);
_opl->writeReg(0xE0 + car, patch.carWave);
_opl->writeReg(0xC0 + channel, patch.feedbackConnection);
@@ -681,7 +729,8 @@ void DrillerOPLMusicPlayer::applyPulseWidth(int channel, const VoiceState &v) {
modLevel = patch.modLevel > brightnessBoost ? patch.modLevel - brightnessBoost : 0;
byte feedback = (patch.feedbackConnection >> 1) & 0x07;
- feedback = MIN<byte>(7, feedback + (centerDistance >> 8));
+ // Only a little feedback; near the max (7) the FM tone turns to noise.
+ feedback = MIN<byte>(4, feedback + (centerDistance >> 9));
feedbackConnection = (patch.feedbackConnection & 0x01) | (feedback << 1);
if (edgeDistance <= kDrillerNarrowPulseEdgeDistance)
attenuation = kDrillerNarrowPulseAttenuation;
Commit: 015a24a17305ca81f61ad804e6bb441dcd79d096
https://github.com/scummvm/scummvm/commit/015a24a17305ca81f61ad804e6bb441dcd79d096
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-24T14:13:48+02:00
Commit Message:
FREESCAPE: improved culling support in Driller
Changed paths:
engines/freescape/area.cpp
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/objects/object.h
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index 65b6d3b4fbd..144db15a0f9 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -257,11 +257,8 @@ static float aabbMinProjection(const Math::AABB &aabb, const Math::Vector3d &axi
return support.dotProduct(axis);
}
-static bool aabbIntersectsHalfSpace(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &normal, float padding) {
- return aabbMaxProjection(aabb, normal) - camera.dotProduct(normal) >= -padding;
-}
-
static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3d &camera, const Math::Vector3d &direction, float fov, float aspectRatio, float nearClipPlane, float farClipPlane) {
+ (void)aspectRatio;
if (!aabb.isValid())
return false;
@@ -269,17 +266,7 @@ static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3
if (front.getSquareMagnitude() == 0.0f)
return true;
- const Math::Vector3d worldUp(0.0f, 1.0f, 0.0f);
- Math::Vector3d right = Math::Vector3d::crossProduct(front, worldUp);
- if (right.getSquareMagnitude() < 0.0001f)
- right = Math::Vector3d(1.0f, 0.0f, 0.0f);
- else
- right.normalize();
- Math::Vector3d up = Math::Vector3d::crossProduct(right, front).getNormalized();
-
const float padding = 32.0f;
- const float horizontalScale = tan(Math::deg2rad(fov) / 2.0f) * 1.25f;
- const float verticalScale = MAX(horizontalScale / MAX(aspectRatio, 0.001f), horizontalScale);
const float minDepth = aabbMinProjection(aabb, front) - camera.dotProduct(front);
const float maxDepth = aabbMaxProjection(aabb, front) - camera.dotProduct(front);
@@ -288,14 +275,22 @@ static bool aabbIntersectsViewVolume(const Math::AABB &aabb, const Math::Vector3
if (minDepth > farClipPlane + padding)
return false;
- if (!aabbIntersectsHalfSpace(aabb, camera, front * horizontalScale + right, padding))
- return false;
- if (!aabbIntersectsHalfSpace(aabb, camera, front * horizontalScale - right, padding))
- return false;
- if (!aabbIntersectsHalfSpace(aabb, camera, front * verticalScale + up, padding))
- return false;
- if (!aabbIntersectsHalfSpace(aabb, camera, front * verticalScale - up, padding))
- return false;
+ // Coarse view-octant cull matching the original (compute_view_clip_bounds):
+ // keep every object in the world octant(s) the frustum spans rather than a tight
+ // cone, and on axes the frustum straddles (its component within the fov
+ // half-angle) keep both sides. The kept set must match the original because the
+ // painter's bubble sort below is non-transitive: a tighter cone drops
+ // straddle-axis side objects and reorders the visible ones.
+ const float threshold = (float)sin(Math::deg2rad(fov) / 2.0f);
+ const Math::Vector3d mn = aabb.getMin();
+ const Math::Vector3d mx = aabb.getMax();
+ for (int i = 0; i < 3; i++) {
+ const float comp = front.getValue(i);
+ if (comp > threshold && mx.getValue(i) < camera.getValue(i))
+ return false;
+ if (comp < -threshold && mn.getValue(i) > camera.getValue(i))
+ return false;
+ }
return true;
}
@@ -465,16 +460,14 @@ void Area::draw(Freescape::Renderer *gfx, uint32 animationTicks, Math::Vector3d
// It is only applied to the vertices during the projection phase (L850f/L9177).
int n = _sortedObjects.size();
if (n > 1 && sort) {
- // Pre-sort by distance from camera (furthest first) to provide a stable initial
- // ordering for the non-transitive bubble sort below. The original game achieves
- // this by culling off-screen objects via a rendering volume check (L8bb7/L845b)
- // before sorting, which prevents distant off-screen objects from interfering with
- // the depth ordering of visible objects through non-transitive comparisons.
+ // Seed the non-transitive bubble sort below in object load (file) order --
+ // the same order the original game's renderer iterates its object list.
+ // That data order is what makes the Newell pairwise sort resolve
+ // correctly; any depth heuristic (center distance, nearest depth) is only
+ // an approximation and mis-orders some scenes, the file order does not.
Common::sort(_sortedObjects.begin(), _sortedObjects.end(),
- [&camera](Object *a, Object *b) {
- Math::Vector3d centerA = (a->_occlusionBox.getMin() + a->_occlusionBox.getMax()) * 0.5f;
- Math::Vector3d centerB = (b->_occlusionBox.getMin() + b->_occlusionBox.getMax()) * 0.5f;
- return (centerA - camera).getSquareMagnitude() > (centerB - camera).getSquareMagnitude();
+ [](Object *a, Object *b) {
+ return a->_loadIndex < b->_loadIndex;
});
for (int i = 0; i < n; i++) { // L9c31_whole_object_pass_loop
bool changed = false;
@@ -652,16 +645,10 @@ void Area::drawDepthLayer(Freescape::Renderer *gfx, uint32 animationTicks, Math:
// It is only applied to the vertices during the projection phase (L850f/L9177).
int n = _depthLayerSortedObjects.size();
if (n > 1 && sort) {
- // Pre-sort by distance from camera (furthest first) to provide a stable initial
- // ordering for the non-transitive bubble sort below. The original game achieves
- // this by culling off-screen objects via a rendering volume check (L8bb7/L845b)
- // before sorting, which prevents distant off-screen objects from interfering with
- // the depth ordering of visible objects through non-transitive comparisons.
+ // Seed in object load (file) order, matching the original (see Area::draw).
Common::sort(_depthLayerSortedObjects.begin(), _depthLayerSortedObjects.end(),
- [&camera](Object *a, Object *b) {
- Math::Vector3d centerA = (a->_occlusionBox.getMin() + a->_occlusionBox.getMax()) * 0.5f;
- Math::Vector3d centerB = (b->_occlusionBox.getMin() + b->_occlusionBox.getMax()) * 0.5f;
- return (centerA - camera).getSquareMagnitude() > (centerB - camera).getSquareMagnitude();
+ [](Object *a, Object *b) {
+ return a->_loadIndex < b->_loadIndex;
});
for (int i = 0; i < n; i++) { // L9c31_whole_object_pass_loop
bool changed = false;
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 0e21095df66..464dfe797ea 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -785,6 +785,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
if (newObject) {
newObject->scale(scale);
+ newObject->_loadIndex = object;
if (newObject->getType() == kEntranceType) {
if (entrancesByID->contains(newObject->getObjectID() & 0x7fff))
error("WARNING: replacing object id %d (%d)", newObject->getObjectID(), newObject->getObjectID() & 0x7fff);
@@ -1154,6 +1155,7 @@ void FreescapeEngine::loadGlobalObjects(Common::SeekableReadStream *file, int of
for (int i = 0; i < size; i++) {
Object *gobj = load8bitObject(file);
assert(gobj);
+ gobj->_loadIndex = 0x4000 + i; // after area objects, in global load order
assert(!globalObjectsByID->contains(gobj->getObjectID()));
debugC(1, kFreescapeDebugParser, "Adding global object: %d", gobj->getObjectID());
(*globalObjectsByID)[gobj->getObjectID()] = gobj;
diff --git a/engines/freescape/objects/object.h b/engines/freescape/objects/object.h
index b1b852c27b4..e85b7c1cb8e 100644
--- a/engines/freescape/objects/object.h
+++ b/engines/freescape/objects/object.h
@@ -93,6 +93,9 @@ public:
Math::AABB _boundingBox;
Math::AABB _occlusionBox;
Object *_partOfGroup = nullptr;
+ // Position in the object file, used to seed the renderer's depth sort in the
+ // same order the original game iterates its object list (see Area::draw).
+ uint16 _loadIndex = 0xFFFF;
};
} // End of namespace Freescape
Commit: d54c958df5118de9ad4615ecc71ca91eafccfd4a
https://github.com/scummvm/scummvm/commit/d54c958df5118de9ad4615ecc71ca91eafccfd4a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-24T14:13:48+02:00
Commit Message:
FREESCAPE: improved culling support in Total Eclipse and Castle Master
Changed paths:
engines/freescape/loaders/8bitBinaryLoader.cpp
engines/freescape/objects/geometricobject.cpp
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 464dfe797ea..a604be0ebc0 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -785,7 +785,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
if (newObject) {
newObject->scale(scale);
- newObject->_loadIndex = object;
+ newObject->_loadIndex = 0x4000 + object; // area objects render after globals (original pass 2)
if (newObject->getType() == kEntranceType) {
if (entrancesByID->contains(newObject->getObjectID() & 0x7fff))
error("WARNING: replacing object id %d (%d)", newObject->getObjectID(), newObject->getObjectID() & 0x7fff);
@@ -1155,7 +1155,7 @@ void FreescapeEngine::loadGlobalObjects(Common::SeekableReadStream *file, int of
for (int i = 0; i < size; i++) {
Object *gobj = load8bitObject(file);
assert(gobj);
- gobj->_loadIndex = 0x4000 + i; // after area objects, in global load order
+ gobj->_loadIndex = i; // global objects render before area objects (original pass 1)
assert(!globalObjectsByID->contains(gobj->getObjectID()));
debugC(1, kFreescapeDebugParser, "Adding global object: %d", gobj->getObjectID());
(*globalObjectsByID)[gobj->getObjectID()] = gobj;
diff --git a/engines/freescape/objects/geometricobject.cpp b/engines/freescape/objects/geometricobject.cpp
index 311f462698e..e98717b8ba0 100644
--- a/engines/freescape/objects/geometricobject.cpp
+++ b/engines/freescape/objects/geometricobject.cpp
@@ -268,6 +268,7 @@ Object *GeometricObject::duplicate() {
);
copy->_cyclingColors = _cyclingColors;
+ copy->_loadIndex = _loadIndex;
return copy;
}
Commit: d3ddaec0ea030078f4be31aeeafcb7e681f4e59c
https://github.com/scummvm/scummvm/commit/d3ddaec0ea030078f4be31aeeafcb7e681f4e59c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-24T14:13:48+02:00
Commit Message:
EEM: allow to play the Mac release from the installer
Changed paths:
A engines/eem/installer.cpp
A engines/eem/installer.h
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/module.mk
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index cae6ac41901..9665f18d349 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -81,7 +81,7 @@ const ADGameDescription gameDescriptions[] = {
},
{
"eem",
- "Mac",
+ "",
AD_ENTRY2s("MysteryData", "d94c087c27e68cc299d7c5e737e458f9", 941029,
"PICS.DBD", "8905041070ff1352666d98cd78d5501c", 3800445),
Common::EN_ANY,
@@ -89,6 +89,19 @@ const ADGameDescription gameDescriptions[] = {
ADGF_UNSTABLE,
GUI_OPTIONS_EEM_MAC
},
+ {
+ // Macintosh floppy release played straight from its installer files
+ // ("Eagle Eye Installer" + "EEM Install Data 2".."6"); the engine
+ // decompresses them on the fly (see installer.cpp).
+ "eem",
+ "",
+ AD_ENTRY2s("Eagle Eye Installer", "08440dbf0cb47fb57e522f050159ffaa", 1391577,
+ "EEM Install Data 2", "aebccc677e149b37285f291f6ac72f57", 1446428),
+ Common::EN_ANY,
+ Common::kPlatformMacintosh,
+ ADGF_UNSTABLE,
+ GUI_OPTIONS_EEM_MAC
+ },
{
// Eagle Eye Mysteries in London
"eem2",
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index c9cab6db0a4..d89eea60354 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -39,6 +39,7 @@
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/installer.h"
#include "eem/music.h"
#include "eem/site.h"
@@ -771,6 +772,20 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
bool EEMEngine::openArchives() {
const bool mac = isMacintosh();
+ // The Mac release can be played straight from its floppy installer. When
+ // those files are present, mount a virtual archive that decompresses the
+ // game data (PICS.DBD, MysteryData, fonts, ...) on demand, so the opens
+ // below and the Mac resource-fork lookups resolve transparently.
+ if (mac && !SearchMan.hasArchive("eem-installer")) {
+ const Common::FSNode gameDir(ConfMan.getPath("path"));
+ if (Common::Archive *installer = createInstallerArchive(gameDir)) {
+ SearchMan.add("eem-installer", installer);
+ debugC(1, kDebugGeneral, "Mounted Eagle Eye Mysteries Mac installer archive");
+ } else {
+ warning("EEMTEST: createInstallerArchive returned null");
+ }
+ }
+
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"), mac)) {
warning("PICS archive missing");
return false;
diff --git a/engines/eem/installer.cpp b/engines/eem/installer.cpp
new file mode 100644
index 00000000000..8c2096787b0
--- /dev/null
+++ b/engines/eem/installer.cpp
@@ -0,0 +1,573 @@
+/* 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 "eem/installer.h"
+
+#include "common/algorithm.h"
+#include "common/array.h"
+#include "common/debug.h"
+#include "common/endian.h"
+#include "common/fs.h"
+#include "common/memstream.h"
+#include "common/str.h"
+#include "common/stream.h"
+#include "common/textconsole.h"
+
+#include "eem/detection.h"
+
+namespace EEM {
+
+// ===========================================================================
+// Compact Pro decompressor
+//
+// The installer compresses each fork with Compact Pro's scheme: an outer RLE
+// escape layer (ESC1 = 0x81, ESC2 = 0x82) wrapped around an LZH stage built
+// from three Huffman tables (256-entry literals, 64-entry match lengths,
+// 128-entry match offsets) feeding an 8 KiB sliding window. Ported from the
+// reference cpt.c (macutils) decoder.
+// ===========================================================================
+
+namespace {
+
+class CompactProDecompressor {
+public:
+ CompactProDecompressor(const byte *src, uint32 srcLen, byte *dst, uint32 dstLen)
+ : _src(src), _srcLen(srcLen), _pos(0), _dst(dst), _dstLen(dstLen), _outPos(0),
+ _remaining((int32)dstLen), _outstat(kNone), _save(0), _lzptr(0),
+ _newbits(0), _bitsavail(0), _error(false) {
+ memset(_lz, 0, sizeof(_lz));
+ }
+
+ // `compressed` selects the LZH stage; otherwise the bytes are only RLE
+ // escaped. `inLen` is the size of the compressed span (the RLE stage reads
+ // exactly that many input bytes).
+ bool unpack(bool compressed, uint32 inLen) {
+ if (compressed)
+ unpackLZH();
+ else
+ unpackRLE(inLen);
+ return !_error && _remaining == 0;
+ }
+
+private:
+ enum { ESC1 = 0x81, ESC2 = 0x82 };
+ enum { kNone = 0, kEsc1Seen = 1, kEsc2Seen = 2 };
+ enum { kBlockSize = 0x1fff0 };
+
+ struct HuffNode {
+ byte leaf;
+ int value;
+ int left;
+ int right;
+ };
+
+ struct HuffEnt {
+ int value;
+ int length;
+ };
+
+ struct HuffEntLess {
+ bool operator()(const HuffEnt &a, const HuffEnt &b) const {
+ return a.length != b.length ? a.length < b.length : a.value < b.value;
+ }
+ };
+
+ byte getByte() {
+ if (_pos >= _srcLen) {
+ _error = true;
+ return 0;
+ }
+ return _src[_pos++];
+ }
+
+ void emit(byte b) {
+ if (_outPos < _dstLen)
+ _dst[_outPos++] = b;
+ _remaining--;
+ }
+
+ void outCh(int chIn) {
+ byte ch = chIn & 0xff;
+ _lz[_lzptr & 8191] = ch;
+ _lzptr++;
+
+ if (_outstat == kNone) {
+ if (ch == ESC1 && _remaining != 1) {
+ _outstat = kEsc1Seen;
+ } else {
+ _save = ch;
+ emit(ch);
+ }
+ } else if (_outstat == kEsc1Seen) {
+ if (ch == ESC2) {
+ _outstat = kEsc2Seen;
+ } else {
+ _save = ESC1;
+ emit(ESC1);
+ if (_remaining == 0)
+ return;
+ if (ch == ESC1 && _remaining != 1)
+ return; // remains in kEsc1Seen
+ _outstat = kNone;
+ _save = ch;
+ emit(ch);
+ }
+ } else { // kEsc2Seen: ch is a run length for the previous byte
+ _outstat = kNone;
+ if (ch != 0) {
+ int c = (ch - 1) & 0xff;
+ while (c != 0) {
+ emit(_save);
+ if (_remaining == 0)
+ return;
+ c = (c - 1) & 0xff;
+ }
+ } else {
+ emit(ESC1);
+ if (_remaining == 0)
+ return;
+ _save = ESC2;
+ emit(_save);
+ }
+ }
+ }
+
+ void unpackRLE(uint32 inLen) {
+ for (uint32 i = 0; i < inLen; i++) {
+ outCh(getByte());
+ if (_error || _remaining == 0)
+ break;
+ }
+ }
+
+ bool readHuff(Common::Array<HuffNode> &nodes, int size) {
+ int treeBytes = getByte();
+ if (size < treeBytes * 2) {
+ _error = true;
+ return false;
+ }
+
+ int treeCount[32];
+ memset(treeCount, 0, sizeof(treeCount));
+ Common::Array<HuffEnt> entries;
+ int i = 0, maxlen = 0;
+ for (int t = 0; t < treeBytes; t++) {
+ int b = getByte();
+ int lens[2] = { b >> 4, b & 0xf };
+ for (int k = 0; k < 2; k++) {
+ int length = lens[k];
+ if (length) {
+ if (length > maxlen)
+ maxlen = length;
+ treeCount[length]++;
+ HuffEnt e;
+ e.value = i;
+ e.length = length;
+ entries.push_back(e);
+ }
+ i++;
+ }
+ }
+
+ int j = 0;
+ for (int x = 0; x <= maxlen; x++)
+ j = (j << 1) + treeCount[x];
+ int unused = maxlen ? ((1 << maxlen) - j) : 0;
+ for (int u = 0; u < unused; u++) {
+ HuffEnt e;
+ e.value = size;
+ e.length = maxlen;
+ entries.push_back(e);
+ }
+
+ Common::sort(entries.begin(), entries.end(), HuffEntLess());
+
+ const int total = size * 2 + 32;
+ nodes.clear();
+ nodes.resize(total);
+ for (int n = 0; n < total; n++) {
+ nodes[n].leaf = 0;
+ nodes[n].value = 0;
+ nodes[n].left = 0;
+ nodes[n].right = 0;
+ }
+
+ int idx = (int)entries.size() - 1;
+ int lvlstart = total - 1, nxt = total - 1;
+ for (int codelen = maxlen; codelen >= 1; codelen--) {
+ while (idx >= 0 && entries[idx].length == codelen) {
+ nodes[nxt].leaf = 1;
+ nodes[nxt].value = entries[idx].value;
+ nxt--;
+ idx--;
+ }
+ int parents = nxt;
+ if (codelen > 1) {
+ int jj = lvlstart;
+ while (jj > parents + 1) {
+ nodes[nxt].leaf = 0;
+ nodes[nxt].left = jj - 1;
+ nodes[nxt].right = jj;
+ nxt--;
+ jj -= 2;
+ }
+ }
+ lvlstart = parents;
+ }
+ nodes[0].leaf = 0;
+ nodes[0].left = nxt + 1;
+ nodes[0].right = nxt + 2;
+ return true;
+ }
+
+ int getBit() {
+ int b = (_newbits >> 31) & 1;
+ _bitsavail--;
+ if (_bitsavail < 16) {
+ uint32 hi = getByte();
+ uint32 lo = getByte();
+ _newbits |= (hi << 8) | lo;
+ _bitsavail += 16;
+ }
+ _newbits = _newbits << 1;
+ return b;
+ }
+
+ int get6Bits() {
+ int b = (_newbits >> 26) & 0x3f;
+ _bitsavail -= 6;
+ _newbits = _newbits << 6;
+ if (_bitsavail < 16) {
+ uint32 hi = getByte();
+ uint32 lo = getByte();
+ uint32 cn = (hi << 8) | lo;
+ _newbits |= cn << (16 - _bitsavail);
+ _bitsavail += 16;
+ }
+ return b;
+ }
+
+ int getHuffByte(const Common::Array<HuffNode> &nodes) {
+ int idx = 0;
+ while (!nodes[idx].leaf)
+ idx = getBit() ? nodes[idx].right : nodes[idx].left;
+ return nodes[idx].value;
+ }
+
+ void unpackLZH() {
+ _lz[8189] = _lz[8190] = _lz[8191] = 0;
+ _lzptr = 0;
+ while (_remaining != 0 && !_error) {
+ Common::Array<HuffNode> huffData, huffLen, huffOff;
+ if (!readHuff(huffData, 256) || !readHuff(huffLen, 64) || !readHuff(huffOff, 128))
+ return;
+
+ int blockCount = 0;
+ uint32 hi = getByte();
+ uint32 lo = getByte();
+ _newbits = ((hi << 8) | lo) << 16;
+ _bitsavail = 16;
+
+ while (blockCount < (int)kBlockSize && _remaining != 0 && !_error) {
+ if (getBit()) {
+ outCh(getHuffByte(huffData));
+ blockCount += 2;
+ } else {
+ int length = getHuffByte(huffLen);
+ int off = (getHuffByte(huffOff) << 6) | get6Bits();
+ uint32 bptr = _lzptr - off;
+ while (length > 0) {
+ outCh(_lz[bptr & 8191]);
+ bptr++;
+ length--;
+ }
+ blockCount += 3;
+ }
+ }
+ }
+ }
+
+ const byte *_src;
+ uint32 _srcLen;
+ uint32 _pos;
+ byte *_dst;
+ uint32 _dstLen;
+ uint32 _outPos;
+ int32 _remaining;
+ int _outstat;
+ int _save;
+ uint32 _lzptr;
+ byte _lz[8192];
+ uint32 _newbits;
+ int _bitsavail;
+ bool _error;
+};
+
+} // End of anonymous namespace
+
+class InstallerArchive : public Common::Archive {
+private:
+ struct Entry {
+ Common::String name;
+ uint32 pos; // logical offset of the resource span within _archive
+ uint32 rsrcLen;
+ uint32 dataLen;
+ uint32 compRLen;
+ uint32 compDLen;
+ uint16 cptFlag;
+ };
+
+ class Member : public Common::ArchiveMember {
+ public:
+ Member(const InstallerArchive *archive, const Entry *entry)
+ : _archive(archive), _entry(entry) {}
+
+ Common::SeekableReadStream *createReadStream() const override {
+ return _archive->createStream(*_entry, false);
+ }
+ Common::SeekableReadStream *createReadStreamForAltStream(Common::AltStreamType altStreamType) const override {
+ if (altStreamType == Common::AltStreamType::MacResourceFork)
+ return _archive->createStream(*_entry, true);
+ return nullptr;
+ }
+ Common::String getName() const override { return _entry->name; }
+ Common::Path getPathInArchive() const override { return Common::Path(_entry->name); }
+ Common::String getFileName() const override { return _entry->name; }
+ bool isInMacArchive() const override { return true; }
+
+ private:
+ const InstallerArchive *_archive;
+ const Entry *_entry;
+ };
+
+public:
+ bool load(const Common::FSNode &dir);
+
+ bool hasFile(const Common::Path &path) const override;
+ int listMembers(Common::ArchiveMemberList &list) const override;
+ const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
+ Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
+ Common::SeekableReadStream *createReadStreamForMemberAltStream(const Common::Path &path,
+ Common::AltStreamType altStreamType) const override;
+ char getPathSeparator() const override { return ':'; }
+
+private:
+ const Entry *findEntry(const Common::Path &path) const;
+ Common::SeekableReadStream *createStream(const Entry &entry, bool resFork) const;
+ void parseCatalog();
+
+ Common::Array<byte> _archive;
+ Common::Array<Entry> _entries;
+ Common::Array<uint32> _diskBase; // [1..6] start offset of each disk within _archive
+};
+
+bool InstallerArchive::load(const Common::FSNode &dir) {
+ // disk 1 first, then the five continuation segments, concatenated into one
+ // logical stream (payload spans cross floppy boundaries).
+ Common::Array<Common::String> names;
+ names.push_back("Eagle Eye Installer");
+ for (int i = 2; i <= 6; i++)
+ names.push_back(Common::String::format("EEM Install Data %d", i));
+
+ _diskBase.resize(names.size() + 1); // 1-based; index 0 unused
+ for (uint i = 0; i < _diskBase.size(); i++)
+ _diskBase[i] = 0xffffffff;
+
+ for (uint i = 0; i < names.size(); i++) {
+ Common::FSNode node = dir.getChild(names[i]);
+ if (!node.exists()) {
+ if (i == 0)
+ return false; // installer not present here
+ // Missing continuation segments are tolerated (e.g. CD layout);
+ // only entries that reference them will fail to load.
+ break;
+ }
+ Common::ScopedPtr<Common::SeekableReadStream> stream(node.createReadStream());
+ if (!stream) {
+ warning("EEM installer: cannot read %s", names[i].c_str());
+ return i != 0;
+ }
+ const uint32 oldSize = _archive.size();
+ const uint32 addSize = (uint32)stream->size();
+ _diskBase[i + 1] = oldSize; // disk numbers are 1-based
+ _archive.resize(oldSize + addSize);
+ if (stream->read(_archive.data() + oldSize, addSize) != addSize) {
+ warning("EEM installer: short read on %s", names[i].c_str());
+ return false;
+ }
+ }
+
+ parseCatalog();
+ if (_entries.empty()) {
+ warning("EEM installer: no catalog entries found");
+ return false;
+ }
+ debugC(1, kDebugGeneral, "EEM installer: %u entries, %u bytes",
+ _entries.size(), _archive.size());
+ return true;
+}
+
+void InstallerArchive::parseCatalog() {
+ // The catalog occupies the start of disk 1. Each entry is a Pascal name,
+ // followed by a disk byte + big-endian 32-bit offset, an 8-byte marker
+ // ("DATASFS1"/"APPLSFS1") and a 32-byte field block.
+ const byte *cat = _archive.data();
+ const uint32 catSize = _archive.size();
+ const uint32 scanEnd = MIN<uint32>(catSize, 0x500);
+ const char *markers[2] = { "DATASFS1", "APPLSFS1" };
+
+ for (int mi = 0; mi < 2; mi++) {
+ const char *mk = markers[mi];
+ for (uint32 pos = 5; pos + 8 + 32 <= catSize && pos + 8 <= scanEnd; pos++) {
+ if (memcmp(cat + pos, mk, 8) != 0)
+ continue;
+
+ const int disk = cat[pos - 5];
+ const uint32 off = READ_BE_UINT32(cat + pos - 4);
+
+ // Recover the file name: a Pascal string ending right before the
+ // 5-byte disk/offset prefix.
+ const uint32 nameEnd = pos - 5;
+ Common::String name;
+ const uint32 lo = nameEnd > 80 ? nameEnd - 80 : 0;
+ for (uint32 lpos = lo; lpos < nameEnd; lpos++) {
+ const uint32 len = cat[lpos];
+ if (lpos + 1 + len != nameEnd)
+ continue;
+ bool printable = true;
+ for (uint32 c = lpos + 1; c < nameEnd; c++) {
+ if (cat[c] < 32 || cat[c] >= 127) {
+ printable = false;
+ break;
+ }
+ }
+ if (printable)
+ name = Common::String((const char *)cat + lpos + 1, len);
+ }
+ if (name.empty())
+ continue;
+
+ const byte *f = cat + pos + 8;
+ Entry e;
+ e.name = name;
+ e.cptFlag = READ_BE_UINT16(f + 14);
+ e.rsrcLen = READ_BE_UINT32(f + 16);
+ e.dataLen = READ_BE_UINT32(f + 20);
+ e.compRLen = READ_BE_UINT32(f + 24);
+ e.compDLen = READ_BE_UINT32(f + 28);
+
+ // Map the per-disk relative offset to the concatenated stream. The
+ // segments were appended in order, so the global position is the
+ // loaded base of `disk` plus `off`; spans flowing into later disks
+ // stay contiguous. Skip entries on segments that were not loaded.
+ if (disk < 1 || (uint)disk >= _diskBase.size() ||
+ _diskBase[disk] == 0xffffffff)
+ continue;
+ e.pos = _diskBase[disk] + off;
+ _entries.push_back(e);
+ }
+ }
+}
+
+bool InstallerArchive::hasFile(const Common::Path &path) const {
+ return findEntry(path) != nullptr;
+}
+
+int InstallerArchive::listMembers(Common::ArchiveMemberList &list) const {
+ for (uint i = 0; i < _entries.size(); i++)
+ list.push_back(Common::ArchiveMemberPtr(new Member(this, &_entries[i])));
+ return _entries.size();
+}
+
+const Common::ArchiveMemberPtr InstallerArchive::getMember(const Common::Path &path) const {
+ const Entry *e = findEntry(path);
+ if (!e)
+ return nullptr;
+ return Common::ArchiveMemberPtr(new Member(this, e));
+}
+
+Common::SeekableReadStream *InstallerArchive::createReadStreamForMember(const Common::Path &path) const {
+ const Entry *e = findEntry(path);
+ if (!e)
+ return nullptr;
+ return createStream(*e, false);
+}
+
+Common::SeekableReadStream *InstallerArchive::createReadStreamForMemberAltStream(
+ const Common::Path &path, Common::AltStreamType altStreamType) const {
+ const Entry *e = findEntry(path);
+ if (!e || altStreamType != Common::AltStreamType::MacResourceFork)
+ return nullptr;
+ return createStream(*e, true);
+}
+
+const InstallerArchive::Entry *InstallerArchive::findEntry(const Common::Path &path) const {
+ const Common::String want = path.baseName();
+ for (uint i = 0; i < _entries.size(); i++) {
+ if (_entries[i].name.equalsIgnoreCase(want))
+ return &_entries[i];
+ }
+ return nullptr;
+}
+
+Common::SeekableReadStream *InstallerArchive::createStream(const Entry &entry, bool resFork) const {
+ const uint32 outLen = resFork ? entry.rsrcLen : entry.dataLen;
+ if (outLen == 0) {
+ if (!resFork)
+ return new Common::MemoryReadStream(nullptr, 0, DisposeAfterUse::NO);
+ return nullptr;
+ }
+
+ const uint32 srcPos = resFork ? entry.pos : entry.pos + entry.compRLen;
+ const uint32 srcLen = resFork ? entry.compRLen : entry.compDLen;
+ const bool compressed = resFork ? (entry.cptFlag & 0x02) != 0
+ : (entry.cptFlag & 0x04) != 0;
+
+ if (srcPos > _archive.size() || srcLen > _archive.size() - srcPos) {
+ warning("EEM installer: %s span out of range", entry.name.c_str());
+ return nullptr;
+ }
+
+ byte *dst = (byte *)malloc(outLen);
+ if (!dst)
+ return nullptr;
+
+ CompactProDecompressor cp(_archive.data() + srcPos, srcLen, dst, outLen);
+ if (!cp.unpack(compressed, srcLen)) {
+ warning("EEM installer: failed to decompress %s%s", entry.name.c_str(),
+ resFork ? " (resource fork)" : "");
+ free(dst);
+ return nullptr;
+ }
+
+ return new Common::MemoryReadStream(dst, outLen, DisposeAfterUse::YES);
+}
+
+Common::Archive *createInstallerArchive(const Common::FSNode &dir) {
+ InstallerArchive *archive = new InstallerArchive();
+ if (!archive->load(dir)) {
+ delete archive;
+ return nullptr;
+ }
+ return archive;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/installer.h b/engines/eem/installer.h
new file mode 100644
index 00000000000..69a983f6c50
--- /dev/null
+++ b/engines/eem/installer.h
@@ -0,0 +1,55 @@
+/* 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 EEM_INSTALLER_H
+#define EEM_INSTALLER_H
+
+#include "common/archive.h"
+
+namespace Common {
+class FSNode;
+}
+
+namespace EEM {
+
+// The Macintosh floppy release of Eagle Eye Mysteries ships a custom
+// self-extracting installer spread across six files:
+//
+// "Eagle Eye Installer" (disk 1: catalog + first payload span)
+// "EEM Install Data 2".."EEM Install Data 6" (disks 2-6: payload)
+//
+// The catalog lives in the data fork of "Eagle Eye Installer"; each entry is
+// flagged "DATASFS1" (a data file) or "APPLSFS1" (the game application) and the
+// fork payloads are Compact Pro compressed (an RLE escape layer over an LZH
+// stage). createInstallerArchive() parses that catalog and returns an archive
+// that decompresses members on demand, so ScummVM can run straight from the
+// installer files with no manual extraction step.
+//
+// Data files (PICS.DBD, ANI.DBX, MysteryData, ...) are exposed as plain
+// members; the application and "EEM Sound&Music" resource forks are exposed as
+// Mac resource-fork alt streams so Common::MacResManager can read them.
+//
+// Returns nullptr when the installer files are absent or cannot be parsed.
+Common::Archive *createInstallerArchive(const Common::FSNode &dir);
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 6879b600e85..917289b1522 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
eem.o \
font.o \
graphics.o \
+ installer.o \
metaengine.o \
music.o \
mystery.o \
More information about the Scummvm-git-logs
mailing list