[Scummvm-git-logs] scummvm master -> b22dba7b28bbc47c128048915fa9a7b12db931e5

neuromancer noreply at scummvm.org
Sat Jun 6 15:34:24 UTC 2026


This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
84dfb4fc43 COMMON: Allow adding files to SpectrumTapeArchive
b22dba7b28 FREESCAPE: add ZX Spectrum tape support


Commit: 84dfb4fc433293629240ef9938396b16e7079f64
    https://github.com/scummvm/scummvm/commit/84dfb4fc433293629240ef9938396b16e7079f64
Author: Shadow Maker (shm at vtrd.in)
Date: 2026-06-06T17:34:19+02:00

Commit Message:
COMMON: Allow adding files to SpectrumTapeArchive

Changed paths:
    common/formats/spectrum_tape.cpp
    common/formats/spectrum_tape.h


diff --git a/common/formats/spectrum_tape.cpp b/common/formats/spectrum_tape.cpp
index 175146cbd3b..d18a460c5ba 100644
--- a/common/formats/spectrum_tape.cpp
+++ b/common/formats/spectrum_tape.cpp
@@ -471,15 +471,19 @@ SpectrumTapeArchive::SpectrumTapeArchive(const SpectrumTapeBlocks &blocks) {
 		if (!getTapBody(block, body, &flag) || flag == 0x00 || body.empty())
 			continue;
 
-		ArchiveFile entry;
-		entry.name = Path(getTapeFileName(pendingHeader, body, fileIndex++), Path::kNoSeparator);
-		entry.data = body;
-		_files.push_back(entry);
+		addFile(Path(getTapeFileName(pendingHeader, body, fileIndex++), Path::kNoSeparator), body);
 
 		pendingHeader.valid = false;
 	}
 }
 
+void SpectrumTapeArchive::addFile(const Path &path, const Array<byte> &data) {
+	ArchiveFile entry;
+	entry.name = path;
+	entry.data = data;
+	_files.push_back(entry);
+}
+
 bool SpectrumTapeArchive::hasFile(const Path &path) const {
 	for (const ArchiveFile &file : _files) {
 		if (file.name.equalsIgnoreCase(path))
diff --git a/common/formats/spectrum_tape.h b/common/formats/spectrum_tape.h
index ebb61d4c3ac..f72dccfa24e 100644
--- a/common/formats/spectrum_tape.h
+++ b/common/formats/spectrum_tape.h
@@ -42,8 +42,11 @@ bool parseSpectrumTape(SeekableReadStream &stream, SpectrumTapeBlocks &blocks);
 
 class SpectrumTapeArchive : public Archive {
 public:
+	SpectrumTapeArchive() {}
 	SpectrumTapeArchive(const SpectrumTapeBlocks &blocks);
 
+	void addFile(const Path &path, const Array<byte> &data);
+
 	bool hasFile(const Path &path) const override;
 	int listMembers(ArchiveMemberList &list) const override;
 	const ArchiveMemberPtr getMember(const Path &path) const override;


Commit: b22dba7b28bbc47c128048915fa9a7b12db931e5
    https://github.com/scummvm/scummvm/commit/b22dba7b28bbc47c128048915fa9a7b12db931e5
Author: Shadow Maker (shm at vtrd.in)
Date: 2026-06-06T17:34:19+02:00

Commit Message:
FREESCAPE: add ZX Spectrum tape support

Changed paths:
  A engines/freescape/zx_tape.cpp
  A engines/freescape/zx_tape.h
    engines/freescape/assets.cpp
    engines/freescape/detection.cpp
    engines/freescape/games/eclipse/zx.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/assets.cpp b/engines/freescape/assets.cpp
index c02332fb6bb..c76219cedee 100644
--- a/engines/freescape/assets.cpp
+++ b/engines/freescape/assets.cpp
@@ -23,10 +23,13 @@
 // available at https://github.com/TomHarte/Phantasma/ (MIT)
 
 #include "common/file.h"
+#include "common/archive.h"
+#include "common/config-manager.h"
 #include "common/compression/unzip.h"
 #include "image/bmp.h"
 
 #include "freescape/freescape.h"
+#include "freescape/zx_tape.h"
 
 namespace Freescape {
 
@@ -125,6 +128,9 @@ void FreescapeEngine::loadDataBundle() {
 	if (versionData != expectedVersion)
 		error("Unexpected version number for freescape.dat: expecting '%s' but found '%s'", expectedVersion.c_str(), versionData);
 	free(versionData);
+
+	if (Common::Archive *archive = makeZxSpectrumTapeArchive(*_gameDescription, ConfMan.getPath("path")))
+		SearchMan.add("freescape-zx-tape", archive, 10);
 }
 
 Graphics::Surface *FreescapeEngine::loadBundledImage(const Common::String &name, bool appendRenderMode) {
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index afba0909dbf..b6db9b61304 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -24,6 +24,10 @@
 
 #include "freescape/games/driller/driller.h"
 #include "freescape/games/eclipse/eclipse.h"
+#include "freescape/zx_tape.h"
+
+#include "common/config-manager.h"
+#include "common/file.h"
 
 namespace Freescape {
 
@@ -613,7 +617,7 @@ const ADGameDescription gameDescriptions[] = {
 	{
 		"totaleclipse2",
 		"",
-		AD_ENTRY1s("totaleclipse.zx.data", "5e80cb6a518d5ab2192b845801b1a32e", 35661),
+		AD_ENTRY1s("totaleclipse2.zx.data", "5e80cb6a518d5ab2192b845801b1a32e", 35661),
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_NO_FLAGS,
@@ -1263,9 +1267,70 @@ public:
 	const DebugChannelDef *getDebugChannels() const override {
 		return debugFlagList;
 	}
+	Common::Error identifyGame(DetectedGame &game, const void **descriptor) override;
+	DetectedGames detectGames(const Common::FSList &fslist, uint32 skipADFlags, bool skipIncomplete) override;
 	DetectedGame toDetectedGame(const ADDetectedGame &adGame, ADDetectedGameExtraInfo *extraInfo) const override;
+
+private:
+	ADDetectedGames detectZxTapeGames(const Common::FSList &fslist, uint32 skipADFlags) const;
 };
 
+Common::Error FreescapeMetaEngineDetection::identifyGame(DetectedGame &game, const void **descriptor) {
+	Common::Error err = AdvancedMetaEngineDetection<ADGameDescription>::identifyGame(game, descriptor);
+	if (err.getCode() == Common::kNoError)
+		return err;
+
+	Common::Path path = ConfMan.hasKey("path") ? ConfMan.getPath("path") : Common::Path(".");
+	Common::FSNode dir(path);
+	Common::FSList files;
+	if (!dir.isDirectory() || !dir.getChildren(files, Common::FSNode::kListAll) || files.empty())
+		return err;
+
+	Common::String gameId = ConfMan.get("gameid");
+	ADDetectedGames tapeGames = detectZxTapeGames(files, 0);
+	for (const ADDetectedGame &tapeGame : tapeGames) {
+		if (tapeGame.desc->gameId == gameId) {
+			game = toDetectedGame(tapeGame, nullptr);
+			*descriptor = new ADDynamicGameDescription<ADGameDescription>(tapeGame.desc);
+			return Common::kNoError;
+		}
+	}
+
+	return err;
+}
+
+DetectedGames FreescapeMetaEngineDetection::detectGames(const Common::FSList &fslist, uint32 skipADFlags, bool skipIncomplete) {
+	DetectedGames detectedGames = AdvancedMetaEngineDetection::detectGames(fslist, skipADFlags, skipIncomplete);
+
+	ADDetectedGames tapeGames = detectZxTapeGames(fslist, skipADFlags);
+	for (const ADDetectedGame &game : tapeGames)
+		detectedGames.push_back(toDetectedGame(game, nullptr));
+
+	return detectedGames;
+}
+
+ADDetectedGames FreescapeMetaEngineDetection::detectZxTapeGames(const Common::FSList &fslist, uint32 skipADFlags) const {
+	ADDetectedGames detectedGames;
+
+	for (const Common::FSNode &node : fslist) {
+		Common::File file;
+		Common::String name = node.getName();
+		if ((name.hasSuffixIgnoreCase(".tap") || name.hasSuffixIgnoreCase(".tzx")) && file.open(node)) {
+			for (const ADGameDescription *desc = Freescape::gameDescriptions; desc->gameId; ++desc) {
+				if (!(desc->flags & skipADFlags) && desc->platform == Common::kPlatformZX) {
+					file.seek(0);
+					Freescape::ZxTapeFileList files;
+					if (Freescape::extractZxSpectrumTapeFiles(file, desc->gameId, files) &&
+							Freescape::matchZxSpectrumTapeFiles(files, *desc, _md5Bytes))
+						detectedGames.push_back(ADDetectedGame(desc));
+				}
+			}
+		}
+	}
+
+	return detectedGames;
+}
+
 DetectedGame FreescapeMetaEngineDetection::toDetectedGame(const ADDetectedGame &adGame, ADDetectedGameExtraInfo *extraInfo) const {
 	DetectedGame game = AdvancedMetaEngineDetection::toDetectedGame(adGame);
 
diff --git a/engines/freescape/games/eclipse/zx.cpp b/engines/freescape/games/eclipse/zx.cpp
index 103e11d3760..dabb21644bc 100644
--- a/engines/freescape/games/eclipse/zx.cpp
+++ b/engines/freescape/games/eclipse/zx.cpp
@@ -86,24 +86,28 @@ void EclipseEngine::loadHeartFramesZX(Common::SeekableReadStream *file, int rest
 
 void EclipseEngine::loadAssetsZXFullGame() {
 	Common::File file;
+	const char *prefix = isEclipse2() ? "totaleclipse2" : "totaleclipse";
+	Common::Path titleFile(Common::String::format("%s.zx.title", prefix));
+	Common::Path borderFile(Common::String::format("%s.zx.border", prefix));
+	Common::Path dataFile(Common::String::format("%s.zx.data", prefix));
 
-	file.open("totaleclipse.zx.title");
+	file.open(titleFile);
 	if (file.isOpen()) {
 		_title = loadAndConvertScrImage(&file);
 	} else
-		error("Unable to find totaleclipse.zx.title");
+		error("Unable to find %s", titleFile.toString().c_str());
 
 	file.close();
-	file.open("totaleclipse.zx.border");
+	file.open(borderFile);
 	if (file.isOpen()) {
 		_border = loadAndConvertScrImage(&file);
 	} else
-		error("Unable to find totaleclipse.zx.border");
+		error("Unable to find %s", borderFile.toString().c_str());
 	file.close();
 
-	file.open("totaleclipse.zx.data");
+	file.open(dataFile);
 	if (!file.isOpen())
-		error("Failed to open totaleclipse.zx.data");
+		error("Failed to open %s", dataFile.toString().c_str());
 
 	if (isEclipse2()) {
 		loadMessagesFixedSize(&file, 0x2ac, 16, 30);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index a88d1003f37..57fcd93b830 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -72,7 +72,8 @@ MODULE_OBJS := \
 	sound/zx.o \
 	ui.o \
 	unpack.o \
-	wb.o
+	wb.o \
+	zx_tape.o
 
 ifdef USE_TINYGL
 MODULE_OBJS += \
@@ -105,4 +106,5 @@ endif
 include $(srcdir)/rules.mk
 
 # Detection objects
-DETECT_OBJS += $(MODULE)/detection.o
+DETECT_OBJS += $(MODULE)/detection.o \
+	$(MODULE)/zx_tape.o
diff --git a/engines/freescape/zx_tape.cpp b/engines/freescape/zx_tape.cpp
new file mode 100644
index 00000000000..3190231fab1
--- /dev/null
+++ b/engines/freescape/zx_tape.cpp
@@ -0,0 +1,121 @@
+/* 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 "freescape/zx_tape.h"
+
+#include "common/file.h"
+#include "common/formats/spectrum_tape.h"
+#include "common/fs.h"
+#include "common/md5.h"
+#include "common/memstream.h"
+#include "engines/advancedDetector.h"
+
+namespace Freescape {
+
+bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *prefix, ZxTapeFileList &files) {
+	files.clear();
+
+	Common::SpectrumTapeBlocks blocks;
+	if (!Common::parseSpectrumTape(stream, blocks))
+		return false;
+
+	Common::Array<byte> title;
+	Common::Array<byte> border;
+	Common::Array<byte> data;
+
+	for (const Common::SpectrumTapeBlock &block : blocks) {
+		if (block.tap.size() >= 2) {
+			Common::Array<byte> body;
+			body.assign(block.tap.begin() + 1, block.tap.end() - 1);
+			if (body.size() == 6912) {
+				title = border;
+				border = body;
+			} else if (body.size() >= data.size()) {
+				data = body;
+			}
+		}
+	}
+
+	const struct {
+		const char *suffix;
+		const Common::Array<byte> &payload;
+	} outputs[] = {
+		{ "title", title },
+		{ "border", border },
+		{ "data", data }
+	};
+	for (uint i = 0; i < ARRAYSIZE(outputs); ++i) {
+		if (!outputs[i].payload.empty()) {
+			ZxTapeFile file;
+			file.name = Common::Path(Common::String::format("%s.zx.%s", prefix, outputs[i].suffix), Common::Path::kNoSeparator);
+			file.data = outputs[i].payload;
+			files.push_back(file);
+		}
+	}
+
+	return !data.empty();
+}
+
+bool matchZxSpectrumTapeFiles(const ZxTapeFileList &files, const ADGameDescription &desc, uint md5Bytes) {
+	bool matched = desc.platform == Common::kPlatformZX;
+
+	for (const ADGameFileDescription *fileDesc = desc.filesDescriptions; matched && fileDesc->fileName; ++fileDesc) {
+		bool fileMatched = false;
+		Common::Path fileName(fileDesc->fileName, Common::Path::kNoSeparator);
+		for (uint i = 0; !fileMatched && i < files.size(); ++i) {
+			fileMatched = files[i].name.equalsIgnoreCase(fileName) &&
+				(fileDesc->fileSize == AD_NO_SIZE || fileDesc->fileSize == files[i].data.size());
+			if (fileMatched && fileDesc->md5) {
+				Common::MemoryReadStream stream(files[i].data.data(), files[i].data.size());
+				fileMatched = Common::computeStreamMD5AsString(stream, md5Bytes) == fileDesc->md5;
+			}
+		}
+		matched = fileMatched;
+	}
+
+	return matched;
+}
+
+Common::Archive *makeZxSpectrumTapeArchive(const ADGameDescription &desc, const Common::Path &gamePath) {
+	Common::Archive *archive = nullptr;
+
+	Common::FSList files;
+	if (desc.platform == Common::kPlatformZX && Common::FSNode(gamePath).getChildren(files, Common::FSNode::kListFilesOnly)) {
+		for (const Common::FSNode &node : files) {
+			Common::File file;
+			Common::String name = node.getName();
+			if ((name.hasSuffixIgnoreCase(".tap") || name.hasSuffixIgnoreCase(".tzx")) && file.open(node)) {
+				ZxTapeFileList tapeFiles;
+				if (extractZxSpectrumTapeFiles(file, desc.gameId, tapeFiles) && matchZxSpectrumTapeFiles(tapeFiles, desc)) {
+					Common::SpectrumTapeArchive *tapeArchive = new Common::SpectrumTapeArchive();
+					for (const ZxTapeFile &tapeFile : tapeFiles)
+						tapeArchive->addFile(tapeFile.name, tapeFile.data);
+					archive = tapeArchive;
+					break;
+				}
+			}
+		}
+	}
+
+	return archive;
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/zx_tape.h b/engines/freescape/zx_tape.h
new file mode 100644
index 00000000000..166775ee7d2
--- /dev/null
+++ b/engines/freescape/zx_tape.h
@@ -0,0 +1,51 @@
+/* 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 FREESCAPE_ZX_TAPE_H
+#define FREESCAPE_ZX_TAPE_H
+
+#include "common/array.h"
+#include "common/path.h"
+
+namespace Common {
+class Archive;
+class FSNode;
+class SeekableReadStream;
+}
+
+struct ADGameDescription;
+
+namespace Freescape {
+
+struct ZxTapeFile {
+	Common::Path name;
+	Common::Array<byte> data;
+};
+
+typedef Common::Array<ZxTapeFile> ZxTapeFileList;
+
+bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *prefix, ZxTapeFileList &files);
+bool matchZxSpectrumTapeFiles(const ZxTapeFileList &files, const ADGameDescription &desc, uint md5Bytes = 5000);
+Common::Archive *makeZxSpectrumTapeArchive(const ADGameDescription &desc, const Common::Path &gamePath);
+
+} // End of namespace Freescape
+
+#endif




More information about the Scummvm-git-logs mailing list