[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