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