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

neuromancer noreply at scummvm.org
Mon Nov 24 17:47:23 UTC 2025


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:
080e88600d PRIVATE: Clear inventory flag in LoseInventory()
db4b2bb5af PRIVATE: New save format with versioning
c4646ef1dc PRIVATE: Remove incompatible saves from listings


Commit: 080e88600d20aebc4e7dfd2e7c3f6f320d341166
    https://github.com/scummvm/scummvm/commit/080e88600d20aebc4e7dfd2e7c3f6f320d341166
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-11-24T18:47:18+01:00

Commit Message:
PRIVATE: Clear inventory flag in LoseInventory()

We now track inventory flags so that LoseInventory() can clear them.
This removes an item from Marlowe's inventory, instead of just removing
it from the casebook.

This alters the save format, breaking all saves.

The next commit alters the save format further, but also adds versioning
so that we can gracefully handle this and make future changes.

Changed paths:
    engines/private/funcs.cpp
    engines/private/private.cpp
    engines/private/private.h


diff --git a/engines/private/funcs.cpp b/engines/private/funcs.cpp
index 6a1178e1d55..20078fa1031 100644
--- a/engines/private/funcs.cpp
+++ b/engines/private/funcs.cpp
@@ -439,20 +439,16 @@ static void fInventory(ArgArray args) {
 			g_private->playSound(g_private->getTakeLeaveSound(), 1, false, false);
 		}
 	} else {
+		Common::String flag;
 		if (v1.type == NAME) {
-			v1.u.sym = g_private->maps.lookupVariable(v1.u.sym->name);
 			if (strcmp(c.u.str, "\"REMOVE\"") == 0) {
-				v1.u.sym->u.val = 0;
-				if (g_private->inInventory(bmp))
-					g_private->inventory.remove(bmp);
+				g_private->removeInventory(bmp);
 			} else {
-				v1.u.sym->u.val = 1;
-				if (!g_private->inInventory(bmp))
-					g_private->inventory.push_back(bmp);
+				flag = *(v1.u.sym->name);
+				g_private->addInventory(bmp, flag);
 			}
 		} else {
-			if (!g_private->inInventory(bmp))
-				g_private->inventory.push_back(bmp);
+			g_private->addInventory(bmp, flag);
 		}
 		if (v2.type == NAME) {
 			v2.u.sym = g_private->maps.lookupVariable(v2.u.sym->name);
diff --git a/engines/private/private.cpp b/engines/private/private.cpp
index 1267e0a87ab..27c041bfce8 100644
--- a/engines/private/private.cpp
+++ b/engines/private/private.cpp
@@ -964,9 +964,7 @@ void PrivateEngine::selectMask(Common::Point mousePos) {
 			if (m.flag1 != nullptr) { // TODO: check this
 				// an item was taken
 				if (_toTake) {
-					if (!inInventory(m.inventoryItem))
-						inventory.push_back(m.inventoryItem);
-					setSymbol(m.flag1, 1);
+					addInventory(m.inventoryItem, *(m.flag1->name));
 					playSound(getTakeSound(), 1, false, false);
 					_toTake = false;
 					_haveTakenItem = true;
@@ -1178,31 +1176,57 @@ void PrivateEngine::addMemory(const Common::String &path) {
 }
 
 bool PrivateEngine::inInventory(const Common::String &bmp) const {
-	for (NameList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
-		if (*it == bmp)
+	for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
+		if (it->diaryImage == bmp)
 			return true;
 	}
 	return false;
 }
 
+void PrivateEngine::addInventory(const Common::String &bmp, Common::String &flag) {
+	// set game flag
+	if (!flag.empty()) {
+		Symbol *sym = maps.lookupVariable(&flag);
+		setSymbol(sym, 1);
+	}
+
+	// add to casebook
+	if (!inInventory(bmp)) {
+		InventoryItem i;
+		i.diaryImage = bmp;
+		i.flag = flag;
+		inventory.push_back(i);
+	}
+}
+
+void PrivateEngine::removeInventory(const Common::String &bmp) {
+	for (InvList::iterator it = inventory.begin(); it != inventory.end(); ++it) {
+		if (it->diaryImage == bmp) {
+			// clear game flag
+			if (!it->flag.empty()) {
+				Symbol *sym = maps.lookupVariable(&(it->flag));
+				setSymbol(sym, 0);
+			}
+			// remove from casebook
+			inventory.erase(it);
+			break;
+		}
+	}
+}
+
 void PrivateEngine::removeRandomInventory() {
 	// This logic was extracted from the executable.
 	// Examples:
 	//   0-3 items:  0 items removed
 	//   4-6 items:  1 item removed
 	//   7-10 items: 2 items removed
-	//
-	// TODO: Clear the inventory flag for the item.
-	// We are currently only removing items from the diary. We need to also
-	// remove them from Marlowe's inventory by clearing their item flag.
-	// We can do this once item flags are stored and included in save files.
 	uint numberOfItemsToRemove = (inventory.size() * 30) / 100;
 	for (uint i = 0; i < numberOfItemsToRemove; i++) {
 		uint indexToRemove = _rnd->getRandomNumber(inventory.size() - 1);
 		uint index = 0;
 		for (InvList::iterator it = inventory.begin(); it != inventory.end(); ++it) {
 			if (index == indexToRemove) {
-				inventory.erase(it);
+				removeInventory(it->diaryImage);
 				break;
 			}
 			index++;
@@ -1555,7 +1579,10 @@ Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream)
 	inventory.clear();
 	uint32 size = stream->readUint32LE();
 	for (uint32 i = 0; i < size; ++i) {
-		inventory.push_back(stream->readString());
+		InventoryItem inv;
+		inv.diaryImage = stream->readString();
+		inv.flag = stream->readString();
+		inventory.push_back(inv);
 	}
 	_haveTakenItem = (inventory.size() > 1); // TODO: include this in save format
 
@@ -1671,8 +1698,10 @@ Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool is
 	}
 
 	stream->writeUint32LE(inventory.size());
-	for (NameList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
-		stream->writeString(*it);
+	for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
+		stream->writeString(it->diaryImage);
+		stream->writeByte(0);
+		stream->writeString(it->flag);
 		stream->writeByte(0);
 	}
 
@@ -2459,8 +2488,8 @@ void PrivateEngine::loadLocations(const Common::Rect &rect) {
 
 void PrivateEngine::loadInventory(uint32 x, const Common::Rect &r1, const Common::Rect &r2) {
 	int16 offset = 0;
-	for (NameList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
-		Graphics::Surface *surface = loadMask(*it, r1.left, r1.top + offset, true);
+	for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) {
+		Graphics::Surface *surface = loadMask(it->diaryImage, r1.left, r1.top + offset, true);
 		surface->free();
 		delete surface;
 		offset += 20;
diff --git a/engines/private/private.h b/engines/private/private.h
index cbe9678d03f..deb6908d080 100644
--- a/engines/private/private.h
+++ b/engines/private/private.h
@@ -147,6 +147,11 @@ typedef struct DiaryPage {
 	int locationID;
 } DiaryPage;
 
+typedef struct InventoryItem {
+	Common::String diaryImage;
+	Common::String flag;
+} InventoryItem;
+
 // funcs
 
 typedef struct FuncTable {
@@ -163,7 +168,7 @@ typedef Common::List<ExitInfo> ExitList;
 typedef Common::List<MaskInfo> MaskList;
 typedef Common::List<Common::String> SoundList;
 typedef Common::List<PhoneInfo> PhoneList;
-typedef Common::List<Common::String> InvList;
+typedef Common::List<InventoryItem> InvList;
 typedef Common::List<Common::Rect *> RectList;
 
 // arrays
@@ -352,6 +357,8 @@ public:
 	// Diary
 	InvList inventory;
 	bool inInventory(const Common::String &bmp) const;
+	void addInventory(const Common::String &bmp, Common::String &flag);
+	void removeInventory(const Common::String &bmp);
 	void removeRandomInventory();
 	Common::String _diaryLocPrefix;
 	void loadLocations(const Common::Rect &);


Commit: db4b2bb5afb8d37f6ed88d652782494f2dad8817
    https://github.com/scummvm/scummvm/commit/db4b2bb5afb8d37f6ed88d652782494f2dad8817
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-11-24T18:47:18+01:00

Commit Message:
PRIVATE: New save format with versioning

Changed paths:
  A engines/private/savegame.cpp
  A engines/private/savegame.h
    engines/private/module.mk
    engines/private/private.cpp


diff --git a/engines/private/module.mk b/engines/private/module.mk
index b1f187dbde7..44de2aabf26 100644
--- a/engines/private/module.mk
+++ b/engines/private/module.mk
@@ -9,6 +9,7 @@ MODULE_OBJS := \
 	lexer.o \
 	metaengine.o \
 	private.o \
+	savegame.o \
 	symbol.o
 
 MODULE_DIRS += \
diff --git a/engines/private/private.cpp b/engines/private/private.cpp
index 27c041bfce8..327c24bb878 100644
--- a/engines/private/private.cpp
+++ b/engines/private/private.cpp
@@ -43,6 +43,7 @@
 #include "private/decompiler.h"
 #include "private/grammar.h"
 #include "private/private.h"
+#include "private/savegame.h"
 #include "private/tokens.h"
 
 namespace Private {
@@ -1558,8 +1559,23 @@ Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream)
 	stopSound(true);
 	destroyVideo();
 
-	Common::Serializer s(stream, nullptr);
 	debugC(1, kPrivateDebugFunction, "loadGameStream");
+
+	// Read and validate metadata header
+	SavegameMetadata meta;
+	if (!readSavegameMetadata(stream, meta)) {
+		return Common::kReadingFailed;
+	}
+
+	// Log unexpected language or platform
+	if (meta.language != _language) {
+		warning("Save language %d different than game %d", meta.language, _language);
+	}
+	if (meta.platform != _platform) {
+		warning("Save platform  %d different than game %d", meta.platform, _platform);
+	}
+
+	Common::Serializer s(stream, nullptr);
 	int val;
 
 	for (NameList::iterator it = maps.variableList.begin(); it != maps.variableList.end(); ++it) {
@@ -1584,7 +1600,8 @@ Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream)
 		inv.flag = stream->readString();
 		inventory.push_back(inv);
 	}
-	_haveTakenItem = (inventory.size() > 1); // TODO: include this in save format
+	_toTake = (stream->readByte() == 1);
+	_haveTakenItem = (stream->readByte() == 1);
 
 	// Diary pages
 	_diaryPages.clear();
@@ -1614,6 +1631,15 @@ Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream)
 		addDossier(page1, page2);
 	}
 
+	// Police Bust
+	_policeBustEnabled = (stream->readByte() == 1);
+	_policeSirenPlayed = (stream->readByte() == 1);
+	_numberOfClicks = stream->readSint32LE();
+	_numberClicksAfterSiren = stream->readSint32LE();
+	_policeBustMovieIndex = stream->readSint32LE();
+	_policeBustMovie = stream->readString();
+	_policeBustPreviousSetting = stream->readString();
+
 	// Radios
 	size = stream->readUint32LE();
 	_AMRadio.clear();
@@ -1685,6 +1711,13 @@ Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool is
 	if (isAutosave)
 		return Common::kNoError;
 
+	// Metadata
+	SavegameMetadata meta;
+	meta.version = kCurrentSavegameVersion;
+	meta.language = _language;
+	meta.platform = _platform;
+	writeSavegameMetadata(stream, meta);
+
 	// Variables
 	for (NameList::const_iterator it = maps.variableList.begin(); it != maps.variableList.end(); ++it) {
 		const Private::Symbol *sym = maps.variables.getVal(*it);
@@ -1704,6 +1737,8 @@ Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool is
 		stream->writeString(it->flag);
 		stream->writeByte(0);
 	}
+	stream->writeByte(_toTake ? 1 : 0);
+	stream->writeByte(_haveTakenItem ? 1 : 0);
 
 	stream->writeUint32LE(_diaryPages.size());
 	for (uint i = 0; i < _diaryPages.size(); i++) {
@@ -1732,6 +1767,17 @@ Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool is
 		stream->writeByte(0);
 	}
 
+	// Police Bust
+	stream->writeByte(_policeBustEnabled ? 1 : 0);
+	stream->writeByte(_policeSirenPlayed ? 1 : 0);
+	stream->writeSint32LE(_numberOfClicks);
+	stream->writeSint32LE(_numberClicksAfterSiren);
+	stream->writeSint32LE(_policeBustMovieIndex);
+	stream->writeString(_policeBustMovie);
+	stream->writeByte(0);
+	stream->writeString(_policeBustPreviousSetting);
+	stream->writeByte(0);
+
 	// Radios
 	stream->writeUint32LE(_AMRadio.size());
 	for (SoundList::const_iterator it = _AMRadio.begin(); it != _AMRadio.end(); ++it) {
@@ -2457,13 +2503,7 @@ void PrivateEngine::loadLocations(const Common::Rect &rect) {
 		locationID++;
 	}
 	Common::sort(visitedLocations.begin(), visitedLocations.end(), [&locationIDs](const Symbol *a, const Symbol *b) {
-		if (a->u.val != b->u.val) {
-			return a->u.val < b->u.val;
-		} else {
-			// backwards compatibility for older saves files that stored 1
-			// for visited locations and displayed them in a fixed order.
-			return locationIDs[a] < locationIDs[b];
-		}
+		return a->u.val < b->u.val;
 	});
 
 	// Load the sorted visited locations
diff --git a/engines/private/savegame.cpp b/engines/private/savegame.cpp
new file mode 100644
index 00000000000..a73366b421a
--- /dev/null
+++ b/engines/private/savegame.cpp
@@ -0,0 +1,67 @@
+/* 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 "common/debug.h"
+#include "common/stream.h"
+
+#include "engines/private/savegame.h"
+
+namespace Private {
+
+static const uint32 kSavegameHeader = MKTAG('P','E','Y','E');
+
+void writeSavegameMetadata(Common::WriteStream *stream, const SavegameMetadata &meta) {
+	stream->writeUint32BE(kSavegameHeader);
+	stream->writeUint16LE(meta.version);
+	stream->writeSByte(meta.language);
+	stream->writeSByte(meta.platform);
+}
+
+bool readSavegameMetadata(Common::SeekableReadStream *stream, SavegameMetadata &meta) {
+	byte buffer[8];
+	stream->read(buffer, 8);
+	if (stream->eos() || stream->err()) {
+		return false;
+	}
+
+	uint32 header = READ_BE_UINT32(buffer);
+	if (header != kSavegameHeader) {
+		debugN(1, "Save does not have metadata header");
+		return false;
+	}
+
+	meta.version = READ_LE_UINT16(buffer + 4);
+	if (meta.version < kMinimumSavegameVersion) {
+		debugN("Save version %d lower than minimum %d", meta.version, kMinimumSavegameVersion);
+		return false;
+	}
+	if (meta.version > kCurrentSavegameVersion) {
+		debugN("Save version %d newer than current %d", meta.version, kCurrentSavegameVersion);
+		return false;
+	}
+
+	meta.language = (Common::Language)buffer[6];
+	meta.platform = (Common::Platform)buffer[7];
+
+	return true;
+}
+
+} // End of namespace Private
diff --git a/engines/private/savegame.h b/engines/private/savegame.h
new file mode 100644
index 00000000000..588bd103f48
--- /dev/null
+++ b/engines/private/savegame.h
@@ -0,0 +1,64 @@
+/* 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 PRIVATE_SAVEGAME_H
+#define PRIVATE_SAVEGAME_H
+
+#include "common/language.h"
+#include "common/platform.h"
+
+namespace Common {
+class SeekableReadStream;
+class WriteStream;
+}
+
+namespace Private {
+
+// Savegame format history:
+//
+// Version - new/changed feature
+// =============================
+//       1 - Metadata header and more game state (November 2025)
+//
+// Earlier versions did not have a header and not supported.
+
+const uint16 kCurrentSavegameVersion = 1;
+const uint16 kMinimumSavegameVersion = 1;
+
+struct SavegameMetadata {
+	uint16 version;
+	Common::Language language;
+	Common::Platform platform;
+};
+
+/**
+ * Write the header to a savegame.
+ */
+void writeSavegameMetadata(Common::WriteStream *stream, const SavegameMetadata &meta);
+
+/**
+ * Read the header from a savegame.
+ */
+bool readSavegameMetadata(Common::SeekableReadStream *stream, SavegameMetadata &meta);
+
+} // End of namespace Private
+
+#endif


Commit: c4646ef1dcb4bb973791ce20e3329048bb19e91d
    https://github.com/scummvm/scummvm/commit/c4646ef1dcb4bb973791ce20e3329048bb19e91d
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-11-24T18:47:18+01:00

Commit Message:
PRIVATE: Remove incompatible saves from listings

Changed paths:
    engines/private/metaengine.cpp


diff --git a/engines/private/metaengine.cpp b/engines/private/metaengine.cpp
index c45f54a2d95..bcf2aa24882 100644
--- a/engines/private/metaengine.cpp
+++ b/engines/private/metaengine.cpp
@@ -21,10 +21,12 @@
 
 #include "engines/advancedDetector.h"
 #include "graphics/scaler.h"
+#include "common/savefile.h"
 #include "common/translation.h"
 
 #include "private/private.h"
 #include "private/detection.h"
+#include "private/savegame.h"
 
 #include "backends/keymapper/action.h"
 #include "backends/keymapper/keymapper.h"
@@ -68,6 +70,7 @@ public:
 
 	Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
 	void getSavegameThumbnail(Graphics::Surface &thumb) override;
+	SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const override;
 	Common::KeymapArray initKeymaps(const char *target) const override;
 };
 
@@ -88,6 +91,35 @@ void PrivateMetaEngine::getSavegameThumbnail(Graphics::Surface &thumb) {
 	}
 }
 
+/**
+ * querySaveMetaInfos override that filters out saves with incompatible formats.
+ *
+ * The Private Eye save format was significantly changed to add more engine state.
+ * Older saves are incompatible, and we might have to change the format again.
+ * Save files now contain a version number in their header so that we can detect
+ * that a save is compatible, and not present incompatible saves to users.
+ */
+SaveStateDescriptor PrivateMetaEngine::querySaveMetaInfos(const char *target, int slot) const {
+	using namespace Private;
+	
+	SaveStateDescriptor desc = MetaEngine::querySaveMetaInfos(target, slot);
+	if (desc.getSaveSlot() == -1) {
+		return desc;
+	}
+
+	// Only saves with compatible metadata headers are allowed.
+	Common::ScopedPtr<Common::InSaveFile> f(g_system->getSavefileManager()->openForLoading(
+		getSavegameFile(slot, target)));
+	if (f) {
+		SavegameMetadata meta;
+		if (!readSavegameMetadata(f.get(), meta)) {
+			return SaveStateDescriptor();
+		}
+	}
+
+	return desc;
+}
+
 Common::KeymapArray PrivateMetaEngine::initKeymaps(const char *target) const {
 	using namespace Common;
 	using namespace Private;




More information about the Scummvm-git-logs mailing list