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

sev- noreply at scummvm.org
Mon Jul 21 12:13:37 UTC 2025


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

Summary:
087b6123b8 DETECTION: Add ADGF_ADDON flag
a80f5eb574 GOB: Add five Adibou2 add-ons to detection tables, with ADGF_ADDON flag
2e10337804 GUI: Ensure add-ons cannot be added as an independent game
3ab25845ae ENGINES: Add a warning message for not supported add-ons
66b134f400 DOCS: Mention "enable_unsupported_addon_warning" key
7ad9b0b20b GOB: Implement add-ons detection at game startup
e773b9443e GUI: Add-ons support in game launcher
807fd261f0 GUI: INTEGRITY: Support checking games with add-ons
b186077900 GOB: Add Adibou2 add-ons in game ids list
54824deb84 GOB: Add a few log messages in add-ons detection step
dcc50f046b GOB: Remove "extra" field in Adibou add-ons detection entries
e0634b8fd1 ENGINES: Move add-ons detection logic from GOB to base Engine class


Commit: 087b6123b8c20c5c8ab0e70dd123672ec8193137
    https://github.com/scummvm/scummvm/commit/087b6123b8c20c5c8ab0e70dd123672ec8193137
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
DETECTION: Add ADGF_ADDON flag

To be used with add-on games, that cannot be run independently without
their base game.

Changed paths:
    engines/advancedDetector.cpp
    engines/advancedDetector.h
    engines/game.h


diff --git a/engines/advancedDetector.cpp b/engines/advancedDetector.cpp
index fe1a469355a..c982003febd 100644
--- a/engines/advancedDetector.cpp
+++ b/engines/advancedDetector.cpp
@@ -162,6 +162,10 @@ static Common::String generatePreferredTarget(const ADGameDescription *desc, int
 		res = res + "-" + getLanguageCode(desc->language);
 	}
 
+	if (desc->flags & ADGF_ADDON) {
+		res = res + "-addon";
+	}
+
 	return res;
 }
 
@@ -208,6 +212,9 @@ DetectedGame AdvancedMetaEngineDetectionBase::toDetectedGame(const ADDetectedGam
 	else if (desc->flags & ADGF_WARNING)
 		game.gameSupportLevel = kWarningGame;
 
+	if (desc->flags & ADGF_ADDON)
+		game.isAddOn = true;
+
 	game.setGUIOptions(desc->guiOptions + _guiOptions);
 	game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(desc->language));
 
diff --git a/engines/advancedDetector.h b/engines/advancedDetector.h
index 74c36fae1cb..25717d1f24e 100644
--- a/engines/advancedDetector.h
+++ b/engines/advancedDetector.h
@@ -137,6 +137,7 @@ struct ADGameFileDescription {
  */
 enum ADGameFlags : uint {
 	ADGF_NO_FLAGS        =  0u,        ///< No flags.
+	ADGF_ADDON           = (1u << 15), ///< An add-on game, that cannot be run independently without its base game.
 	ADGF_TAILMD5         = (1u << 16), ///< Calculate the MD5 for this entry from the end of the file.
 	ADGF_AUTOGENTARGET   = (1u << 17), ///< Automatically generate gameid from @ref ADGameDescription::extra.
 	ADGF_UNSTABLE        = (1u << 18), ///< Flag to designate not yet officially supported games that are not fit for public testing.
diff --git a/engines/game.h b/engines/game.h
index e427dfd3167..8aa25c1ca81 100644
--- a/engines/game.h
+++ b/engines/game.h
@@ -186,6 +186,11 @@ struct DetectedGame {
 	 */
 	bool canBeAdded;
 
+	/**
+	 * The game is an add-on that cannot be run independently.
+	 */
+	bool isAddOn = false;
+
 	Common::String gameId;
 	Common::String preferredTarget;
 	Common::String description;


Commit: a80f5eb574fc7100047e4e6765034bb5ad7484aa
    https://github.com/scummvm/scummvm/commit/a80f5eb574fc7100047e4e6765034bb5ad7484aa
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GOB: Add five Adibou2 add-ons to detection tables, with ADGF_ADDON flag

Changed paths:
    engines/gob/detection/tables_adibou2.h


diff --git a/engines/gob/detection/tables_adibou2.h b/engines/gob/detection/tables_adibou2.h
index 4ef0f90ec5b..e828a048dce 100644
--- a/engines/gob/detection/tables_adibou2.h
+++ b/engines/gob/detection/tables_adibou2.h
@@ -278,6 +278,86 @@
 	0, 0, 0
 },
 
+// -- Add-ons : Read/Count 4-5 years --
+{
+	{
+		"adibou2readcount45",
+		"ADIBOU 2 - Lecture/Calcul 4-5 ans",
+		AD_ENTRY2s("intro_ap.stk", "7ff46d8c804186d3a11bf6b921fac2c0", 40835594,
+				   "appli_01.vmd",  "11635be4aeaac46d199e7e37cf905240", 54402),
+		FR_FRA,
+		kPlatformWindows,
+		ADGF_ADDON,
+		GUIO0()
+	},
+	kFeatures640x480,
+	0, 0, 0
+},
+
+// -- Add-ons : Read/Count 6-7 years --
+{
+	{
+		"adibou2readcount67",
+		"ADIBOU 2 - Lecture/Calcul 6-7 ans",
+		AD_ENTRY2s("intro_ap.stk", "0e91d0d693d5731353ad4738f4aa065c", 36540132,
+				   "appli_03.vmd", "6bf95a48f366bdf8af3a198c7b723c77", 58858),
+		FR_FRA,
+		kPlatformWindows,
+		ADGF_ADDON,
+		GUIO0()
+	},
+	kFeatures640x480,
+	0, 0, 0
+},
+
+// -- Add-ons : "Nature & Sciences" --
+{
+	{
+		"adibou2sciences",
+		"ADIBOU 2 - Je découvre la nature et les sciences",
+		AD_ENTRY2s("intro_ap.stk", "bff25481fc05bc5c6a3aaa8c17e89e5b", 3446050,
+				   "FICHES.ITK", "1670cc3373df162aed3219368665a1ca", 51025920),
+		FR_FRA,
+		kPlatformWindows,
+		ADGF_ADDON,
+		GUIO0()
+	},
+	kFeatures640x480,
+	0, 0, 0
+},
+
+// -- Add-ons : "Anglais" (English for non-native speakers) --
+{
+	{
+		"adibou2anglais",
+		"ADIBOU 2 - Anglais",
+		AD_ENTRY2s("intro_ap.stk", "1c83832cfeeace2a4b1b9ca448fc5322", 1967132,
+				   "LIPSYNC.ITK", "90ea1687c8d40989b5ff52c7ecaaf8b3", 107792384),
+		FR_FRA,
+		kPlatformWindows,
+		ADGF_ADDON | ADGF_UNSTABLE,
+		GUIO0()
+	},
+	kFeatures640x480,
+	0, 0, 0
+},
+
+// -- Add-ons : Music --
+{
+	{
+		"adibou2music",
+		"ADIBOU 2 - Musique",
+		AD_ENTRY2s("intro_ap.stk", "2147748e04ac11bd7155779e1456be07", 1631068,
+				   "MUZIKO.ITK", "101cd1690f13bf458e3988822a46e942", 54806528),
+		FR_FRA,
+		kPlatformWindows,
+		ADGF_ADDON | ADGF_UNSTABLE,
+		GUIO0()
+	},
+	kFeatures640x480,
+	0, 0, 0
+},
+
 // -- Demos --
 
 {


Commit: 2e10337804720739c0ee4459c3f3bef7c164da8b
    https://github.com/scummvm/scummvm/commit/2e10337804720739c0ee4459c3f3bef7c164da8b
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GUI: Ensure add-ons cannot be added as an independent game

Through single add or mass add

Changed paths:
    engines/engine.cpp
    engines/engine.h
    gui/launcher.cpp
    gui/massadd.cpp


diff --git a/engines/engine.cpp b/engines/engine.cpp
index f4dbbfeed6b..10fca8cfa88 100644
--- a/engines/engine.cpp
+++ b/engines/engine.cpp
@@ -814,6 +814,23 @@ bool Engine::warnUserAboutUnsupportedGame(Common::String msg) {
 	return true;
 }
 
+void Engine::errorAddingAddOnWithoutBaseGame(Common::String addOnName, Common::String gameId) {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->pushState();
+		g_gui.initTextToSpeech();
+	}
+
+	Common::U32String messageFormat = _("The game \"%s\" you are trying to add is an add-on for \"%s\" that cannot be run independently."
+		" Please copy the add-on contents into a subdirectory of the base game, and start the base game itself.");
+	Common::U32String message = Common::U32String::format(messageFormat, addOnName.c_str(), gameId.c_str());
+
+	GUI::MessageDialog(message).runModal();
+
+	if (ttsMan != nullptr)
+		ttsMan->popState();
+}
+
 void Engine::errorUnsupportedGame(Common::String extraMsg) {
 	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
 	if (ttsMan != nullptr) {
diff --git a/engines/engine.h b/engines/engine.h
index 90da7dcecb9..d60c11d434d 100644
--- a/engines/engine.h
+++ b/engines/engine.h
@@ -591,6 +591,16 @@ public:
 	 */
 	static bool warnUserAboutUnsupportedGame(Common::String msg = Common::String());
 
+	/**
+	/**
+	 * Display an error message to the user that the game is an add-on than cannot be
+	 * run independently.
+	 *
+	 * @param addOnName The name of the add-on.
+	 * @param gameId    The ID of the base game that this add-on requires.
+	 */
+	static void errorAddingAddOnWithoutBaseGame(Common::String addOnName, Common::String gameId);
+
 	/**
 	 * Display an error message to the user that the game is not supported.
 	 *
diff --git a/gui/launcher.cpp b/gui/launcher.cpp
index 6cbf4a91f15..de514473761 100644
--- a/gui/launcher.cpp
+++ b/gui/launcher.cpp
@@ -730,6 +730,11 @@ bool LauncherDialog::doGameDetection(const Common::Path &path) {
 
 	if (0 <= idx && idx < (int)candidates.size()) {
 		const DetectedGame &result = candidates[idx];
+		if (result.isAddOn) {
+			Engine::errorAddingAddOnWithoutBaseGame(result.description, result.gameId);
+			return true;
+		}
+
 		Common::String domain = EngineMan.createTargetForGame(result);
 
 		// Display edit dialog for the new entry
diff --git a/gui/massadd.cpp b/gui/massadd.cpp
index 1eddf6d9879..9aca458f1f5 100644
--- a/gui/massadd.cpp
+++ b/gui/massadd.cpp
@@ -199,7 +199,7 @@ void MassAddDialog::handleTickle() {
 		}
 
 		// Run the detector on the dir
-		DetectionResults detectionResults = EngineMan.detectGames(files, (ADGF_WARNING | ADGF_UNSUPPORTED), true);
+		DetectionResults detectionResults = EngineMan.detectGames(files, (ADGF_WARNING | ADGF_UNSUPPORTED | ADGF_ADDON), true);
 
 		if (detectionResults.foundUnknownGames()) {
 			Common::U32String report = detectionResults.generateUnknownGameReport(false, 80);


Commit: 3ab25845ae17556492ca0e4412936112a422da60
    https://github.com/scummvm/scummvm/commit/3ab25845ae17556492ca0e4412936112a422da60
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
ENGINES: Add a warning message for not supported add-ons

Changed paths:
    base/commandLine.cpp
    engines/engine.cpp
    engines/engine.h


diff --git a/base/commandLine.cpp b/base/commandLine.cpp
index a14faee4b81..a078cf7daca 100644
--- a/base/commandLine.cpp
+++ b/base/commandLine.cpp
@@ -332,6 +332,7 @@ void registerDefaults() {
 	ConfMan.registerDefault("cdrom", 0);
 
 	ConfMan.registerDefault("enable_unsupported_game_warning", true);
+	ConfMan.registerDefault("enable_unsupported_addon_warning", true);
 
 #ifdef USE_FLUIDSYNTH
 	ConfMan.registerDefault("soundfont", "Roland_SC-55.sf2");
diff --git a/engines/engine.cpp b/engines/engine.cpp
index 10fca8cfa88..c1178a174fe 100644
--- a/engines/engine.cpp
+++ b/engines/engine.cpp
@@ -814,6 +814,32 @@ bool Engine::warnUserAboutUnsupportedGame(Common::String msg) {
 	return true;
 }
 
+bool Engine::warnUserAboutUnsupportedAddOn(Common::String addOnName) {
+	if (ConfMan.getBool("enable_unsupported_addon_warning")) {
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if (ttsMan != nullptr) {
+			ttsMan->pushState();
+			g_gui.initTextToSpeech();
+		}
+
+		Common::U32String messageFormat = _("WARNING: the game you are about to start contains the add-on \"%s\""
+			" which is not yet fully supported by ScummVM. As such, it is likely to be unstable, and any saved"
+			" game you make might not work in future versions of ScummVM.");
+
+		Common::U32String message = Common::U32String::format(messageFormat, addOnName.c_str());
+
+		GUI::MessageDialog alert(message, _("Start anyway"), _("Cancel"));
+		int status = alert.runModal();
+
+		if (ttsMan != nullptr)
+			ttsMan->popState();
+
+		return status == GUI::kMessageOK;
+	}
+
+	return true;
+}
+
 void Engine::errorAddingAddOnWithoutBaseGame(Common::String addOnName, Common::String gameId) {
 	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
 	if (ttsMan != nullptr) {
diff --git a/engines/engine.h b/engines/engine.h
index d60c11d434d..c6e8651caf8 100644
--- a/engines/engine.h
+++ b/engines/engine.h
@@ -592,6 +592,15 @@ public:
 	static bool warnUserAboutUnsupportedGame(Common::String msg = Common::String());
 
 	/**
+	 * Display a warning to the user that the game contains an add-not which is not
+	 * fully supported.
+	 *
+	 * @param addOnName The name of the add-on.
+	 *
+	 * @return True if the user chooses to start anyway, false otherwise.
+	 */
+	static bool warnUserAboutUnsupportedAddOn(Common::String addOnName);
+
 	/**
 	 * Display an error message to the user that the game is an add-on than cannot be
 	 * run independently.


Commit: 66b134f400fb4d85bd35b58b29a007a0f95b3e75
    https://github.com/scummvm/scummvm/commit/66b134f400fb4d85bd35b58b29a007a0f95b3e75
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
DOCS: Mention "enable_unsupported_addon_warning" key

Changed paths:
    doc/docportal/advanced_topics/configuration_file.rst


diff --git a/doc/docportal/advanced_topics/configuration_file.rst b/doc/docportal/advanced_topics/configuration_file.rst
index 34a29cc0e72..5258985a29d 100755
--- a/doc/docportal/advanced_topics/configuration_file.rst
+++ b/doc/docportal/advanced_topics/configuration_file.rst
@@ -184,6 +184,7 @@ There are many recognized configuration keys. In the table below, each key is ei
 		":ref:`enable_video_upscale <upscale>`",boolean,true,
 		":ref:`enable_tts <ttsenabled>`",boolean,false,
 		enable_unsupported_game_warning,boolean,true, Shows a warning when adding a game that is unsupported.
+		enable_unsupported_addon_warning,boolean,true, Shows a warning when starting a game including an add-on that is unsupported.
 		":ref:`extended_timer <extended>`",boolean,false,
 		extra,string, ,"Shows additional information about a game, such as version"
 		":ref:`english_speech <english>`",boolean,false,


Commit: 7ad9b0b20bf4f263059c419e832bf1ee83e3de65
    https://github.com/scummvm/scummvm/commit/7ad9b0b20bf4f263059c419e832bf1ee83e3de65
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GOB: Implement add-ons detection at game startup

The add-ons are registered in hidden games entries, that cannot be run,
and refer their parent game via a "parent=" key.

When starting a game known to contain add-ons, the following steps are
performed:

- If a subdirectory of the game directory matches an add-on from the
detection tables, we add a new entry for it (unless it already exists),
with proper "parent" key. If this add-on is marked as unstable, a warning
message is displayed

- If the path of an existing add-on entry does not exists anymore, the
entry is purged.

Changed paths:
    engines/gob/gob.h
    engines/gob/metaengine.cpp


diff --git a/engines/gob/gob.h b/engines/gob/gob.h
index 29a862e2d42..a35df455b98 100644
--- a/engines/gob/gob.h
+++ b/engines/gob/gob.h
@@ -87,6 +87,8 @@
  * - Croustibat
  */
 
+class GobMetaEngine;
+
 namespace Gob {
 
 class Game;
@@ -258,7 +260,12 @@ public:
 	~GobEngine() override;
 
 	void initGame(const GOBGameDescription *gd);
+	Common::ErrorCode updateAddOns(const GobMetaEngine *metaEngine, const GOBGameDescription *gd) const;
+
 	GameType getGameType(const char *gameId) const;
+	bool gameTypeHasAddOns();
+	bool dirCanBeGameAddOn(Common::FSDirectory dir) const;
+	bool dirMustBeGameAddOn(Common::FSDirectory dir) const;
 
 	/**
 	 * Used to obtain the game version as a fallback
diff --git a/engines/gob/metaengine.cpp b/engines/gob/metaengine.cpp
index d2dc956f3f5..c5ccebf85bb 100644
--- a/engines/gob/metaengine.cpp
+++ b/engines/gob/metaengine.cpp
@@ -27,8 +27,15 @@
 
 #include "engines/advancedDetector.h"
 
+#include "common/config-manager.h"
+#include "common/hashmap.h"
+
 #include "common/translation.h"
 
+#include "gui/chooser.h"
+#include "gui/message.h"
+#include "gui/unknown-game-dialog.h"
+
 #include "gob/gameidtotype.h"
 #include "gob/gob.h"
 
@@ -75,9 +82,16 @@ bool Gob::GobEngine::hasFeature(EngineFeature f) const {
 }
 
 Common::Error GobMetaEngine::createInstance(OSystem *syst, Engine **engine, const Gob::GOBGameDescription *gd) const {
-	*engine = new Gob::GobEngine(syst);
-	((Gob::GobEngine *)*engine)->initGame(gd);
-	return Common::kNoError;
+
+	Gob::GobEngine *gobEngine = new Gob::GobEngine(syst);
+	*engine = gobEngine;
+	gobEngine->initGame(gd);
+	Common::ErrorCode errorCode = Common::kNoError;
+
+	if (gobEngine->gameTypeHasAddOns())
+		errorCode = gobEngine->updateAddOns(this, gd);
+
+	return errorCode;
 }
 
 
@@ -101,6 +115,158 @@ GameType GobEngine::getGameType(const char *gameId) const {
 	error("Unknown game ID: %s", gameId);
 }
 
+bool GobEngine::gameTypeHasAddOns() {
+	return  getGameType() == kGameTypeAdibou1 ||
+			getGameType() == kGameTypeAdibou2 ||
+			getGameType() == kGameTypeAdi2 ||
+			getGameType() == kGameTypeAdi4;
+}
+
+
+// Accelerator, to discard some directories we know have no chance to be add-ons
+bool GobEngine::dirCanBeGameAddOn(Common::FSDirectory dir) const {
+	if (getGameType() == kGameTypeAdibou2)
+		return dir.hasFile("intro_ap.stk");
+
+	return true;
+}
+
+// To display a warning if a directory likely to be an add-on does not match anything
+bool GobEngine::dirMustBeGameAddOn(Common::FSDirectory dir) const {
+	if (getGameType() == kGameTypeAdibou2)
+		return dir.hasFile("intro_ap.stk");
+
+	return false;
+}
+
+Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const GOBGameDescription *gd) const {
+	const Plugin *detectionPlugin = EngineMan.findDetectionPlugin(metaEngine->getName());
+	if (!detectionPlugin) {
+		warning("Engine plugin for GOB not present. Add-ons detection is disabled");
+		return Common::kNoError;
+	}
+
+	// Update silently the targets associated with the add-ons, unless some unsupported version is detected
+
+	// List already registered add-ons for this game, and detect removed ones
+	Common::ConfigManager::DomainMap::iterator iter = ConfMan.beginGameDomains();
+	Common::HashMap<Common::Path, bool, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> existingAddOnsPaths;
+
+	bool anyAddOnRemoved = false;
+	for (; iter != ConfMan.endGameDomains(); ++iter) {
+		Common::String name(iter->_key);
+		Common::ConfigManager::Domain &dom = iter->_value;
+
+		Common::String parent;
+		if (dom.tryGetVal("parent", parent) && parent == ConfMan.getActiveDomainName()) {
+			// Existing add-on, check if its path still exists
+			Common::Path addOnPath(Common::Path::fromConfig(dom.getVal("path")));
+			if (addOnPath.empty() || !Common::FSNode(addOnPath).isDirectory()) {
+				// Path does not exist, remove the add-on
+				ConfMan.removeGameDomain(name);
+				anyAddOnRemoved = true;
+			} else {
+				existingAddOnsPaths[addOnPath] = true;
+			}
+		}
+	}
+
+	if (anyAddOnRemoved)
+		ConfMan.flushToDisk();
+
+	// Look for newly added add-ons
+	bool anyAddOnAdded = false;
+	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
+	Common::FSList subdirNodes;
+	gameDataDir.getChildren(subdirNodes, Common::FSNode::kListDirectoriesOnly);
+	for (const Common::FSNode &subdirNode : subdirNodes) {
+		Common::FSDirectory subdir(subdirNode);
+		if (dirCanBeGameAddOn(subdir)) {
+			Common::FSList files;
+			if (!subdirNode.getChildren(files, Common::FSNode::kListAll))
+				continue;
+
+			ADCacheMan.clear();
+
+			DetectedGames detectedGames = detectionPlugin->get<MetaEngineDetection>().detectGames(files);
+			DetectedGames detectedAddOns;
+			for (DetectedGame &game : detectedGames) {
+				if (game.isAddOn) {
+					detectedAddOns.push_back(game);
+				}
+			}
+
+			int idx = 0;
+			if (detectedAddOns.empty() && dirMustBeGameAddOn(subdir)) {
+				Common::U32String msgFormat(_("The directory '%s' looks like an add-on for the game '%s', but ScummVM could not find any matching add-on in it."));
+				Common::U32String msg = Common::U32String::format(msgFormat,
+																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str(),
+																  gd->desc.gameId);
+
+				GUI::MessageDialog alert(msg);
+				alert.runModal();
+				continue;
+			} else if (detectedAddOns.size() == 1) {
+				// Exact match
+				idx = 0;
+			} else {
+				// Display the candidates to the user and let her/him pick one
+				Common::U32StringArray list;
+				for (idx = 0; idx < (int)detectedAddOns.size(); idx++) {
+					Common::U32String description = detectedAddOns[idx].description;
+
+					if (detectedAddOns[idx].hasUnknownFiles) {
+						description += Common::U32String(" - ");
+						// Unknown game variant
+						description += _("Unknown variant");
+					}
+
+					list.push_back(description);
+				}
+
+				Common::U32String msgFormat(_("Directory '%s' matches several add-ons, please pick one."));
+				Common::U32String msg = Common::U32String::format(msgFormat,
+																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+
+				GUI::ChooserDialog dialog(msg);
+				dialog.setList(list);
+				idx = dialog.runModal();
+				if (idx < 0)
+					return Common::kUserCanceled;
+			}
+
+			if (0 <= idx && idx < (int)detectedAddOns.size()) {
+				DetectedGame &selectedAddOn = detectedAddOns[idx];
+				selectedAddOn.path = subdirNode.getPath();
+				selectedAddOn.shortPath = subdirNode.getDisplayName();
+
+				if (selectedAddOn.hasUnknownFiles) {
+					GUI::UnknownGameDialog dialog(selectedAddOn);
+					dialog.runModal();
+					continue; // Do not create an entry for unknown variants
+				}
+
+				if (selectedAddOn.gameSupportLevel != kStableGame) {
+					if (!warnUserAboutUnsupportedAddOn(selectedAddOn.description)) {
+						return Common::kUserCanceled;
+					}
+				}
+
+				if (!existingAddOnsPaths.contains(subdirNode.getPath())) {
+					Common::String domain = EngineMan.createTargetForGame(selectedAddOn);
+					ConfMan.set("parent", ConfMan.getActiveDomainName(), domain);
+					anyAddOnAdded = true;
+				}
+			}
+		}
+	}
+
+	if (anyAddOnAdded)
+		ConfMan.flushToDisk();
+
+	return Common::kNoError;
+}
+
 void GobEngine::initGame(const GOBGameDescription *gd) {
 	if (gd->startTotBase == nullptr)
 		_startTot = "intro.tot";


Commit: e773b9443ee89332752a1bafe6565d3148c8e61c
    https://github.com/scummvm/scummvm/commit/e773b9443ee89332752a1bafe6565d3148c8e61c
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GUI: Add-ons support in game launcher

- Display add-ons greyed out under their base game in list mode (unless
some grouping is active)
- Add an "- Add-on" suffix to add'ons' description
- Hide add-ons in grid mode
- Ensure add-ons cannot be run, edited or removed from the launcher
- Ensure removing the base game also removes all its add-ons

Changed paths:
    gui/launcher.cpp
    gui/launcher.h


diff --git a/gui/launcher.cpp b/gui/launcher.cpp
index de514473761..354d1a1fe5b 100644
--- a/gui/launcher.cpp
+++ b/gui/launcher.cpp
@@ -463,6 +463,14 @@ void LauncherDialog::removeGame(int item) {
 		assert(item >= 0);
 		ConfMan.removeGameDomain(_domains[item]);
 
+		// Remove all the add-ons for this game
+		const Common::ConfigManager::DomainMap &domains = ConfMan.getGameDomains();
+		for (const auto &domain : domains) {
+			if (domain._value.getValOrDefault("parent") == _domains[item]) {
+				ConfMan.removeGameDomain(domain._key);
+			}
+		}
+
 		// Write config to disk
 		ConfMan.flushToDisk();
 
@@ -566,13 +574,16 @@ void LauncherDialog::loadGame(int item) {
 	PluginMan.loadDetectionPlugin(); // only for uncached manager
 }
 
-Common::Array<LauncherEntry> LauncherDialog::generateEntries(const Common::ConfigManager::DomainMap &domains) {
+Common::Array<LauncherEntry> LauncherDialog::generateEntries(const Common::ConfigManager::DomainMap &domains, bool skipAddOns) {
 	Common::Array<LauncherEntry> domainList;
 	for (const auto &domain : domains) {
 		// Do not list temporary targets added when starting a game from the command line
 		if (domain._value.contains("id_came_from_command_line"))
 			continue;
 
+		if (skipAddOns && domain._value.contains("parent"))
+			continue;
+
 		Common::String description;
 		Common::String title;
 
@@ -761,6 +772,11 @@ bool LauncherDialog::doGameDetection(const Common::Path &path) {
 
 void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
 	int item = getSelected();
+	bool isAddOn = false;
+	if (item >= 0) {
+		Common::ConfigManager::Domain *domain = ConfMan.getDomain(_domains[item]);
+		isAddOn = domain && domain->contains("parent");
+	}
 
 	switch (cmd) {
 	case kAddGameCmd:
@@ -777,15 +793,15 @@ void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 dat
 		break;
 #endif
 	case kRemoveGameCmd:
-		if (item < 0) return;
+		if (item < 0 || isAddOn) return;
 		removeGame(item);
 		break;
 	case kEditGameCmd:
-		if (item < 0) return;
+		if (item < 0 || isAddOn) return;
 		editGame(item);
 		break;
 	case kLoadGameCmd:
-		if (item < 0) return;
+		if (item < 0 || isAddOn) return;
 		loadGame(item);
 		break;
 #ifdef ENABLE_EVENTRECORDER
@@ -806,7 +822,7 @@ void LauncherDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 dat
 		break;
 	case kStartCmd:
 		// Start the selected game.
-		if (item < 0) return;
+		if (item < 0 || isAddOn) return;
 		ConfMan.setActiveDomain(_domains[item]);
 		close();
 		break;
@@ -1121,7 +1137,36 @@ void LauncherSimple::updateListing(int selPos) {
 	const bool scanEntries = (numEntries == -1) || ((int)domains.size() <= numEntries);
 
 	// Turn it into a sorted list of entries
-	Common::Array<LauncherEntry> domainList = generateEntries(domains);
+	Common::Array<LauncherEntry> domainList = generateEntries(domains, false);
+	Common::HashMap<Common::String, Common::Array<const LauncherEntry*>> addOnsMap;
+
+	for (const auto &curDomain : domainList) {
+		Common::String parentDomain;
+		if (curDomain.domain->tryGetVal("parent", parentDomain)) {
+			Common::Array<const LauncherEntry*> &gameAddOns = addOnsMap.getOrCreateVal(parentDomain);
+			gameAddOns.push_back(&curDomain);
+		}
+	}
+
+	if (!addOnsMap.empty()) {
+		// Rebuild the list by adding add-ons just next to their parent game
+		Common::Array<LauncherEntry> newDomainList;
+		for (const auto &curDomain : domainList) {
+			if (curDomain.domain->contains("parent"))
+				continue;
+
+			newDomainList.push_back(curDomain);
+			Common::Array<const LauncherEntry*> gameAddOns;
+			if (addOnsMap.tryGetVal(curDomain.key, gameAddOns)) {
+				// Add add-ons for this game
+				for (const auto *addOn : gameAddOns) {
+					newDomainList.push_back(*addOn);
+				}
+			}
+		}
+
+		domainList = newDomainList;
+	}
 
 	// And fill out our structures
 	for (const auto &curDomain : domainList) {
@@ -1138,8 +1183,16 @@ void LauncherSimple::updateListing(int selPos) {
 				// description += Common::String::format(" (%s)", _("Not found"));
 			}
 		}
+
+		bool isAddOn = curDomain.domain->contains("parent");
+		if (isAddOn)
+			color = ThemeEngine::kFontColorAlternate;
+
 		Common::U32String gameDesc = GUI::ListWidget::getThemeColor(color) + Common::U32String(curDomain.description);
 
+		if (isAddOn)
+			gameDesc += Common::U32String(" - ") + _("Add-on");
+
 		l.push_back(gameDesc);
 		_domains.push_back(curDomain.key);
 	}
@@ -1339,16 +1392,22 @@ void LauncherSimple::handleCommand(CommandSender *sender, uint32 cmd, uint32 dat
 }
 
 void LauncherSimple::updateButtons() {
-	bool enable = (_list->getSelected() >= 0);
+	int item = _list->getSelected();
+	bool isAddOn = false;
+	if (item >= 0) {
+		const Common::ConfigManager::Domain *domain = ConfMan.getDomain(_domains[item]);
+		isAddOn = domain && domain->contains("parent");
+	}
+
+	bool enable = (item >= 0 && !isAddOn);
 
 	_startButton->setEnabled(enable);
 	_editButton->setEnabled(enable);
 	_removeButton->setEnabled(enable);
 
-	int item = _list->getSelected();
 	bool en = enable;
 
-	if (item >= 0)
+	if (item >= 0  && !isAddOn)
 		en = !(Common::checkGameGUIOption(GUIO_NOLAUNCHLOAD, ConfMan.get("guioptions", _domains[item])));
 
 	_loadButton->setEnabled(en);
@@ -1558,7 +1617,7 @@ void LauncherGrid::updateListing(int selPos) {
 	const Common::ConfigManager::DomainMap &domains = ConfMan.getGameDomains();
 
 	// Turn it into a sorted list of entries
-	Common::Array<LauncherEntry> domainList = generateEntries(domains);
+	Common::Array<LauncherEntry> domainList = generateEntries(domains, true);
 
 	Common::Array<GridItemInfo> gridList;
 
diff --git a/gui/launcher.h b/gui/launcher.h
index 2831f6ae3a9..cc98bb92d69 100644
--- a/gui/launcher.h
+++ b/gui/launcher.h
@@ -202,7 +202,7 @@ protected:
 	 */
 	void loadGame(int item);
 
-	Common::Array<LauncherEntry> generateEntries(const Common::ConfigManager::DomainMap &domains);
+	Common::Array<LauncherEntry> generateEntries(const Common::ConfigManager::DomainMap &domains, bool skipAddOns);
 
 	/**
 	 * Select the target with the given name in the launcher game list.


Commit: 807fd261f070962d7c057b5a0148d39a0b33280a
    https://github.com/scummvm/scummvm/commit/807fd261f070962d7c057b5a0148d39a0b33280a
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GUI: INTEGRITY: Support checking games with add-ons

- If a game has add-ons, display a dialog to let the user choose which
  part should be checked
- When checking the base game, ignore the add-ons directories
- When checking an add-on, use its directory as root

Changed paths:
    gui/integrity-dialog.cpp
    gui/integrity-dialog.h


diff --git a/gui/integrity-dialog.cpp b/gui/integrity-dialog.cpp
index d54d9a29b77..0806228ce24 100644
--- a/gui/integrity-dialog.cpp
+++ b/gui/integrity-dialog.cpp
@@ -30,6 +30,7 @@
 #include "common/tokenizer.h"
 #include "common/translation.h"
 
+#include "gui/chooser.h"
 #include "gui/gui-manager.h"
 #include "gui/launcher.h"
 #include "gui/message.h"
@@ -71,6 +72,7 @@ struct ChecksumDialogState {
 
 	Common::String endpoint;
 	Common::Path gamePath;
+	Common::HashMap<Common::Path, bool, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> ignoredSubdirsMap;
 	Common::String gameid;
 	Common::String engineid;
 	Common::String extra;
@@ -126,6 +128,48 @@ IntegrityDialog::IntegrityDialog(Common::String endpoint, Common::String domain)
 		g_checksum_state = new ChecksumDialogState();
 		g_checksum_state->dialog = this;
 
+		Common::Array<Common::String> gameAddOns;
+
+		Common::ConfigManager::DomainMap::iterator iter = ConfMan.beginGameDomains();
+		for (; iter != ConfMan.endGameDomains(); ++iter) {
+			Common::String name(iter->_key);
+			Common::ConfigManager::Domain &dom = iter->_value;
+
+			Common::String parent;
+			if (dom.tryGetVal("parent", parent) && parent == domain)
+				gameAddOns.push_back(name);
+		}
+
+		if (!gameAddOns.empty()) {
+			// Ask the user to choose between the base game or one of its add-ons
+			Common::U32StringArray list;
+			list.push_back(ConfMan.get("description", domain));
+
+			for (Common::String &gameAddOn : gameAddOns) {
+				list.push_back(ConfMan.get("description", gameAddOn));
+			}
+
+			ChooserDialog dialog(_("This game includes add-ons, pick the part you want to be checked:"));
+			dialog.setList(list);
+			int idx = dialog.runModal();
+			if (idx < 0) {
+				// User cancelled the dialog
+				_close = true;
+				return;
+			}
+
+			if (idx >= 1 && idx < (int)gameAddOns.size() + 1) {
+				// User selected an add-on, change the selected domain
+				domain = gameAddOns[idx - 1];
+			} else {
+				// User selected the base game, ignore the add-ons subdirectories
+				for (Common::String &gameAddOn : gameAddOns) {
+					Common::Path addOnPath = ConfMan.getPath("path", gameAddOn);
+					g_checksum_state->ignoredSubdirsMap[addOnPath] = true;
+				}
+			}
+		}
+
 		setState(kChecksumStateCalculating);
 		refreshWidgets();
 
@@ -136,7 +180,7 @@ IntegrityDialog::IntegrityDialog(Common::String endpoint, Common::String domain)
 		g_checksum_state->extra = ConfMan.get("extra", domain);
 		g_checksum_state->platform = ConfMan.get("platform", domain);
 		g_checksum_state->language = ConfMan.get("language", domain);
-		calculateTotalSize(g_checksum_state->gamePath);
+		calculateTotalSize(g_checksum_state->gamePath, g_checksum_state->ignoredSubdirsMap);
 	} else {
 		g_checksum_state->dialog = this;
 
@@ -311,7 +355,7 @@ void IntegrityDialog::setError(Common::U32String &msg) {
 	_cancelButton->setCmd(kCleanupCmd);
 }
 
-void IntegrityDialog::calculateTotalSize(Common::Path gamePath) {
+void IntegrityDialog::calculateTotalSize(Common::Path gamePath, const Common::HashMap<Common::Path, bool, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> &ignoredSubdirsMap) {
 	const Common::FSNode dir(gamePath);
 
 	if (!dir.exists() || !dir.isDirectory())
@@ -326,9 +370,10 @@ void IntegrityDialog::calculateTotalSize(Common::Path gamePath) {
 
 	// Process the files and subdirectories in the current directory recursively
 	for (const auto &entry : fileList) {
-		if (entry.isDirectory())
-			calculateTotalSize(entry.getPath());
-		else {
+		if (entry.isDirectory()) {
+			if (!ignoredSubdirsMap.contains(entry.getPath()))
+				calculateTotalSize(entry.getPath(), ignoredSubdirsMap);
+		} else {
 			Common::File file;
 			if (!file.open(entry))
 				continue;
@@ -407,7 +452,8 @@ Common::Array<Common::StringArray> IntegrityDialog::generateChecksums(Common::Pa
 			continue;
 
 		if (entry.isDirectory()) {
-			generateChecksums(entry.getPath(), fileChecksums, gamePath);
+			if (!g_checksum_state->ignoredSubdirsMap.contains(entry.getPath()))
+				generateChecksums(entry.getPath(), fileChecksums, gamePath);
 
 			continue;
 		}
diff --git a/gui/integrity-dialog.h b/gui/integrity-dialog.h
index 2b917eef92e..c2b3b0987f5 100644
--- a/gui/integrity-dialog.h
+++ b/gui/integrity-dialog.h
@@ -83,7 +83,7 @@ public:
 	void checksumResponseCallback(const Common::JSONValue *r);
 	void errorCallback(const Networking::ErrorResponse &error);
 
-	void calculateTotalSize(Common::Path gamePath);
+	void calculateTotalSize(Common::Path gamePath, const Common::HashMap<Common::Path, bool, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> &ignoredSubdirsMap);
 
 	Common::Array<Common::StringArray> generateChecksums(Common::Path currentPath, Common::Array<Common::StringArray> &fileChecksums, Common::Path gamePath);
 	Common::JSONValue *generateJSONRequest(Common::Path gamePath, Common::String gameid, Common::String engineid, Common::String extra, Common::String platform, Common::String language);


Commit: b186077900333af97823064c48bdcb0f3de4ec95
    https://github.com/scummvm/scummvm/commit/b186077900333af97823064c48bdcb0f3de4ec95
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GOB: Add Adibou2 add-ons in game ids list

Changed paths:
    engines/gob/detection/tables.h


diff --git a/engines/gob/detection/tables.h b/engines/gob/detection/tables.h
index 162533e2ba7..3c96451ef89 100644
--- a/engines/gob/detection/tables.h
+++ b/engines/gob/detection/tables.h
@@ -74,6 +74,11 @@ static const PlainGameDescriptor gobGames[] = {
 	{"adi5", "ADI 5"},
 	{"adibou1", "Adibou 1"},
 	{"adibou2", "Adibou 2"},
+	{"adibou2readcount45", "Adibou 2 Read/Count 4-5 years"},
+	{"adibou2readcount67", "Adibou 2 Read/Count 6-7 years"},
+	{"adibou2sciences", "Adibou 2 Nature & Sciences"},
+	{"adibou2anglais", "Adibou 2 Anglais"},
+	{"adibou2music", "Adibou 2 Music"},
 	{"adibou3", "Adibou 3"},
 	{"adiboucuisine", "Adibou présente la Cuisine"},
 	{"adiboudessin", "Adibou présente le Dessin"},


Commit: 54824deb8424102f1afcfc746958c4b09d5972f6
    https://github.com/scummvm/scummvm/commit/54824deb8424102f1afcfc746958c4b09d5972f6
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GOB: Add a few log messages in add-ons detection step

Changed paths:
    engines/gob/metaengine.cpp


diff --git a/engines/gob/metaengine.cpp b/engines/gob/metaengine.cpp
index c5ccebf85bb..ec2e423d6b2 100644
--- a/engines/gob/metaengine.cpp
+++ b/engines/gob/metaengine.cpp
@@ -150,7 +150,7 @@ Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const
 
 	// List already registered add-ons for this game, and detect removed ones
 	Common::ConfigManager::DomainMap::iterator iter = ConfMan.beginGameDomains();
-	Common::HashMap<Common::Path, bool, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> existingAddOnsPaths;
+	Common::HashMap<Common::Path, Common::String, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> existingAddOnsPaths;
 
 	bool anyAddOnRemoved = false;
 	for (; iter != ConfMan.endGameDomains(); ++iter) {
@@ -163,10 +163,14 @@ Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const
 			Common::Path addOnPath(Common::Path::fromConfig(dom.getVal("path")));
 			if (addOnPath.empty() || !Common::FSNode(addOnPath).isDirectory()) {
 				// Path does not exist, remove the add-on
+				debug("Removing entry of deleted add-on '%s' (former path: '%s')",
+					  name.c_str(),
+					  addOnPath.toString(Common::Path::kNativeSeparator).c_str());
+
 				ConfMan.removeGameDomain(name);
 				anyAddOnRemoved = true;
 			} else {
-				existingAddOnsPaths[addOnPath] = true;
+				existingAddOnsPaths[addOnPath] = name;
 			}
 		}
 	}
@@ -241,6 +245,9 @@ Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const
 				selectedAddOn.shortPath = subdirNode.getDisplayName();
 
 				if (selectedAddOn.hasUnknownFiles) {
+					debug("Detected an unknown variant of add-on '%s' (path: '%s')",
+						  selectedAddOn.gameId.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
 					GUI::UnknownGameDialog dialog(selectedAddOn);
 					dialog.runModal();
 					continue; // Do not create an entry for unknown variants
@@ -252,8 +259,16 @@ Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const
 					}
 				}
 
-				if (!existingAddOnsPaths.contains(subdirNode.getPath())) {
+				Common::String addOnName;
+				if (existingAddOnsPaths.tryGetVal(subdirNode.getPath(), addOnName)) {
+					debug("Detected existing add-on '%s' (path: '%s')",
+						  addOnName.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+				} else {
 					Common::String domain = EngineMan.createTargetForGame(selectedAddOn);
+					debug("Detected new add-on '%s' (path: '%s')",
+						  domain.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
 					ConfMan.set("parent", ConfMan.getActiveDomainName(), domain);
 					anyAddOnAdded = true;
 				}


Commit: dcc50f046b5c8640f4326c8f15a73f8426d16f9a
    https://github.com/scummvm/scummvm/commit/dcc50f046b5c8640f4326c8f15a73f8426d16f9a
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
GOB: Remove "extra" field in Adibou add-ons detection entries

It lead to too long labels in launcher list.

Changed paths:
    engines/gob/detection/tables_adibou2.h


diff --git a/engines/gob/detection/tables_adibou2.h b/engines/gob/detection/tables_adibou2.h
index e828a048dce..7feb723f57c 100644
--- a/engines/gob/detection/tables_adibou2.h
+++ b/engines/gob/detection/tables_adibou2.h
@@ -282,7 +282,7 @@
 {
 	{
 		"adibou2readcount45",
-		"ADIBOU 2 - Lecture/Calcul 4-5 ans",
+		"", // "Lecture/Calcul 4-5 ans"
 		AD_ENTRY2s("intro_ap.stk", "7ff46d8c804186d3a11bf6b921fac2c0", 40835594,
 				   "appli_01.vmd",  "11635be4aeaac46d199e7e37cf905240", 54402),
 		FR_FRA,
@@ -298,7 +298,7 @@
 {
 	{
 		"adibou2readcount67",
-		"ADIBOU 2 - Lecture/Calcul 6-7 ans",
+		"", // "Lecture/Calcul 6-7 ans"
 		AD_ENTRY2s("intro_ap.stk", "0e91d0d693d5731353ad4738f4aa065c", 36540132,
 				   "appli_03.vmd", "6bf95a48f366bdf8af3a198c7b723c77", 58858),
 		FR_FRA,
@@ -314,7 +314,7 @@
 {
 	{
 		"adibou2sciences",
-		"ADIBOU 2 - Je découvre la nature et les sciences",
+		"", // "Je découvre la nature et les sciences"
 		AD_ENTRY2s("intro_ap.stk", "bff25481fc05bc5c6a3aaa8c17e89e5b", 3446050,
 				   "FICHES.ITK", "1670cc3373df162aed3219368665a1ca", 51025920),
 		FR_FRA,
@@ -330,7 +330,7 @@
 {
 	{
 		"adibou2anglais",
-		"ADIBOU 2 - Anglais",
+		"",
 		AD_ENTRY2s("intro_ap.stk", "1c83832cfeeace2a4b1b9ca448fc5322", 1967132,
 				   "LIPSYNC.ITK", "90ea1687c8d40989b5ff52c7ecaaf8b3", 107792384),
 		FR_FRA,
@@ -346,7 +346,7 @@
 {
 	{
 		"adibou2music",
-		"ADIBOU 2 - Musique",
+		"",
 		AD_ENTRY2s("intro_ap.stk", "2147748e04ac11bd7155779e1456be07", 1631068,
 				   "MUZIKO.ITK", "101cd1690f13bf458e3988822a46e942", 54806528),
 		FR_FRA,


Commit: e0634b8fd1f693b65ecc13ad1a4919fb3423636d
    https://github.com/scummvm/scummvm/commit/e0634b8fd1f693b65ecc13ad1a4919fb3423636d
Author: Simon Delamarre (simon.delamarre14 at gmail.com)
Date: 2025-07-21T14:13:28+02:00

Commit Message:
ENGINES: Move add-ons detection logic from GOB to base Engine class

Changed paths:
    base/main.cpp
    engines/engine.cpp
    engines/engine.h
    engines/gob/gob.h
    engines/gob/metaengine.cpp


diff --git a/base/main.cpp b/base/main.cpp
index 4ebcea52ad8..652b5881677 100644
--- a/base/main.cpp
+++ b/base/main.cpp
@@ -217,6 +217,12 @@ static Common::Error runGame(const Plugin *enginePlugin, OSystem &system, const
 		err = metaEngine.createInstance(&system, &engine, game, meDescriptor);
 	}
 
+	if (err.getCode() == Common::kNoError) {
+		// Update add-on targets
+		if (engine != nullptr && engine->gameTypeHasAddOns())
+			err = engine->updateAddOns(&metaEngine);
+	}
+
 	// Check for errors
 	if (!engine || err.getCode() != Common::kNoError) {
 
diff --git a/engines/engine.cpp b/engines/engine.cpp
index c1178a174fe..fe69372146b 100644
--- a/engines/engine.cpp
+++ b/engines/engine.cpp
@@ -46,11 +46,13 @@
 #include "base/version.h"
 
 #include "gui/gui-manager.h"
+#include "gui/chooser.h"
 #include "gui/debugger.h"
 #include "gui/dialog.h"
 #include "gui/EventRecorder.h"
 #include "gui/message.h"
 #include "gui/saveload.h"
+#include "gui/unknown-game-dialog.h"
 
 #include "audio/mixer.h"
 
@@ -1134,4 +1136,158 @@ void PauseToken::operator=(PauseToken &&t2) {
 	_engine = t2._engine;
 	t2._engine = nullptr;
 }
+
+bool Engine::gameTypeHasAddOns() const {
+	return false;
+}
+
+bool Engine::dirCanBeGameAddOn(Common::FSDirectory) const {
+	return true;
+}
+
+bool Engine::dirMustBeGameAddOn(Common::FSDirectory) const {
+	return false;
+}
+
+Common::ErrorCode Engine::updateAddOns(const MetaEngine *metaEngine) const {
+	const Plugin *detectionPlugin = EngineMan.findDetectionPlugin(metaEngine->getName());
+	if (!detectionPlugin) {
+		warning("Engine plugin for %s not present. Add-ons detection is disabled", metaEngine->getName());
+		return Common::kNoError;
+	}
+
+	// List already registered add-ons for this game, and detect removed ones
+	Common::ConfigManager::DomainMap::iterator iter = ConfMan.beginGameDomains();
+	Common::HashMap<Common::Path, Common::String, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> existingAddOnsPaths;
+
+	bool anyAddOnRemoved = false;
+	for (; iter != ConfMan.endGameDomains(); ++iter) {
+		Common::String name(iter->_key);
+		Common::ConfigManager::Domain &dom = iter->_value;
+
+		Common::String parent;
+		if (dom.tryGetVal("parent", parent) && parent == ConfMan.getActiveDomainName()) {
+			// Existing add-on, check if its path still exists
+			Common::Path addOnPath(Common::Path::fromConfig(dom.getVal("path")));
+			if (addOnPath.empty() || !Common::FSNode(addOnPath).isDirectory()) {
+				// Path does not exist, remove the add-on
+				debug("Removing entry of deleted add-on '%s' (former path: '%s')",
+					  name.c_str(),
+					  addOnPath.toString(Common::Path::kNativeSeparator).c_str());
+
+				ConfMan.removeGameDomain(name);
+				anyAddOnRemoved = true;
+			} else {
+				existingAddOnsPaths[addOnPath] = name;
+			}
+		}
+	}
+
+	if (anyAddOnRemoved)
+		ConfMan.flushToDisk();
+
+	// Look for newly added add-ons
+	bool anyAddOnAdded = false;
+	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
+	Common::FSList subdirNodes;
+	gameDataDir.getChildren(subdirNodes, Common::FSNode::kListDirectoriesOnly);
+	for (const Common::FSNode &subdirNode : subdirNodes) {
+		Common::FSDirectory subdir(subdirNode);
+		if (dirCanBeGameAddOn(subdir)) {
+			Common::FSList files;
+			if (!subdirNode.getChildren(files, Common::FSNode::kListAll))
+				continue;
+
+			ADCacheMan.clear();
+
+			DetectedGames detectedGames = detectionPlugin->get<MetaEngineDetection>().detectGames(files);
+			DetectedGames detectedAddOns;
+			for (DetectedGame &game : detectedGames) {
+				if (game.isAddOn) {
+					detectedAddOns.push_back(game);
+				}
+			}
+
+			int idx = 0;
+			if (detectedAddOns.empty() && dirMustBeGameAddOn(subdir)) {
+				Common::U32String msgFormat(_("The directory '%s' looks like an add-on for the game '%s', but ScummVM could not find any matching add-on in it."));
+				Common::U32String msg = Common::U32String::format(msgFormat,
+																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str(),
+																  ConfMan.getActiveDomainName().c_str());
+
+				GUI::MessageDialog alert(msg);
+				alert.runModal();
+				continue;
+			} else if (detectedAddOns.size() == 1) {
+				// Exact match
+				idx = 0;
+			} else {
+				// Display the candidates to the user and let her/him pick one
+				Common::U32StringArray list;
+				for (idx = 0; idx < (int)detectedAddOns.size(); idx++) {
+					Common::U32String description = detectedAddOns[idx].description;
+
+					if (detectedAddOns[idx].hasUnknownFiles) {
+						description += Common::U32String(" - ");
+						// Unknown game variant
+						description += _("Unknown variant");
+					}
+
+					list.push_back(description);
+				}
+
+				Common::U32String msgFormat(_("Directory '%s' matches several add-ons, please pick one."));
+				Common::U32String msg = Common::U32String::format(msgFormat,
+																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+
+				GUI::ChooserDialog dialog(msg);
+				dialog.setList(list);
+				idx = dialog.runModal();
+				if (idx < 0)
+					return Common::kUserCanceled;
+			}
+
+			if (0 <= idx && idx < (int)detectedAddOns.size()) {
+				DetectedGame &selectedAddOn = detectedAddOns[idx];
+				selectedAddOn.path = subdirNode.getPath();
+				selectedAddOn.shortPath = subdirNode.getDisplayName();
+
+				if (selectedAddOn.hasUnknownFiles) {
+					debug("Detected an unknown variant of add-on '%s' (path: '%s')",
+						  selectedAddOn.gameId.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+					GUI::UnknownGameDialog dialog(selectedAddOn);
+					dialog.runModal();
+					continue; // Do not create an entry for unknown variants
+				}
+
+				if (selectedAddOn.gameSupportLevel != kStableGame) {
+					if (!warnUserAboutUnsupportedAddOn(selectedAddOn.description)) {
+						return Common::kUserCanceled;
+					}
+				}
+
+				Common::String addOnName;
+				if (existingAddOnsPaths.tryGetVal(subdirNode.getPath(), addOnName)) {
+					debug("Detected existing add-on '%s' (path: '%s')",
+						  addOnName.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+				} else {
+					Common::String domain = EngineMan.createTargetForGame(selectedAddOn);
+					debug("Detected new add-on '%s' (path: '%s')",
+						  domain.c_str(),
+						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
+					ConfMan.set("parent", ConfMan.getActiveDomainName(), domain);
+					anyAddOnAdded = true;
+				}
+			}
+		}
+	}
+
+	if (anyAddOnAdded)
+		ConfMan.flushToDisk();
+
+	return Common::kNoError;
+}
+
 #endif
diff --git a/engines/engine.h b/engines/engine.h
index c6e8651caf8..33af4c26e26 100644
--- a/engines/engine.h
+++ b/engines/engine.h
@@ -22,6 +22,7 @@
 #ifndef ENGINES_ENGINE_H
 #define ENGINES_ENGINE_H
 
+#include "common/error.h"
 #include "common/scummsys.h"
 #include "common/str.h"
 #include "common/language.h"
@@ -39,6 +40,7 @@ class Mixer;
 }
 namespace Common {
 class Error;
+class FSDirectory;
 class EventManager;
 class SaveFileManager;
 class TimerManager;
@@ -690,6 +692,27 @@ public:
 		return 0;
 	}
 
+	/**
+	 * Can the game type currently being played have add-ons?
+	 */
+	virtual	bool gameTypeHasAddOns() const;
+
+	/**
+	 * To discard some directories we know have no chance to be add-ons
+	 */
+	virtual bool dirCanBeGameAddOn(Common::FSDirectory dir) const;
+
+	/**
+	 * To display a warning if a directory likely to be an add-on does not match anything
+	 */
+	virtual bool dirMustBeGameAddOn(Common::FSDirectory dir) const;
+
+	/**
+	 * Update the add-ons targets associated with a base game (silently, unless some unsupported version is detected).
+	 */
+	Common::ErrorCode updateAddOns(const MetaEngine *metaEngine) const;
+
+
 protected:
 	/**
 	 * Syncs the engine's mixer using the default volume syncing behavior.
diff --git a/engines/gob/gob.h b/engines/gob/gob.h
index a35df455b98..36fbe992a15 100644
--- a/engines/gob/gob.h
+++ b/engines/gob/gob.h
@@ -260,12 +260,11 @@ public:
 	~GobEngine() override;
 
 	void initGame(const GOBGameDescription *gd);
-	Common::ErrorCode updateAddOns(const GobMetaEngine *metaEngine, const GOBGameDescription *gd) const;
 
 	GameType getGameType(const char *gameId) const;
-	bool gameTypeHasAddOns();
-	bool dirCanBeGameAddOn(Common::FSDirectory dir) const;
-	bool dirMustBeGameAddOn(Common::FSDirectory dir) const;
+	bool gameTypeHasAddOns() const override;
+	bool dirCanBeGameAddOn(Common::FSDirectory dir) const override;
+	bool dirMustBeGameAddOn(Common::FSDirectory dir) const override;
 
 	/**
 	 * Used to obtain the game version as a fallback
diff --git a/engines/gob/metaengine.cpp b/engines/gob/metaengine.cpp
index ec2e423d6b2..f6dafb8f954 100644
--- a/engines/gob/metaengine.cpp
+++ b/engines/gob/metaengine.cpp
@@ -32,10 +32,6 @@
 
 #include "common/translation.h"
 
-#include "gui/chooser.h"
-#include "gui/message.h"
-#include "gui/unknown-game-dialog.h"
-
 #include "gob/gameidtotype.h"
 #include "gob/gob.h"
 
@@ -82,16 +78,10 @@ bool Gob::GobEngine::hasFeature(EngineFeature f) const {
 }
 
 Common::Error GobMetaEngine::createInstance(OSystem *syst, Engine **engine, const Gob::GOBGameDescription *gd) const {
-
 	Gob::GobEngine *gobEngine = new Gob::GobEngine(syst);
 	*engine = gobEngine;
 	gobEngine->initGame(gd);
-	Common::ErrorCode errorCode = Common::kNoError;
-
-	if (gobEngine->gameTypeHasAddOns())
-		errorCode = gobEngine->updateAddOns(this, gd);
-
-	return errorCode;
+	return Common::kNoError;
 }
 
 
@@ -115,7 +105,7 @@ GameType GobEngine::getGameType(const char *gameId) const {
 	error("Unknown game ID: %s", gameId);
 }
 
-bool GobEngine::gameTypeHasAddOns() {
+bool GobEngine::gameTypeHasAddOns() const {
 	return  getGameType() == kGameTypeAdibou1 ||
 			getGameType() == kGameTypeAdibou2 ||
 			getGameType() == kGameTypeAdi2 ||
@@ -139,149 +129,6 @@ bool GobEngine::dirMustBeGameAddOn(Common::FSDirectory dir) const {
 	return false;
 }
 
-Common::ErrorCode GobEngine::updateAddOns(const GobMetaEngine *metaEngine, const GOBGameDescription *gd) const {
-	const Plugin *detectionPlugin = EngineMan.findDetectionPlugin(metaEngine->getName());
-	if (!detectionPlugin) {
-		warning("Engine plugin for GOB not present. Add-ons detection is disabled");
-		return Common::kNoError;
-	}
-
-	// Update silently the targets associated with the add-ons, unless some unsupported version is detected
-
-	// List already registered add-ons for this game, and detect removed ones
-	Common::ConfigManager::DomainMap::iterator iter = ConfMan.beginGameDomains();
-	Common::HashMap<Common::Path, Common::String, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> existingAddOnsPaths;
-
-	bool anyAddOnRemoved = false;
-	for (; iter != ConfMan.endGameDomains(); ++iter) {
-		Common::String name(iter->_key);
-		Common::ConfigManager::Domain &dom = iter->_value;
-
-		Common::String parent;
-		if (dom.tryGetVal("parent", parent) && parent == ConfMan.getActiveDomainName()) {
-			// Existing add-on, check if its path still exists
-			Common::Path addOnPath(Common::Path::fromConfig(dom.getVal("path")));
-			if (addOnPath.empty() || !Common::FSNode(addOnPath).isDirectory()) {
-				// Path does not exist, remove the add-on
-				debug("Removing entry of deleted add-on '%s' (former path: '%s')",
-					  name.c_str(),
-					  addOnPath.toString(Common::Path::kNativeSeparator).c_str());
-
-				ConfMan.removeGameDomain(name);
-				anyAddOnRemoved = true;
-			} else {
-				existingAddOnsPaths[addOnPath] = name;
-			}
-		}
-	}
-
-	if (anyAddOnRemoved)
-		ConfMan.flushToDisk();
-
-	// Look for newly added add-ons
-	bool anyAddOnAdded = false;
-	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
-	Common::FSList subdirNodes;
-	gameDataDir.getChildren(subdirNodes, Common::FSNode::kListDirectoriesOnly);
-	for (const Common::FSNode &subdirNode : subdirNodes) {
-		Common::FSDirectory subdir(subdirNode);
-		if (dirCanBeGameAddOn(subdir)) {
-			Common::FSList files;
-			if (!subdirNode.getChildren(files, Common::FSNode::kListAll))
-				continue;
-
-			ADCacheMan.clear();
-
-			DetectedGames detectedGames = detectionPlugin->get<MetaEngineDetection>().detectGames(files);
-			DetectedGames detectedAddOns;
-			for (DetectedGame &game : detectedGames) {
-				if (game.isAddOn) {
-					detectedAddOns.push_back(game);
-				}
-			}
-
-			int idx = 0;
-			if (detectedAddOns.empty() && dirMustBeGameAddOn(subdir)) {
-				Common::U32String msgFormat(_("The directory '%s' looks like an add-on for the game '%s', but ScummVM could not find any matching add-on in it."));
-				Common::U32String msg = Common::U32String::format(msgFormat,
-																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str(),
-																  gd->desc.gameId);
-
-				GUI::MessageDialog alert(msg);
-				alert.runModal();
-				continue;
-			} else if (detectedAddOns.size() == 1) {
-				// Exact match
-				idx = 0;
-			} else {
-				// Display the candidates to the user and let her/him pick one
-				Common::U32StringArray list;
-				for (idx = 0; idx < (int)detectedAddOns.size(); idx++) {
-					Common::U32String description = detectedAddOns[idx].description;
-
-					if (detectedAddOns[idx].hasUnknownFiles) {
-						description += Common::U32String(" - ");
-						// Unknown game variant
-						description += _("Unknown variant");
-					}
-
-					list.push_back(description);
-				}
-
-				Common::U32String msgFormat(_("Directory '%s' matches several add-ons, please pick one."));
-				Common::U32String msg = Common::U32String::format(msgFormat,
-																  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
-
-				GUI::ChooserDialog dialog(msg);
-				dialog.setList(list);
-				idx = dialog.runModal();
-				if (idx < 0)
-					return Common::kUserCanceled;
-			}
-
-			if (0 <= idx && idx < (int)detectedAddOns.size()) {
-				DetectedGame &selectedAddOn = detectedAddOns[idx];
-				selectedAddOn.path = subdirNode.getPath();
-				selectedAddOn.shortPath = subdirNode.getDisplayName();
-
-				if (selectedAddOn.hasUnknownFiles) {
-					debug("Detected an unknown variant of add-on '%s' (path: '%s')",
-						  selectedAddOn.gameId.c_str(),
-						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
-					GUI::UnknownGameDialog dialog(selectedAddOn);
-					dialog.runModal();
-					continue; // Do not create an entry for unknown variants
-				}
-
-				if (selectedAddOn.gameSupportLevel != kStableGame) {
-					if (!warnUserAboutUnsupportedAddOn(selectedAddOn.description)) {
-						return Common::kUserCanceled;
-					}
-				}
-
-				Common::String addOnName;
-				if (existingAddOnsPaths.tryGetVal(subdirNode.getPath(), addOnName)) {
-					debug("Detected existing add-on '%s' (path: '%s')",
-						  addOnName.c_str(),
-						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
-				} else {
-					Common::String domain = EngineMan.createTargetForGame(selectedAddOn);
-					debug("Detected new add-on '%s' (path: '%s')",
-						  domain.c_str(),
-						  subdirNode.getPath().toString(Common::Path::kNativeSeparator).c_str());
-					ConfMan.set("parent", ConfMan.getActiveDomainName(), domain);
-					anyAddOnAdded = true;
-				}
-			}
-		}
-	}
-
-	if (anyAddOnAdded)
-		ConfMan.flushToDisk();
-
-	return Common::kNoError;
-}
-
 void GobEngine::initGame(const GOBGameDescription *gd) {
 	if (gd->startTotBase == nullptr)
 		_startTot = "intro.tot";




More information about the Scummvm-git-logs mailing list