[Scummvm-git-logs] scummvm master -> 54a6e9c6365c842e631f19b01c173eb14edba3ca

neuromancer noreply at scummvm.org
Wed Sep 23 18:57:43 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:
77761a452d FREESCAPE: added support for a compressed variant of eclipse (zx)
54a6e9c636 EEM: workaround to avoid zero-bytes sound pops in eem1 mac (cd)


Commit: 77761a452dbeb0fa8c68621712fc116dea1b7d5a
    https://github.com/scummvm/scummvm/commit/77761a452dbeb0fa8c68621712fc116dea1b7d5a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-23T20:55:57+02:00

Commit Message:
FREESCAPE: added support for a compressed variant of eclipse (zx)

Changed paths:
    engines/freescape/detection.cpp
    engines/freescape/games/eclipse/zx.cpp
    engines/freescape/zx_tape.cpp
    engines/freescape/zx_tape.h


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 04a8553c400..9acf014ca86 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -624,6 +624,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_NO_FLAGS,
 		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
+	{
+		"totaleclipse2",
+		"",
+		AD_ENTRY1s("totaleclipse2.zx.data", "1be6a1747decb1f788e3c277dccb1ac4", 28916),
+		Common::EN_ANY,
+		Common::kPlatformZX,
+		ADGF_NO_FLAGS,
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
+	},
 	{
 		"totaleclipse",
 		"",
diff --git a/engines/freescape/games/eclipse/zx.cpp b/engines/freescape/games/eclipse/zx.cpp
index 2a06e8d08b0..7e104e42483 100644
--- a/engines/freescape/games/eclipse/zx.cpp
+++ b/engines/freescape/games/eclipse/zx.cpp
@@ -26,6 +26,7 @@
 #include "freescape/games/eclipse/ay.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/variables.h"
+#include "freescape/zx_tape.h"
 
 namespace Freescape {
 
@@ -91,21 +92,21 @@ void EclipseEngine::loadAssetsZXFullGame() {
 	Common::Path borderFile(Common::String::format("%s.zx.border", prefix));
 	Common::Path dataFile(Common::String::format("%s.zx.data", prefix));
 
-	file.open(titleFile);
+	file.open(openZxSpectrumFile(titleFile), titleFile.toString());
 	if (file.isOpen()) {
 		_title = loadAndConvertScrImage(&file);
 	} else
 		error("Unable to find %s", titleFile.toString().c_str());
 
 	file.close();
-	file.open(borderFile);
+	file.open(openZxSpectrumFile(borderFile), borderFile.toString());
 	if (file.isOpen()) {
 		_border = loadAndConvertScrImage(&file);
 	} else
 		error("Unable to find %s", borderFile.toString().c_str());
 	file.close();
 
-	file.open(dataFile);
+	file.open(openZxSpectrumFile(dataFile), dataFile.toString());
 	if (!file.isOpen())
 		error("Failed to open %s", dataFile.toString().c_str());
 
diff --git a/engines/freescape/zx_tape.cpp b/engines/freescape/zx_tape.cpp
index a72bde27be8..e373e348e7a 100644
--- a/engines/freescape/zx_tape.cpp
+++ b/engines/freescape/zx_tape.cpp
@@ -126,6 +126,124 @@ void ZxRecordingDecoder::finish() {
 	_sync = false;
 }
 
+bool unpackZxSpectrumBlock(Common::Array<byte> &data) {
+	// Recognize the self-extracting LZ/RLE loader before reading its operands.
+	const byte loader[] = { 0x01, 0x34, 0x00, 0x11, 0xc6, 0x5b, 0xd5, 0xed, 0xb0, 0xb7, 0xc9, 0x11 };
+	const byte lzPrefix[] = { 0x2b, 0x7e, 0x1b, 0x12, 0xd6 };
+	const byte lzSuffix[] = {
+		0x20, 0xf8, 0x2b, 0xb6, 0x28, 0x14, 0x2b, 0xe5, 0x6e, 0x4f, 0xe6, 0x07, 0x67, 0x23, 0x19, 0xa9,
+		0x0f, 0x0f, 0x0f, 0xc6, 0x03, 0x4f, 0xed, 0xb8, 0x13, 0xe1, 0xed, 0x52, 0x19, 0x20, 0xdb, 0xc3
+	};
+	if (data.size() < 66 || data[0] != 0x21 || memcmp(data.data() + 3, loader, sizeof(loader)) ||
+			data[17] != 0x01 || READ_BE_UINT32(data.data() + 20) != 0xedb0eb11 ||
+			memcmp(data.data() + 26, lzPrefix, sizeof(lzPrefix)) || memcmp(data.data() + 32, lzSuffix, sizeof(lzSuffix)))
+		return true;
+
+	int base = READ_LE_UINT16(data.data() + 15);
+	uint16 packedSize = READ_LE_UINT16(data.data() + 18);
+	int end = READ_LE_UINT16(data.data() + 24);
+	int entry = READ_LE_UINT16(data.data() + 64);
+	byte marker = data[31];
+	if (READ_LE_UINT16(data.data() + 1) != base + 14 || !packedSize || packedSize + 66U != data.size() ||
+			base + data.size() > 0x10000 || base + packedSize >= end)
+		return false;
+
+	Common::Array<byte> memory;
+	memory.resize(0x10000);
+	memcpy(memory.data() + base, data.data() + 66, packedSize);
+	int src = base + packedSize;
+	int dst = end;
+
+	// The first stage expands backward until its input and output pointers meet.
+	do {
+		if (src <= base || src >= dst)
+			return false;
+		byte value = memory[--src];
+		memory[--dst] = value;
+		if (value != marker)
+			continue;
+		if (src <= base)
+			return false;
+		byte code = memory[--src];
+		if (code) {
+			if (src <= base)
+				return false;
+			int offset = ((code & 7) << 8) | memory[--src];
+			int count = (code >> 3) + 3;
+			int copy = dst + offset + 1;
+			if (copy >= end || dst - count + 1 < src)
+				return false;
+			while (count--)
+				memory[dst--] = memory[copy--];
+			++dst;
+		}
+	} while (src != dst);
+
+	if (entry < base || entry + 4 > end)
+		return false;
+	bool screen = memory[entry] == 0x21 && READ_LE_UINT16(memory.data() + entry + 1) == base && memory[entry + 3] == 0xe5;
+	if (screen)
+		entry += 4;
+
+	const byte rleLoader[] = { 0x01, 0x12, 0x00, 0xd5, 0xed, 0xb0, 0x21 };
+	const byte rleCode[] = { 0x7e, 0x07, 0xcb, 0x3f, 0x2b, 0xed, 0xa8, 0xe0, 0x3d, 0xcb, 0xbf, 0x28, 0xf3, 0x30, 0xf6, 0x23, 0x18, 0xf3 };
+	if (entry + 40 > end || memory[entry] != 0x21 || READ_LE_UINT16(memory.data() + entry + 1) != entry + 22 ||
+			memory[entry + 3] != 0x11 || memcmp(memory.data() + entry + 6, rleLoader, sizeof(rleLoader)) ||
+			memory[entry + 15] != 0x11 || memory[entry + 18] != 0x01 || memory[entry + 21] != 0xc9 ||
+			memcmp(memory.data() + entry + 22, rleCode, sizeof(rleCode)))
+		return false;
+
+	src = READ_LE_UINT16(memory.data() + entry + 13);
+	dst = READ_LE_UINT16(memory.data() + entry + 16);
+	int remaining = READ_LE_UINT16(memory.data() + entry + 19);
+	int start = dst + 1 - remaining;
+	if (!remaining || start < base || src < start || src >= entry || src > dst)
+		return false;
+	end = dst + 1;
+
+	// The second stage uses the high bit for repeated runs; a zero count means 128.
+	while (remaining) {
+		if (src < start || src > dst)
+			return false;
+		byte control = memory[src--];
+		int count = MIN<int>((control & 0x7f) ? (control & 0x7f) : 128, remaining);
+		remaining -= count;
+		while (count--) {
+			if (src < start || src > dst)
+				return false;
+			memory[dst--] = memory[src];
+			if (!(control & 0x80))
+				--src;
+		}
+		if (control & 0x80)
+			--src;
+	}
+
+	if (screen) {
+		// Discard the screen-copy routine preceding the SCR image.
+		if (start != base + 12 || end - start != 6912 || memory[base] != 0x21 ||
+				READ_LE_UINT16(memory.data() + base + 1) != start || READ_BE_UINT32(memory.data() + base + 3) != 0x11004001 ||
+				READ_LE_UINT16(memory.data() + base + 7) != 6912 || memory[base + 9] != 0xed ||
+				memory[base + 10] != 0xb0 || memory[base + 11] != 0xc9)
+			return false;
+		base = start;
+	}
+	data.assign(memory.begin() + base, memory.begin() + end);
+	return true;
+}
+
+Common::SeekableReadStream *openZxSpectrumFile(const Common::Path &name) {
+	Common::File file;
+	if (!file.open(name) || file.size() <= 0 || file.size() > 0x10000)
+		return nullptr;
+	Common::Array<byte> data;
+	data.resize(file.size());
+	if (file.read(data.data(), data.size()) != data.size() || !unpackZxSpectrumBlock(data))
+		return nullptr;
+	Common::MemoryReadStream stream(data.data(), data.size());
+	return stream.readStream(stream.size());
+}
+
 bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *prefix, ZxTapeFileList &files) {
 	files.clear();
 
@@ -156,6 +274,8 @@ bool extractZxSpectrumTapeFiles(Common::SeekableReadStream &stream, const char *
 		if (block.tap.size() >= 2) {
 			Common::Array<byte> body;
 			body.assign(block.tap.begin() + 1, block.tap.end() - 1);
+			if (!unpackZxSpectrumBlock(body))
+				return false;
 			if (body.size() >= 160 && READ_BE_UINT32(body.data()) == MKTAG('K', 'I', 'T', 'S') &&
 					READ_LE_UINT16(body.data() + 4) == body.size()) {
 				kitData = body;
diff --git a/engines/freescape/zx_tape.h b/engines/freescape/zx_tape.h
index 166775ee7d2..94aaa478bc3 100644
--- a/engines/freescape/zx_tape.h
+++ b/engines/freescape/zx_tape.h
@@ -42,6 +42,7 @@ struct ZxTapeFile {
 
 typedef Common::Array<ZxTapeFile> ZxTapeFileList;
 
+Common::SeekableReadStream *openZxSpectrumFile(const Common::Path &name);
 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);


Commit: 54a6e9c6365c842e631f19b01c173eb14edba3ca
    https://github.com/scummvm/scummvm/commit/54a6e9c6365c842e631f19b01c173eb14edba3ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-23T20:55:57+02:00

Commit Message:
EEM: workaround to avoid zero-bytes sound pops in eem1 mac (cd)

Changed paths:
    engines/eem/audio.cpp


diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 2d4b760444a..4655c2b4806 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -43,6 +43,25 @@ AudioPlayer::AudioPlayer(EEMEngine *vm) :
 	_isMacintosh(vm && vm->isMacintosh()) {
 }
 
+static Audio::SeekableAudioStream *makeMacSoundStream(Common::SeekableReadStream *stream) {
+	Audio::SeekableAudioStream *audio = Audio::makeMacSndStream(stream, DisposeAfterUse::YES);
+	if (!audio || audio->isStereo() || audio->getLength().totalNumberOfFrames() == 0)
+		return audio;
+
+	// The original Mac speech/SFX resources include a trailing 0x00 sample
+	// in their PCM length. In unsigned 8-bit audio this is full negative
+	// amplitude, not silence, and causes a pop at the end (bug #17223).
+	// Trim it here so both snd and decoded csnd are handled, without
+	// changing the shared decoder or the music instrument resources.
+	const Audio::Timestamp end = audio->getLength().addFrames(-1);
+	int16 lastSample;
+	if (audio->seek(end) && audio->readBuffer(&lastSample, 1) == 1 && lastSample == -32768)
+		return new Audio::SubSeekableAudioStream(audio, Audio::Timestamp(0, audio->getRate()), end);
+
+	audio->rewind();
+	return audio;
+}
+
 struct MacSndResource {
 	const char *name;
 	uint16 id;
@@ -275,7 +294,7 @@ void AudioPlayer::playMacSnd(uint16 resourceId, Audio::SoundHandle &handle,
 	}
 
 	Audio::SeekableAudioStream *audioStream =
-		Audio::makeMacSndStream(stream, DisposeAfterUse::YES);
+		makeMacSoundStream(stream);
 	if (!audioStream) {
 		delete stream;
 		warning("AudioPlayer: Mac snd resource %u is not playable", resourceId);
@@ -475,7 +494,7 @@ bool AudioPlayer::playMacMysterySound(uint num) {
 	}
 
 	Audio::SeekableAudioStream *audioStream = stream
-		? Audio::makeMacSndStream(stream, DisposeAfterUse::YES) : nullptr;
+		? makeMacSoundStream(stream) : nullptr;
 	if (!audioStream) {
 		delete stream;
 		warning("AudioPlayer: Mac mystery sound resource %u is not playable",




More information about the Scummvm-git-logs mailing list