[Scummvm-git-logs] scummvm master -> 67b2df920b20c4c65724445545c145124e5e99d0
whoozle
noreply at scummvm.org
Mon Oct 5 22:53:26 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
c899df252c COMMON: COMPRESSION: Add dynamic streams inflate variant
8b23f3b290 PHOENIXVR: Add WISE archive decompressor for install.exe
67b2df920b PHOENIXVR: add detection entry for the Cameron Files - Secret at Loch Ness USA CD retail release
Commit: c899df252c6b03733f29da3ece92bfff59f616e1
https://github.com/scummvm/scummvm/commit/c899df252c6b03733f29da3ece92bfff59f616e1
Author: Vladimir Menshakov (vladimir.menshakov at gmail.com)
Date: 2026-10-05T23:42:31+01:00
Commit Message:
COMMON: COMPRESSION: Add dynamic streams inflate variant
Changed paths:
common/compression/deflate.h
common/compression/zlib.cpp
diff --git a/common/compression/deflate.h b/common/compression/deflate.h
index 216ea75fc07..615653464c4 100644
--- a/common/compression/deflate.h
+++ b/common/compression/deflate.h
@@ -36,6 +36,7 @@ namespace Common {
* @{
*/
+class ReadStream;
class SeekableReadStream;
class WriteStream;
@@ -90,6 +91,25 @@ static inline bool inflateZlibHeaderless(byte *dst, uint dstLen, const byte *src
return inflateZlibHeaderless(dst, &dstLen, src, srcLen, dict, dictLen);
}
+/**
+ * Wrapper around zlib's inflate functions. This function will call the
+ * necessary inflate functions to uncompress data compressed with deflate
+ * but *not* with the standard zlib header.
+ * It decompress all data until inflate returns end of the stream.
+ * If a dictionary is provided through the dict buffer, uses it to initializes
+ * the internal decompression dictionary, before the decompression takes place.
+ *
+ * @param dst the stream to write into.
+ * @param src the stream to be decompressed.
+ * @param dict (optional) a decompress dictionary.
+ * @param dictLen (optional) the size of the dictionary.
+ * Mandatory if dict is not 0.
+ *
+ * @return true on success (Z_OK or Z_STREAM_END), false otherwise.
+ */
+
+bool inflateZlibHeaderless(Common::WriteStream &dst, Common::ReadStream &src, const byte *dict = nullptr, uint dictLen = 0);
+
/**
* Wrapper around zlib's inflate functions. This function will call the
* modified inflate functions to uncompress data compressed for Clickteam
diff --git a/common/compression/zlib.cpp b/common/compression/zlib.cpp
index c0e0efd053a..2404185ec30 100644
--- a/common/compression/zlib.cpp
+++ b/common/compression/zlib.cpp
@@ -85,6 +85,47 @@ bool inflateZlibHeaderless(byte *dst, uint *dstLen, const byte *src, uint srcLen
return true;
}
+bool inflateZlibHeaderless(Common::WriteStream &dst, Common::ReadStream &src, const byte *dict, uint dictLen) {
+ byte in[16384], out[16384];
+ // Initialize zlib
+ z_stream stream = {};
+
+ // Negative MAX_WBITS tells zlib there's no zlib header
+ int err = inflateInit2(&stream, -MAX_WBITS);
+ if (err != Z_OK)
+ return false;
+
+ // Set the dictionary, if provided
+ if (dict != nullptr) {
+ err = inflateSetDictionary(&stream, const_cast<byte *>(dict), dictLen);
+ if (err != Z_OK) {
+ inflateEnd(&stream);
+ return false;
+ }
+ }
+
+ int ret = Z_OK;
+ while (ret != Z_STREAM_END) {
+ stream.avail_in = src.read(in, sizeof(in));
+ if (stream.avail_in == 0)
+ break;
+ stream.next_in = in;
+
+ do {
+ stream.next_out = out;
+ stream.avail_out = sizeof(out);
+ ret = inflate(&stream, Z_NO_FLUSH);
+ if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
+ inflateEnd(&stream);
+ return false;
+ }
+ dst.write(out, sizeof(out) - stream.avail_out);
+ } while (stream.avail_out == 0);
+ }
+ inflateEnd(&stream);
+ return ret == Z_STREAM_END;
+}
+
#ifndef RELEASE_BUILD
static bool _shownBackwardSeekingWarning = false;
#endif
Commit: 8b23f3b2907755740067e7e03792f570c4ac027a
https://github.com/scummvm/scummvm/commit/8b23f3b2907755740067e7e03792f570c4ac027a
Author: Vladimir Menshakov (vladimir.menshakov at gmail.com)
Date: 2026-10-05T23:52:36+01:00
Commit Message:
PHOENIXVR: Add WISE archive decompressor for install.exe
Found in USA retail CD release of the Cameron Files - Secret at Loch Ness.
Changed paths:
A engines/phoenixvr/wise.cpp
A engines/phoenixvr/wise.h
engines/phoenixvr/module.mk
engines/phoenixvr/phoenixvr.cpp
diff --git a/engines/phoenixvr/module.mk b/engines/phoenixvr/module.mk
index 16bd09064e3..20ab56dbe8c 100644
--- a/engines/phoenixvr/module.mk
+++ b/engines/phoenixvr/module.mk
@@ -18,7 +18,8 @@ MODULE_OBJS = \
script_v2.o \
subtitles.o \
variables.o \
- vr.o
+ vr.o \
+ wise.o
# This module can be built as a plugin
ifeq ($(ENABLE_PHOENIXVR), DYNAMIC_PLUGIN)
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index c784f3ca99e..2184e5cad5c 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -53,6 +53,7 @@
#include "phoenixvr/script.h"
#include "phoenixvr/subtitles.h"
#include "phoenixvr/vr.h"
+#include "phoenixvr/wise.h"
#include "video/4xm_decoder.h"
#include "video/smk_decoder.h"
@@ -343,6 +344,10 @@ PhoenixVREngine::PhoenixVREngine(OSystem *syst, const ADGameDescription *gameDes
_levels.push_back(Level{path, name});
}
}
+ Common::ScopedPtr<Common::File> file(new Common::File);
+ if (file->open("install.exe")) {
+ SearchMan.add("install.exe", createWISEArchive(file.release()), 0, true);
+ }
}
int PhoenixVREngine::version() const {
diff --git a/engines/phoenixvr/wise.cpp b/engines/phoenixvr/wise.cpp
new file mode 100644
index 00000000000..ec12502d1a9
--- /dev/null
+++ b/engines/phoenixvr/wise.cpp
@@ -0,0 +1,368 @@
+#include "phoenixvr/wise.h"
+
+#include "common/array.h"
+#include "common/compression/deflate.h"
+#include "common/crc.h"
+#include "common/debug.h"
+#include "common/hashmap.h"
+#include "common/list.h"
+#include "common/memstream.h"
+#include "common/stream.h"
+#include "common/substream.h"
+#include <array>
+#include <functional>
+
+namespace PhoenixVR {
+namespace {
+struct NE {
+ struct Segment {
+ uint32 offset;
+ uint32 size;
+ uint16 flags;
+ bool automatic() const {
+ return flags & 1;
+ }
+ };
+
+ Common::Array<Segment> segments;
+ uint16 autoSegment;
+
+ NE(Common::SeekableReadStream &stream) {
+ auto magic = stream.readUint16LE();
+ if (magic != 0x5a4d)
+ error("Not an MZ executable");
+ stream.seek(0x3c);
+ auto neOffset = stream.readUint32LE();
+ stream.seek(neOffset);
+ magic = stream.readUint16LE();
+ if (magic != 0x454e)
+ error("Not an NE executable");
+ stream.seek(neOffset + 0x000e);
+ autoSegment = stream.readUint16LE();
+ debug("auto segment: %u", autoSegment);
+ stream.seek(neOffset + 0x001c);
+ auto numSegments = stream.readUint16LE();
+ stream.seek(neOffset + 0x0022);
+ auto segmentTableOffset = stream.readUint16LE();
+ stream.seek(neOffset + 0x0032);
+ auto segmentAddrShift = stream.readUint16LE();
+ stream.seek(neOffset + segmentTableOffset);
+ segments.reserve(numSegments);
+ for (unsigned idx = 0; idx != numSegments; ++idx) {
+ uint32 segmentOffset = stream.readUint16LE();
+ segmentOffset <<= segmentAddrShift;
+ uint32 segmentSize = stream.readUint16LE();
+ if (segmentSize == 0)
+ segmentSize = 0x10000;
+ auto segmentFlags = stream.readUint16LE();
+ auto segmentAlloc = stream.readUint16LE();
+ if (idx + 1 == autoSegment && (segmentFlags & 1) == 0)
+ error("automatic segment must have flag 1");
+ debug("NE: segment %u: offset: %04x, size: %u, flags: %04x, alloc: %u", idx, segmentOffset, segmentSize, segmentFlags, segmentAlloc);
+ segments.push_back(Segment{segmentOffset, segmentSize, segmentFlags});
+ }
+ }
+};
+
+class WiseArchive : public Common::Archive {
+ Common::ScopedPtr<Common::SeekableReadStream> stream;
+ int64 baseStreamsOffset;
+
+ struct Entry {
+ uint32 begin;
+ uint32 end;
+
+ uint32 size() const {
+ return end - begin;
+ }
+ };
+
+ Common::HashMap<Common::Path, Entry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> _entries;
+
+ static Common::Array<byte> inflate(Common::SeekableReadStream &stream, uint size) {
+ Common::MemoryWriteStreamDynamic dst(DisposeAfterUse::YES);
+ if (size <= 4)
+ error("compressed stream can't be shorted than 4 bytes");
+ debug("inflating %u bytes of compressed data", size);
+ auto startedAt = stream.pos();
+ Common::SeekableSubReadStream src(&stream, stream.pos(), stream.pos() + size - 4);
+ if (!Common::inflateZlibHeaderless(dst, src))
+ error("inflateZlibHeaderless failed");
+ stream.seek(startedAt + size - 4);
+ auto crc = stream.readUint32LE();
+ Common::Array<byte> uncompressed{dst.getData(), static_cast<uint>(dst.size())};
+ auto crcU = Common::CRC32{}.crcFast(uncompressed.data(), uncompressed.size());
+ if (crc != crcU)
+ error("CRC mismatch");
+ return uncompressed;
+ }
+
+ struct Script {
+ Common::Array<byte> script;
+ Common::Array<byte> numBytes;
+ Common::Array<byte> numStrings;
+ Common::Array<byte> numLocalized;
+
+ Script() : numBytes(32), numStrings(32), numLocalized(32) {}
+
+ struct FileEntry {
+ Common::Path path;
+ uint32 begin;
+ uint32 end;
+ };
+
+ Common::List<FileEntry> parse() {
+ Common::MemoryReadStream ms(script.data(), script.size());
+ ms.seek(17);
+ for (uint i = 0; i != 4; ++i) {
+ ms.readString();
+ }
+ ms.skip(6);
+ const auto numLanguages = ms.readByte();
+ if (numLanguages == 0 || numLanguages >= 128)
+ error("invalid number of languages (%u)", numLanguages);
+ auto numLangStrings = 6 + (numLanguages > 1 ? 2 + 2 * numLanguages : 1) + 44 * numLanguages;
+ while (numLangStrings--)
+ auto str = ms.readString();
+
+ Common::Array<byte> byteArguments;
+ Common::Array<Common::String> strArguments;
+ Common::List<FileEntry> entries;
+ while (!ms.eos()) {
+ auto opcode = ms.readByte();
+ if (opcode >= numBytes.size() || numBytes[opcode] == 0)
+ error("invalid opcode");
+ byteArguments.resize(numBytes[opcode] - 1);
+ ms.read(byteArguments.data(), byteArguments.size());
+ strArguments.resize(numStrings[opcode] + numLanguages * numLocalized[opcode]);
+ for (uint i = 0; i != strArguments.size(); ++i) {
+ strArguments[i] = ms.readString();
+ }
+ if (opcode == 6)
+ ms.skip(12 * (numLanguages - 1));
+ if (opcode != 0)
+ continue;
+
+ assert(byteArguments.size() >= 11);
+
+ // opcode 0: copy from packed source.
+ auto &dst = strArguments[0];
+ static Common::String prefix = "%MAINDIR%\\";
+ if (!dst.hasPrefixIgnoreCase(prefix))
+ continue;
+
+ Common::Path path(dst.substr(prefix.size(), '\\'));
+ auto begin = READ_LE_UINT32(byteArguments.data() + 2);
+ auto end = READ_LE_UINT32(byteArguments.data() + 6);
+ debug("adding entry %s %08x %08x", path.toString().c_str(), begin, end);
+ entries.push_back(FileEntry{Common::move(path), begin, end});
+ }
+ return entries;
+ }
+ };
+
+public:
+ WiseArchive(Common::SeekableReadStream *s) : stream(s) {
+ auto patternFind = [&](const Common::Array<byte> &data, byte initial, ssize_t patternSize, const std::function<bool(const byte *)> &func) -> long {
+ if (data.size() < patternSize)
+ return -1;
+ auto *start = data.data();
+ while ((data.size() - (start - data.data())) >= patternSize) {
+ auto tailSize = (data.size() - (start - data.data()));
+ auto *next = static_cast<const byte *>(memchr(start, initial, tailSize));
+ if (!next)
+ break;
+ if (func(next))
+ return next - data.data();
+ start = next + 1;
+ }
+ return -1;
+ };
+ NE ne(*s);
+ long overlayOffset = -1;
+ for (auto &seg : ne.segments) {
+ if (seg.automatic())
+ continue;
+
+ stream->seek(seg.offset);
+ Common::Array<byte> data(seg.size);
+ if (stream->read(data.data(), data.size()) != data.size())
+ error("NE segment: short read");
+
+ if (overlayOffset < 0) {
+ overlayOffset = patternFind(data, 0xa1, 30, [&](const byte *src) -> bool {
+ return src[0] == 0xa1 &&
+ src[3] == 0x8b &&
+ src[4] == 0x16 &&
+ src[7] == 0xa3 &&
+ src[10] == 0x89 &&
+ src[11] == 0x16 &&
+ src[14] == 0xa3 &&
+ src[17] == 0x89 &&
+ src[18] == 0x16 &&
+ src[21] == 0xff &&
+ src[22] == 0x36 &&
+ src[25] == 0x52 &&
+ src[26] == 0x50 &&
+ src[27] == 0x6a &&
+ src[28] == 0x00 &&
+ src[29] == 0x9a;
+ });
+ if (overlayOffset >= 0) {
+ auto low = READ_LE_UINT16(data.data() + overlayOffset + 1);
+ auto high = READ_LE_UINT16(data.data() + overlayOffset + 5);
+ if (low + 2 != high)
+ error("non-contiguous overlay pointer");
+ auto &autoSegment = ne.segments[ne.autoSegment - 1];
+ stream->seek(autoSegment.offset + low);
+ overlayOffset = stream->readUint32LE();
+ debug("found overlay at %08lx", overlayOffset);
+ break;
+ }
+ }
+ }
+ if (overlayOffset < 0)
+ error("can't find overlay offset");
+
+ stream->seek(overlayOffset);
+ std::array<uint, 16> header = {};
+ for (size_t i = 0; i != header.size(); ++i)
+ header[i] = stream->readUint32LE();
+
+ auto &flags = header[0];
+ auto &advertisedEOF = header[15];
+ debug("flags: %08x, eof: %08x", flags, advertisedEOF);
+ if (advertisedEOF != stream->size())
+ error("invalid overlay header - EOF does not match stream size");
+
+ debug("script %u/%u", header[7], header[6]);
+ Script script;
+ script.script = inflate(*stream, header[7]);
+ if (script.script.size() != header[6])
+ error("script uncompressed size does not match header");
+
+ debug("packed dll at %08lx, size: %u", stream->pos(), header[8]);
+ Common::Array<byte> dllData;
+ dllData = inflate(*stream, header[8]);
+
+ Common::MemoryReadStream dllStream(dllData.data(), dllData.size());
+ NE dll(dllStream);
+ for (auto &seg : dll.segments) {
+ if (seg.automatic())
+ continue;
+
+ dllStream.seek(seg.offset);
+ Common::Array<byte> data(seg.size);
+ if (dllStream.read(data.data(), data.size()) != data.size())
+ error("NE segment: short read");
+
+ auto instructionTables = patternFind(data, 0x8a, 26, [&](const byte *src) -> bool {
+ return src[0] == 0x8a &&
+ src[1] == 0x87 &&
+ src[4] == 0x8b &&
+ src[5] == 0xc8 &&
+ src[6] == 0x8a &&
+ src[7] == 0x87 &&
+ src[10] == 0x2a &&
+ src[11] == 0xe4 &&
+ src[12] == 0xf7 &&
+ src[13] == 0x2e &&
+ src[16] == 0x8b &&
+ src[17] == 0xf0 &&
+ src[18] == 0x2a &&
+ src[19] == 0xed &&
+ src[20] == 0x03 &&
+ src[21] == 0xf1 &&
+ src[22] == 0x8a &&
+ src[23] == 0x87;
+ });
+ if (instructionTables >= 0) {
+ debug("instruction tables at %08lx", instructionTables);
+ auto stringsOffset = READ_LE_UINT16(data.data() + instructionTables + 2);
+ auto localizedStringsOffset = READ_LE_UINT16(data.data() + instructionTables + 8);
+ auto numBytesOffset = READ_LE_UINT16(data.data() + instructionTables + 24);
+ debug("tables at: %06x %06x %06x", stringsOffset, localizedStringsOffset, numBytesOffset);
+ assert(stringsOffset + script.numStrings.size() <= data.size());
+ assert(localizedStringsOffset + script.numLocalized.size() <= data.size());
+ assert(numBytesOffset + script.numBytes.size() <= data.size());
+ auto &autoSegment = dll.segments[dll.autoSegment - 1];
+ dllStream.seek(autoSegment.offset + stringsOffset);
+ dllStream.read(script.numStrings.data(), script.numStrings.size());
+ dllStream.seek(autoSegment.offset + localizedStringsOffset);
+ dllStream.read(script.numLocalized.data(), script.numLocalized.size());
+ dllStream.seek(autoSegment.offset + numBytesOffset);
+ dllStream.read(script.numBytes.data(), script.numBytes.size());
+ break;
+ }
+ }
+
+ auto skipStream = [&](uint size) {
+ debug("skipping compressed stream %u bytes", size);
+ Common::SeekableSubReadStream ss(stream.get(), stream->pos(), stream->pos() + size);
+ auto skip = inflate(ss, size);
+ debug("skipped %u bytes", skip.size());
+ };
+ if (flags & 0x10)
+ skipStream(header[9]);
+
+ if (flags & 0x8000)
+ skipStream(header[10]);
+
+ if (flags & 0x20000)
+ skipStream(header[11]);
+
+ if (header[12])
+ skipStream(header[12]);
+
+ if (header[13])
+ skipStream(header[13]);
+
+ baseStreamsOffset = stream->pos();
+ debug("base stream offset: %08lx", baseStreamsOffset);
+ for (auto entry : script.parse()) {
+ _entries.setVal(Common::move(entry.path), {entry.begin, entry.end});
+ }
+ }
+
+ char getPathSeparator() const override {
+ return '\\';
+ }
+
+ bool hasFile(const Common::Path &path) const override {
+ return _entries.find(path) != _entries.end();
+ }
+
+ int listMembers(Common::ArchiveMemberList &list) const override {
+ int added = 0;
+ for (auto &kv : _entries) {
+ list.push_back(getMember(kv._key));
+ ++added;
+ }
+ return added;
+ }
+
+ const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override {
+ return Common::ArchiveMemberPtr{new Common::GenericArchiveMember(path, *this)};
+ }
+
+ Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override {
+ auto it = _entries.find(path);
+ if (it == _entries.end())
+ return nullptr;
+
+ auto &entry = it->_value;
+ stream->seek(baseStreamsOffset + entry.begin);
+ auto dst = inflate(*stream, entry.size());
+ auto *ptr = static_cast<byte *>(malloc(dst.size()));
+ if (!ptr)
+ error("createReadStreamForMember: malloc failed");
+ memcpy(ptr, dst.data(), dst.size());
+ return new Common::MemoryReadStream(ptr, dst.size(), DisposeAfterUse::YES);
+ }
+};
+} // namespace
+
+Common::Archive *createWISEArchive(Common::SeekableReadStream *stream) {
+ return new WiseArchive(stream);
+}
+} // namespace PhoenixVR
diff --git a/engines/phoenixvr/wise.h b/engines/phoenixvr/wise.h
new file mode 100644
index 00000000000..81ccc86c9f2
--- /dev/null
+++ b/engines/phoenixvr/wise.h
@@ -0,0 +1,40 @@
+/* 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 PHOENIXVR_WISE_H
+#define PHOENIXVR_WISE_H
+
+/**
+ * @file
+ * Installer WISE decompressor used in engines:
+ * - phoenixvr
+ */
+
+#include "common/archive.h"
+
+namespace PhoenixVR {
+
+// Load WISE installer files from the stream.
+Common::Archive *createWISEArchive(Common::SeekableReadStream *stream);
+
+} // namespace PhoenixVR
+
+#endif // PHOENIXVR_WISE_H
Commit: 67b2df920b20c4c65724445545c145124e5e99d0
https://github.com/scummvm/scummvm/commit/67b2df920b20c4c65724445545c145124e5e99d0
Author: Vladimir Menshakov (vladimir.menshakov at gmail.com)
Date: 2026-10-05T23:52:42+01:00
Commit Message:
PHOENIXVR: add detection entry for the Cameron Files - Secret at Loch Ness USA CD retail release
Changed paths:
engines/phoenixvr/detection_tables.h
diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index 4d40c499548..90f3a950e57 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -256,6 +256,17 @@ const ADGameDescription gameDescriptions[] = {
GUIO1(GUIO_NONE)
},
+ {"lochness",
+ "USA Retail version",
+ AD_ENTRY3s("install.EXE", "872c13b9a3a7be96300d0fc4419e1569", 8048204,
+ "cd1/LEVEL1/script1.pak", "f0ee2e406999f2c983e4a5de2e6a25c5", 21132,
+ "cd2/LEVEL3/script3.pak", "c8ef637a44287b12de150407555ceaad", 26189),
+ Common::EN_ANY,
+ Common::kPlatformWindows,
+ ADGF_DROPPLATFORM | ADGF_CD | ADGF_TESTING,
+ GUIO1(GUIO_NONE)
+ },
+
{"messenger",
"Retail version",
AD_ENTRY6s("script.pak", "1e0f9cb47bc203e9e2983b03ffa85174", 185,
More information about the Scummvm-git-logs
mailing list