1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "common/debug.h"
24 #include "common/util.h"
25 #include "common/file.h"
26 #include "common/macresman.h"
27 #include "common/md5.h"
28 #include "common/config-manager.h"
29 #include "common/punycode.h"
30 #include "common/system.h"
31 #include "common/textconsole.h"
32 #include "common/translation.h"
33 #include "gui/EventRecorder.h"
34 #include "gui/gui-manager.h"
35 #include "gui/message.h"
36 #include "engines/advancedDetector.h"
37 #include "engines/obsolete.h"
38 
39 /**
40  * Adapter to be able to use Common::Archive based code from the AD.
41  */
42 class FileMapArchive : public Common::Archive {
43 public:
FileMapArchive(const AdvancedMetaEngineDetection::FileMap & fileMap)44 	FileMapArchive(const AdvancedMetaEngineDetection::FileMap &fileMap) : _fileMap(fileMap) {}
45 
hasFile(const Common::Path & path) const46 	bool hasFile(const Common::Path &path) const override {
47 		Common::String name = path.toString();
48 		return _fileMap.contains(name);
49 	}
50 
listMembers(Common::ArchiveMemberList & list) const51 	int listMembers(Common::ArchiveMemberList &list) const override {
52 		int files = 0;
53 		for (AdvancedMetaEngineDetection::FileMap::const_iterator it = _fileMap.begin(); it != _fileMap.end(); ++it) {
54 			list.push_back(Common::ArchiveMemberPtr(new Common::FSNode(it->_value)));
55 			++files;
56 		}
57 
58 		return files;
59 	}
60 
getMember(const Common::Path & path) const61 	const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override {
62 		Common::String name = path.toString();
63 		AdvancedMetaEngineDetection::FileMap::const_iterator it = _fileMap.find(name);
64 		if (it == _fileMap.end()) {
65 			return Common::ArchiveMemberPtr();
66 		}
67 
68 		return Common::ArchiveMemberPtr(new Common::FSNode(it->_value));
69 	}
70 
createReadStreamForMember(const Common::Path & path) const71 	Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override {
72 		Common::String name = path.toString();
73 		Common::FSNode fsNode = _fileMap.getValOrDefault(name);
74 		return fsNode.createReadStream();
75 	}
76 
77 private:
78 	const AdvancedMetaEngineDetection::FileMap &_fileMap;
79 };
80 
sanitizeName(const char * name,int maxLen)81 static Common::String sanitizeName(const char *name, int maxLen) {
82 	Common::String res;
83 	Common::String word;
84 	Common::String lastWord;
85 	const char *origname = name;
86 
87 	do {
88 		if (Common::isAlnum(*name)) {
89 			word += tolower(*name);
90 		} else {
91 			// Skipping short words and "the"
92 			if ((word.size() > 2 && !word.equals("the")) || (!word.empty() && Common::isDigit(word[0]))) {
93 				// Adding first word, or when word fits
94 				if (res.empty() || (int)word.size() < maxLen)
95 					res += word;
96 
97 				maxLen -= word.size();
98 			}
99 
100 			if ((*name && *(name + 1) == 0) || !*name) {
101 				if (res.empty()) // Make sure that we add at least something
102 					res += word.empty() ? lastWord : word;
103 
104 				break;
105 			}
106 
107 			if (!word.empty())
108 				lastWord = word;
109 
110 			word.clear();
111 		}
112 		if (*name)
113 			name++;
114 	} while (maxLen > 0);
115 
116 	if (res.empty())
117 		error("AdvancedDetector: Incorrect extra in game: \"%s\"", origname);
118 
119 	return res;
120 }
121 
122 /**
123  * Generate a preferred target value as
124  *   GAMEID-PLAFORM-LANG
125  * or (if ADGF_DEMO has been set)
126  *   GAMEID-demo-PLAFORM-LANG
127  */
generatePreferredTarget(const ADGameDescription * desc,int maxLen,Common::String targetID)128 static Common::String generatePreferredTarget(const ADGameDescription *desc, int maxLen, Common::String targetID) {
129 	Common::String res;
130 
131 	if (!targetID.empty()) {
132 		res = targetID;
133 	} else if (desc->flags & ADGF_AUTOGENTARGET && desc->extra && *desc->extra) {
134 		res = sanitizeName(desc->extra, maxLen);
135 	} else {
136 		res = desc->gameId;
137 	}
138 
139 	if (desc->flags & ADGF_DEMO) {
140 		res = res + "-demo";
141 	}
142 
143 	if (desc->flags & ADGF_CD) {
144 		res = res + "-cd";
145 	}
146 
147 	if (desc->flags & ADGF_REMASTERED) {
148 		res = res + "-remastered";
149 	}
150 
151 	if (desc->platform != Common::kPlatformDOS && desc->platform != Common::kPlatformUnknown && !(desc->flags & ADGF_DROPPLATFORM)) {
152 		res = res + "-" + getPlatformAbbrev(desc->platform);
153 	}
154 
155 	if (desc->language != Common::EN_ANY && desc->language != Common::UNK_LANG && !(desc->flags & ADGF_DROPLANGUAGE)) {
156 		res = res + "-" + getLanguageCode(desc->language);
157 	}
158 
159 	return res;
160 }
161 
toDetectedGame(const ADDetectedGame & adGame,ADDetectedGameExtraInfo * extraInfo) const162 DetectedGame AdvancedMetaEngineDetection::toDetectedGame(const ADDetectedGame &adGame, ADDetectedGameExtraInfo *extraInfo) const {
163 	const ADGameDescription *desc = adGame.desc;
164 
165 	const char *title;
166 	const char *extra;
167 	if (desc->flags & ADGF_USEEXTRAASTITLE) {
168 		title = desc->extra;
169 		extra = "";
170 	} else {
171 		const PlainGameDescriptor *pgd = findPlainGameDescriptor(desc->gameId, _gameIds);
172 		if (pgd) {
173 			title = pgd->description;
174 		} else {
175 			title = "";
176 		}
177 		extra = desc->extra;
178 	}
179 
180 	if (extraInfo) {
181 		if (!extraInfo->gameName.empty())
182 			title = extraInfo->gameName.c_str();
183 	}
184 
185 	DetectedGame game(getEngineId(), desc->gameId, title, desc->language, desc->platform, extra, ((desc->flags & (ADGF_UNSUPPORTED | ADGF_WARNING)) != 0));
186 	game.hasUnknownFiles = adGame.hasUnknownFiles;
187 	game.matchedFiles = adGame.matchedFiles;
188 
189 	if (extraInfo && !extraInfo->targetID.empty()) {
190 		game.preferredTarget = generatePreferredTarget(desc, _maxAutogenLength, extraInfo->targetID);
191 	} else {
192 		game.preferredTarget = generatePreferredTarget(desc, _maxAutogenLength, Common::String());
193 	}
194 
195 	game.gameSupportLevel = kStableGame;
196 	if (desc->flags & ADGF_UNSTABLE)
197 		game.gameSupportLevel = kUnstableGame;
198 	else if (desc->flags & ADGF_TESTING)
199 		game.gameSupportLevel = kTestingGame;
200 	else if (desc->flags & ADGF_UNSUPPORTED)
201 		game.gameSupportLevel = kUnsupportedGame;
202 	else if (desc->flags & ADGF_WARNING)
203 		game.gameSupportLevel = kWarningGame;
204 
205 	game.setGUIOptions(desc->guiOptions + _guiOptions);
206 	game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(desc->language));
207 
208 	if (desc->flags & ADGF_ADDENGLISH)
209 		game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(Common::EN_ANY));
210 
211 	if (_flags & kADFlagUseExtraAsHint)
212 		game.extra = desc->extra;
213 
214 	return game;
215 }
216 
cleanupPirated(ADDetectedGames & matched) const217 bool AdvancedMetaEngineDetection::cleanupPirated(ADDetectedGames &matched) const {
218 	// OKay, now let's sense presence of pirated games
219 	if (!matched.empty()) {
220 		for (uint j = 0; j < matched.size();) {
221 			if (matched[j].desc->flags & ADGF_PIRATED)
222 				matched.remove_at(j);
223 			else
224 				++j;
225 		}
226 
227 		// We ruled out all variants and now have nothing
228 		if (matched.empty()) {
229 			warning("Illegitimate game copy detected. We provide no support in such cases");
230 			if (GUI::GuiManager::hasInstance()) {
231 				GUI::MessageDialog dialog(_("Illegitimate game copy detected. We provide no support in such cases"));
232 				dialog.runModal();
233 			};
234 			return true;
235 		}
236 	}
237 
238 	return false;
239 }
240 
241 
detectGames(const Common::FSList & fslist) const242 DetectedGames AdvancedMetaEngineDetection::detectGames(const Common::FSList &fslist) const {
243 	FileMap allFiles;
244 
245 	if (fslist.empty())
246 		return DetectedGames();
247 
248 	// Compose a hashmap of all files in fslist.
249 	composeFileHashMap(allFiles, fslist, (_maxScanDepth == 0 ? 1 : _maxScanDepth));
250 
251 	// Run the detector on this
252 	ADDetectedGames matches = detectGame(fslist.begin()->getParent(), allFiles, Common::UNK_LANG, Common::kPlatformUnknown, "");
253 
254 	cleanupPirated(matches);
255 
256 	DetectedGames detectedGames;
257 	for (uint i = 0; i < matches.size(); i++) {
258 		DetectedGame game = toDetectedGame(matches[i]);
259 
260 		if (game.hasUnknownFiles && !canPlayUnknownVariants()) {
261 			game.canBeAdded = false;
262 		}
263 
264 		detectedGames.push_back(game);
265 	}
266 
267 	bool foundKnownGames = false;
268 	for (uint i = 0; i < detectedGames.size(); i++) {
269 		foundKnownGames |= !detectedGames[i].hasUnknownFiles;
270 	}
271 
272 	if (!foundKnownGames) {
273 		// Use fallback detector if there were no matches by other means
274 		ADDetectedGameExtraInfo *extraInfo = nullptr;
275 		ADDetectedGame fallbackDetectionResult = fallbackDetect(allFiles, fslist, &extraInfo);
276 
277 		if (fallbackDetectionResult.desc) {
278 			DetectedGame fallbackDetectedGame = toDetectedGame(fallbackDetectionResult, extraInfo);
279 
280 			if (extraInfo != nullptr) {
281 				// then it's our duty to free it
282 				delete extraInfo;
283 			} else {
284 				// don't add fallback when we are specifying the targetID
285 				fallbackDetectedGame.preferredTarget += "-fallback";
286 			}
287 
288 			detectedGames.push_back(fallbackDetectedGame);
289 		}
290 	}
291 
292 	return detectedGames;
293 }
294 
getExtraGuiOptions(const Common::String & target) const295 const ExtraGuiOptions AdvancedMetaEngineDetection::getExtraGuiOptions(const Common::String &target) const {
296 	if (!_extraGuiOptions)
297 		return ExtraGuiOptions();
298 
299 	ExtraGuiOptions options;
300 
301 	// If there isn't any target specified, return all available GUI options.
302 	// Only used when an engine starts in order to set option defaults.
303 	if (target.empty()) {
304 		for (const ADExtraGuiOptionsMap *entry = _extraGuiOptions; entry->guioFlag; ++entry)
305 			options.push_back(entry->option);
306 
307 		return options;
308 	}
309 
310 	// Query the GUI options
311 	const Common::String guiOptionsString = ConfMan.get("guioptions", target);
312 	const Common::String guiOptions = parseGameGUIOptions(guiOptionsString);
313 
314 	// Add all the applying extra GUI options.
315 	for (const ADExtraGuiOptionsMap *entry = _extraGuiOptions; entry->guioFlag; ++entry) {
316 		if (guiOptions.contains(entry->guioFlag))
317 			options.push_back(entry->option);
318 	}
319 
320 	return options;
321 }
322 
createInstance(OSystem * syst,Engine ** engine) const323 Common::Error AdvancedMetaEngineDetection::createInstance(OSystem *syst, Engine **engine) const {
324 	assert(engine);
325 
326 	Common::Language language = Common::UNK_LANG;
327 	Common::Platform platform = Common::kPlatformUnknown;
328 	Common::String extra;
329 
330 	if (ConfMan.hasKey("language"))
331 		language = Common::parseLanguage(ConfMan.get("language"));
332 	if (ConfMan.hasKey("platform"))
333 		platform = Common::parsePlatform(ConfMan.get("platform"));
334 	if (_flags & kADFlagUseExtraAsHint) {
335 		if (ConfMan.hasKey("extra"))
336 			extra = ConfMan.get("extra");
337 	}
338 
339 	Common::String gameid = ConfMan.get("gameid");
340 
341 	Common::String path;
342 	if (ConfMan.hasKey("path")) {
343 		path = ConfMan.get("path");
344 	} else {
345 		path = ".";
346 		warning("No path was provided. Assuming the data files are in the current directory");
347 	}
348 	Common::FSNode dir(path);
349 	Common::FSList files;
350 	if (!dir.isDirectory() || !dir.getChildren(files, Common::FSNode::kListAll)) {
351 		warning("Game data path does not exist or is not a directory (%s)", path.c_str());
352 		return Common::kNoGameDataFoundError;
353 	}
354 
355 	if (files.empty())
356 		return Common::kNoGameDataFoundError;
357 
358 	// Compose a hashmap of all files in fslist.
359 	FileMap allFiles;
360 	composeFileHashMap(allFiles, files, (_maxScanDepth == 0 ? 1 : _maxScanDepth));
361 
362 	// Clear md5 cache before each detection starts, just in case.
363 	MD5Man.clear();
364 
365 	// Run the detector on this
366 	ADDetectedGames matches = detectGame(files.begin()->getParent(), allFiles, language, platform, extra);
367 
368 	if (cleanupPirated(matches))
369 		return Common::kNoGameDataFoundError;
370 
371 	ADDetectedGame agdDesc;
372 	for (uint i = 0; i < matches.size(); i++) {
373 		if (matches[i].desc->gameId == gameid && (!matches[i].hasUnknownFiles || canPlayUnknownVariants())) {
374 			agdDesc = matches[i];
375 			break;
376 		}
377 	}
378 
379 	if (!agdDesc.desc) {
380 		// Use fallback detector if there were no matches by other means
381 		ADDetectedGame fallbackDetectedGame = fallbackDetect(allFiles, files);
382 		agdDesc = fallbackDetectedGame;
383 		if (agdDesc.desc) {
384 			// Seems we found a fallback match. But first perform a basic
385 			// sanity check: the gameid must match.
386 			if (agdDesc.desc->gameId != gameid)
387 				agdDesc = ADDetectedGame();
388 		}
389 	}
390 
391 	if (!agdDesc.desc)
392 		return Common::kNoGameDataFoundError;
393 
394 	DetectedGame gameDescriptor = toDetectedGame(agdDesc);
395 
396 	// If the GUI options were updated, we catch this here and update them in the users config
397 	// file transparently.
398 	ConfMan.setAndFlush("guioptions", gameDescriptor.getGUIOptions());
399 
400 	bool showTestingWarning = false;
401 
402 #ifdef RELEASE_BUILD
403 	showTestingWarning = true;
404 #endif
405 
406 	if (((gameDescriptor.gameSupportLevel == kUnstableGame
407 			|| (gameDescriptor.gameSupportLevel == kTestingGame
408 					&& showTestingWarning)))
409 			&& !Engine::warnUserAboutUnsupportedGame())
410 		return Common::kUserCanceled;
411 
412 	if (gameDescriptor.gameSupportLevel == kWarningGame
413 			&& !Engine::warnUserAboutUnsupportedGame(gameDescriptor.extra))
414 		return Common::kUserCanceled;
415 
416 	if (gameDescriptor.gameSupportLevel == kUnsupportedGame) {
417 		Engine::errorUnsupportedGame(gameDescriptor.extra);
418 		return Common::kUserCanceled;
419 	}
420 
421 	debugC(2, kDebugGlobalDetection, "Running %s", gameDescriptor.description.c_str());
422 	initSubSystems(agdDesc.desc);
423 
424 	PluginList pl = EngineMan.getPlugins(PLUGIN_TYPE_ENGINE);
425 	Plugin *plugin = nullptr;
426 
427 	// By this point of time, we should have only one plugin in memory.
428 	if (pl.size() == 1) {
429 		plugin = pl[0];
430 	}
431 
432 	if (plugin) {
433 		// Call child class's createInstanceMethod.
434 		return plugin->get<AdvancedMetaEngine>().createInstance(syst, engine, agdDesc.desc);
435 	}
436 
437 	return Common::Error(Common::kEnginePluginNotFound);
438 }
439 
composeFileHashMap(FileMap & allFiles,const Common::FSList & fslist,int depth,const Common::String & parentName) const440 void AdvancedMetaEngineDetection::composeFileHashMap(FileMap &allFiles, const Common::FSList &fslist, int depth, const Common::String &parentName) const {
441 	if (depth <= 0)
442 		return;
443 
444 	if (fslist.empty())
445 		return;
446 
447 	for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
448 		Common::String tstr = (_matchFullPaths && !parentName.empty() ? parentName + "/" : "") + file->getName();
449 
450 		if (file->isDirectory()) {
451 			Common::FSList files;
452 
453 			if (!_directoryGlobs)
454 				continue;
455 
456 			bool matched = false;
457 			for (const char * const *glob = _directoryGlobs; *glob; glob++)
458 				if (file->getName().matchString(*glob, true)) {
459 					matched = true;
460 					break;
461 				}
462 
463 			if (!matched)
464 				continue;
465 
466 			if (!file->getChildren(files, Common::FSNode::kListAll))
467 				continue;
468 
469 			composeFileHashMap(allFiles, files, depth - 1, tstr);
470 		}
471 
472 		// Strip any trailing dot
473 		if (tstr.lastChar() == '.')
474 			tstr.deleteLastChar();
475 
476 		allFiles[tstr] = *file;	// Record the presence of this file
477 	}
478 }
479 
480 /* Singleton Cache Storage for MD5 */
481 
482 namespace Common {
483 	DECLARE_SINGLETON(MD5CacheManager);
484 }
485 
getFileProperties(const FileMap & allFiles,const ADGameDescription & game,const Common::String fname,FileProperties & fileProps) const486 bool AdvancedMetaEngineDetection::getFileProperties(const FileMap &allFiles, const ADGameDescription &game, const Common::String fname, FileProperties &fileProps) const {
487 	// FIXME/TODO: We don't handle the case that a file is listed as a regular
488 	// file and as one with resource fork.
489 	Common::String hashname = Common::String::format("%s:%d", fname.c_str(), _md5Bytes);
490 
491 	if (MD5Man.contains(hashname)) {
492 		fileProps.md5 = MD5Man.getMD5(hashname);
493 		fileProps.size = MD5Man.getSize(hashname);
494 		return true;
495 	}
496 
497 	if (game.flags & ADGF_MACRESFORK) {
498 		FileMapArchive fileMapArchive(allFiles);
499 
500 		Common::MacResManager macResMan;
501 
502 		if (!macResMan.open(fname, fileMapArchive))
503 			return false;
504 
505 		fileProps.md5 = macResMan.computeResForkMD5AsString(_md5Bytes);
506 		fileProps.size = macResMan.getResForkDataSize();
507 
508 		if (fileProps.size != 0) {
509 			MD5Man.setMD5(hashname, fileProps.md5);
510 			MD5Man.setSize(hashname, fileProps.size);
511 			return true;
512 		}
513 	}
514 
515 	if (!allFiles.contains(fname))
516 		return false;
517 
518 	Common::File testFile;
519 
520 	if (!testFile.open(allFiles[fname]))
521 		return false;
522 
523 	fileProps.md5 = Common::computeStreamMD5AsString(testFile, _md5Bytes);
524 	fileProps.size = testFile.size();
525 	MD5Man.setMD5(hashname, fileProps.md5);
526 	MD5Man.setSize(hashname, fileProps.size);
527 
528 	return true;
529 }
530 
getFilePropertiesExtern(uint md5Bytes,const FileMap & allFiles,const ADGameDescription & game,const Common::String fname,FileProperties & fileProps) const531 bool AdvancedMetaEngine::getFilePropertiesExtern(uint md5Bytes, const FileMap &allFiles, const ADGameDescription &game, const Common::String fname, FileProperties &fileProps) const {
532 	// FIXME/TODO: We don't handle the case that a file is listed as a regular
533 	// file and as one with resource fork.
534 
535 	if (game.flags & ADGF_MACRESFORK) {
536 		FileMapArchive fileMapArchive(allFiles);
537 
538 		Common::MacResManager macResMan;
539 
540 		if (!macResMan.open(fname, fileMapArchive))
541 			return false;
542 
543 		fileProps.md5 = macResMan.computeResForkMD5AsString(md5Bytes);
544 		fileProps.size = macResMan.getResForkDataSize();
545 
546 		if (fileProps.size != 0)
547 			return true;
548 	}
549 
550 	if (!allFiles.contains(fname))
551 		return false;
552 
553 	Common::File testFile;
554 
555 	if (!testFile.open(allFiles[fname]))
556 		return false;
557 
558 	fileProps.size = testFile.size();
559 	fileProps.md5 = Common::computeStreamMD5AsString(testFile, md5Bytes);
560 	return true;
561 }
562 
detectGame(const Common::FSNode & parent,const FileMap & allFiles,Common::Language language,Common::Platform platform,const Common::String & extra) const563 ADDetectedGames AdvancedMetaEngineDetection::detectGame(const Common::FSNode &parent, const FileMap &allFiles, Common::Language language, Common::Platform platform, const Common::String &extra) const {
564 	FilePropertiesMap filesProps;
565 	ADDetectedGames matched;
566 
567 	const ADGameFileDescription *fileDesc;
568 	const ADGameDescription *g;
569 	const byte *descPtr;
570 
571 	debugC(3, kDebugGlobalDetection, "Starting detection in dir '%s'", parent.getPath().c_str());
572 
573 	// Check which files are included in some ADGameDescription *and* whether
574 	// they are present. Compute MD5s and file sizes for the available files.
575 	for (descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) {
576 		g = (const ADGameDescription *)descPtr;
577 
578 		for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) {
579 			Common::String fname = Common::punycode_decodefilename(fileDesc->fileName);
580 
581 			if (filesProps.contains(fname))
582 				continue;
583 
584 			FileProperties tmp;
585 			if (getFileProperties(allFiles, *g, fname, tmp)) {
586 				debugC(3, kDebugGlobalDetection, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str());
587 			}
588 
589 			// Both positive and negative results are cached to avoid
590 			// repeatedly checking for files.
591 			filesProps[fname] = tmp;
592 		}
593 	}
594 
595 	int maxFilesMatched = 0;
596 	bool gotAnyMatchesWithAllFiles = false;
597 
598 	// MD5 based matching
599 	uint i;
600 	for (i = 0, descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize, ++i) {
601 		g = (const ADGameDescription *)descPtr;
602 
603 		// Do not even bother to look at entries which do not have matching
604 		// language and platform (if specified).
605 		if ((language != Common::UNK_LANG && g->language != Common::UNK_LANG && g->language != language
606 			 && !(language == Common::EN_ANY && (g->flags & ADGF_ADDENGLISH))) ||
607 			(platform != Common::kPlatformUnknown && g->platform != Common::kPlatformUnknown && g->platform != platform)) {
608 			continue;
609 		}
610 
611 		if ((_flags & kADFlagUseExtraAsHint) && !extra.empty() && g->extra != extra)
612 			continue;
613 
614 		ADDetectedGame game(g);
615 		bool allFilesPresent = true;
616 		int curFilesMatched = 0;
617 
618 		// Try to match all files for this game
619 		for (fileDesc = game.desc->filesDescriptions; fileDesc->fileName; fileDesc++) {
620 			Common::String tstr = Common::punycode_decodefilename(fileDesc->fileName);
621 
622 			if (!filesProps.contains(tstr) || filesProps[tstr].size == -1) {
623 				allFilesPresent = false;
624 				break;
625 			}
626 
627 			game.matchedFiles[tstr] = filesProps[tstr];
628 
629 			if (game.hasUnknownFiles)
630 				continue;
631 
632 			if (fileDesc->md5 != nullptr && fileDesc->md5 != filesProps[tstr].md5) {
633 				debugC(3, kDebugGlobalDetection, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesProps[tstr].md5.c_str());
634 				game.hasUnknownFiles = true;
635 				continue;
636 			}
637 
638 			if (fileDesc->fileSize != -1 && fileDesc->fileSize != filesProps[tstr].size) {
639 				debugC(3, kDebugGlobalDetection, "Size Mismatch. Skipping");
640 				game.hasUnknownFiles = true;
641 				continue;
642 			}
643 
644 			debugC(3, kDebugGlobalDetection, "Matched file: %s", tstr.c_str());
645 			curFilesMatched++;
646 		}
647 
648 		// We found at least one entry with all required files present.
649 		// That means that we got new variant of the game.
650 		//
651 		// Without this check we would have erroneous checksum display
652 		// where only located files will be enlisted.
653 		//
654 		// Potentially this could rule out variants where some particular file
655 		// is really missing, but the developers should better know about such
656 		// cases.
657 		if (allFilesPresent && !gotAnyMatchesWithAllFiles) {
658 			if (matched.empty() || strcmp(matched.back().desc->gameId, g->gameId) != 0)
659 				matched.push_back(game);
660 		}
661 
662 		if (allFilesPresent && !game.hasUnknownFiles) {
663 			debugC(2, kDebugGlobalDetection, "Found game: %s (%s %s/%s) (%d)", g->gameId, g->extra,
664 			 getPlatformDescription(g->platform), getLanguageDescription(g->language), i);
665 
666 			if (curFilesMatched > maxFilesMatched) {
667 				debugC(2, kDebugGlobalDetection, " ... new best match, removing all previous candidates");
668 				maxFilesMatched = curFilesMatched;
669 
670 				matched.clear();	// Remove any prior, lower ranked matches.
671 				matched.push_back(game);
672 			} else if (curFilesMatched == maxFilesMatched) {
673 				matched.push_back(game);
674 			} else {
675 				debugC(2, kDebugGlobalDetection, " ... skipped");
676 			}
677 
678 			gotAnyMatchesWithAllFiles = true;
679 		} else {
680 			debugC(5, kDebugGlobalDetection, "Skipping game: %s (%s %s/%s) (%d)", g->gameId, g->extra,
681 			 getPlatformDescription(g->platform), getLanguageDescription(g->language), i);
682 		}
683 	}
684 
685 	return matched;
686 }
687 
detectGameFilebased(const FileMap & allFiles,const ADFileBasedFallback * fileBasedFallback) const688 ADDetectedGame AdvancedMetaEngineDetection::detectGameFilebased(const FileMap &allFiles, const ADFileBasedFallback *fileBasedFallback) const {
689 	const ADFileBasedFallback *ptr;
690 	const char* const* filenames;
691 
692 	int maxNumMatchedFiles = 0;
693 	ADDetectedGame result;
694 
695 	for (ptr = fileBasedFallback; ptr->desc; ++ptr) {
696 		const ADGameDescription *agdesc = ptr->desc;
697 		int numMatchedFiles = 0;
698 		bool fileMissing = false;
699 
700 		for (filenames = ptr->filenames; *filenames; ++filenames) {
701 			debugC(3, kDebugGlobalDetection, "++ %s", *filenames);
702 			if (!allFiles.contains(*filenames)) {
703 				fileMissing = true;
704 				break;
705 			}
706 
707 			numMatchedFiles++;
708 		}
709 
710 		if (!fileMissing) {
711 			debugC(4, kDebugGlobalDetection, "Matched: %s", agdesc->gameId);
712 
713 			if (numMatchedFiles > maxNumMatchedFiles) {
714 				maxNumMatchedFiles = numMatchedFiles;
715 
716 				debugC(4, kDebugGlobalDetection, "and overridden");
717 
718 				ADDetectedGame game(agdesc);
719 				game.hasUnknownFiles = true;
720 
721 				for (filenames = ptr->filenames; *filenames; ++filenames) {
722 					FileProperties tmp;
723 
724 					if (getFileProperties(allFiles, *agdesc, *filenames, tmp))
725 						game.matchedFiles[*filenames] = tmp;
726 				}
727 
728 				result = game;
729 			}
730 		}
731 	}
732 
733 	return result;
734 }
735 
getSupportedGames() const736 PlainGameList AdvancedMetaEngineDetection::getSupportedGames() const {
737 	return PlainGameList(_gameIds);
738 }
739 
findGame(const char * gameId) const740 PlainGameDescriptor AdvancedMetaEngineDetection::findGame(const char *gameId) const {
741 	// First search the list of supported gameids for a match.
742 	const PlainGameDescriptor *g = findPlainGameDescriptor(gameId, _gameIds);
743 	if (g)
744 		return *g;
745 
746 	// No match found
747 	return PlainGameDescriptor::empty();
748 }
749 
AdvancedMetaEngineDetection(const void * descs,uint descItemSize,const PlainGameDescriptor * gameIds,const ADExtraGuiOptionsMap * extraGuiOptions)750 AdvancedMetaEngineDetection::AdvancedMetaEngineDetection(const void *descs, uint descItemSize, const PlainGameDescriptor *gameIds, const ADExtraGuiOptionsMap *extraGuiOptions)
751 	: _gameDescriptors((const byte *)descs), _descItemSize(descItemSize), _gameIds(gameIds),
752 	  _extraGuiOptions(extraGuiOptions) {
753 
754 	_md5Bytes = 5000;
755 	_flags = 0;
756 	_guiOptions = GUIO_NONE;
757 	_maxScanDepth = 1;
758 	_directoryGlobs = NULL;
759 	_matchFullPaths = false;
760 	_maxAutogenLength = 15;
761 }
762 
initSubSystems(const ADGameDescription * gameDesc) const763 void AdvancedMetaEngineDetection::initSubSystems(const ADGameDescription *gameDesc) const {
764 #ifdef ENABLE_EVENTRECORDER
765 	if (gameDesc) {
766 		g_eventRec.processGameDescription(gameDesc);
767 	}
768 #endif
769 }
770 
createInstance(OSystem * syst,Engine ** engine) const771 Common::Error AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) const {
772 	PluginList pl = PluginMan.getPlugins(PLUGIN_TYPE_ENGINE);
773 	if (pl.size() == 1) {
774 		const Plugin *metaEnginePlugin = PluginMan.getMetaEngineFromEngine(pl[0]);
775 		if (metaEnginePlugin) {
776 			return metaEnginePlugin->get<AdvancedMetaEngineDetection>().createInstance(syst, engine);
777 		}
778 	}
779 
780 	return Common::Error();
781 }
782