ResidualVM logo ResidualVM website - Forums - Contact us BuildBot - Doxygen - Wiki curved edge

advancedDetector.cpp

Go to the documentation of this file.
00001 /* ScummVM - Graphic Adventure Engine
00002  *
00003  * ScummVM is the legal property of its developers, whose names
00004  * are too numerous to list here. Please refer to the COPYRIGHT
00005  * file distributed with this source distribution.
00006  *
00007  * This program is free software; you can redistribute it and/or
00008  * modify it under the terms of the GNU General Public License
00009  * as published by the Free Software Foundation; either version 2
00010  * of the License, or (at your option) any later version.
00011  *
00012  * This program is distributed in the hope that it will be useful,
00013  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00014  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00015  * GNU General Public License for more details.
00016  *
00017  * You should have received a copy of the GNU General Public License
00018  * along with this program; if not, write to the Free Software
00019  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
00020  *
00021  */
00022 
00023 #include "common/debug.h"
00024 #include "common/util.h"
00025 #include "common/file.h"
00026 #include "common/macresman.h"
00027 #include "common/md5.h"
00028 #include "common/config-manager.h"
00029 #include "common/system.h"
00030 #include "common/textconsole.h"
00031 #include "common/translation.h"
00032 #include "gui/EventRecorder.h"
00033 #include "engines/advancedDetector.h"
00034 #include "engines/obsolete.h"
00035 
00036 static Common::String sanitizeName(const char *name) {
00037     Common::String res;
00038 
00039     while (*name) {
00040         if (Common::isAlnum(*name))
00041             res += tolower(*name);
00042         name++;
00043     }
00044 
00045     return res;
00046 }
00047 
00054 static Common::String generatePreferredTarget(const ADGameDescription *desc) {
00055     Common::String res;
00056 
00057     if (desc->flags & ADGF_AUTOGENTARGET && desc->extra && *desc->extra) {
00058         res = sanitizeName(desc->extra);
00059     } else {
00060         res = desc->gameId;
00061     }
00062 
00063     if (desc->flags & ADGF_DEMO) {
00064         res = res + "-demo";
00065     }
00066 
00067     if (desc->flags & ADGF_CD) {
00068         res = res + "-cd";
00069     }
00070 
00071     if (desc->platform != Common::kPlatformDOS && desc->platform != Common::kPlatformUnknown && !(desc->flags & ADGF_DROPPLATFORM)) {
00072         res = res + "-" + getPlatformAbbrev(desc->platform);
00073     }
00074 
00075     if (desc->language != Common::EN_ANY && desc->language != Common::UNK_LANG && !(desc->flags & ADGF_DROPLANGUAGE)) {
00076         res = res + "-" + getLanguageCode(desc->language);
00077     }
00078 
00079     return res;
00080 }
00081 
00082 DetectedGame AdvancedMetaEngine::toDetectedGame(const ADDetectedGame &adGame) const {
00083     const ADGameDescription *desc = adGame.desc;
00084 
00085     const char *gameId = _singleId ? _singleId : desc->gameId;
00086 
00087     const char *title;
00088     const char *extra;
00089     if (desc->flags & ADGF_USEEXTRAASTITLE) {
00090         title = desc->extra;
00091         extra = "";
00092     } else {
00093         const PlainGameDescriptor *pgd = findPlainGameDescriptor(desc->gameId, _gameIds);
00094         if (pgd) {
00095             title = pgd->description;
00096         } else {
00097             title = "";
00098         }
00099         extra = desc->extra;
00100     }
00101 
00102     DetectedGame game(gameId, title, desc->language, desc->platform, extra);
00103     game.hasUnknownFiles = adGame.hasUnknownFiles;
00104     game.matchedFiles = adGame.matchedFiles;
00105     game.preferredTarget = generatePreferredTarget(desc);
00106 
00107     game.gameSupportLevel = kStableGame;
00108     if (desc->flags & ADGF_UNSTABLE)
00109         game.gameSupportLevel = kUnstableGame;
00110     else if (desc->flags & ADGF_TESTING)
00111         game.gameSupportLevel = kTestingGame;
00112 
00113     game.setGUIOptions(desc->guiOptions + _guiOptions);
00114     game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(desc->language));
00115 
00116     if (desc->flags & ADGF_ADDENGLISH)
00117         game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(Common::EN_ANY));
00118 
00119     if (_flags & kADFlagUseExtraAsHint)
00120         game.extra = desc->extra;
00121 
00122     return game;
00123 }
00124 
00125 bool cleanupPirated(ADDetectedGames &matched) {
00126     // OKay, now let's sense presence of pirated games
00127     if (!matched.empty()) {
00128         for (uint j = 0; j < matched.size();) {
00129             if (matched[j].desc->flags & ADGF_PIRATED)
00130                 matched.remove_at(j);
00131             else
00132                 ++j;
00133         }
00134 
00135         // We ruled out all variants and now have nothing
00136         if (matched.empty()) {
00137             warning("Illegitimate game copy detected. We provide no support in such cases");
00138             return true;
00139         }
00140     }
00141 
00142     return false;
00143 }
00144 
00145 
00146 DetectedGames AdvancedMetaEngine::detectGames(const Common::FSList &fslist) const {
00147     FileMap allFiles;
00148 
00149     if (fslist.empty())
00150         return DetectedGames();
00151 
00152     // Compose a hashmap of all files in fslist.
00153     composeFileHashMap(allFiles, fslist, (_maxScanDepth == 0 ? 1 : _maxScanDepth));
00154 
00155     // Run the detector on this
00156     ADDetectedGames matches = detectGame(fslist.begin()->getParent(), allFiles, Common::UNK_LANG, Common::kPlatformUnknown, "");
00157 
00158     cleanupPirated(matches);
00159 
00160     DetectedGames detectedGames;
00161     for (uint i = 0; i < matches.size(); i++) {
00162         DetectedGame game = toDetectedGame(matches[i]);
00163 
00164         if (game.hasUnknownFiles) {
00165             // Non fallback games with unknown files cannot be added/launched
00166             game.canBeAdded = false;
00167         }
00168 
00169         detectedGames.push_back(game);
00170     }
00171 
00172     bool foundKnownGames = false;
00173     for (uint i = 0; i < detectedGames.size(); i++) {
00174         foundKnownGames |= detectedGames[i].canBeAdded;
00175     }
00176 
00177     if (!foundKnownGames) {
00178         // Use fallback detector if there were no matches by other means
00179         ADDetectedGame fallbackDetectionResult = fallbackDetect(allFiles, fslist);
00180 
00181         if (fallbackDetectionResult.desc) {
00182             DetectedGame fallbackDetectedGame = toDetectedGame(fallbackDetectionResult);
00183             fallbackDetectedGame.preferredTarget += "-fallback";
00184 
00185             detectedGames.push_back(fallbackDetectedGame);
00186         }
00187     }
00188 
00189     return detectedGames;
00190 }
00191 
00192 const ExtraGuiOptions AdvancedMetaEngine::getExtraGuiOptions(const Common::String &target) const {
00193     if (!_extraGuiOptions)
00194         return ExtraGuiOptions();
00195 
00196     ExtraGuiOptions options;
00197 
00198     // If there isn't any target specified, return all available GUI options.
00199     // Only used when an engine starts in order to set option defaults.
00200     if (target.empty()) {
00201         for (const ADExtraGuiOptionsMap *entry = _extraGuiOptions; entry->guioFlag; ++entry)
00202             options.push_back(entry->option);
00203 
00204         return options;
00205     }
00206 
00207     // Query the GUI options
00208     const Common::String guiOptionsString = ConfMan.get("guioptions", target);
00209     const Common::String guiOptions = parseGameGUIOptions(guiOptionsString);
00210 
00211     // Add all the applying extra GUI options.
00212     for (const ADExtraGuiOptionsMap *entry = _extraGuiOptions; entry->guioFlag; ++entry) {
00213         if (guiOptions.contains(entry->guioFlag))
00214             options.push_back(entry->option);
00215     }
00216 
00217     return options;
00218 }
00219 
00220 Common::Error AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) const {
00221     assert(engine);
00222 
00223     Common::Language language = Common::UNK_LANG;
00224     Common::Platform platform = Common::kPlatformUnknown;
00225     Common::String extra;
00226 
00227     if (ConfMan.hasKey("language"))
00228         language = Common::parseLanguage(ConfMan.get("language"));
00229     if (ConfMan.hasKey("platform"))
00230         platform = Common::parsePlatform(ConfMan.get("platform"));
00231     if (_flags & kADFlagUseExtraAsHint) {
00232         if (ConfMan.hasKey("extra"))
00233             extra = ConfMan.get("extra");
00234     }
00235 
00236     Common::String gameid = ConfMan.get("gameid");
00237 
00238     Common::String path;
00239     if (ConfMan.hasKey("path")) {
00240         path = ConfMan.get("path");
00241     } else {
00242         path = ".";
00243         warning("No path was provided. Assuming the data files are in the current directory");
00244     }
00245     Common::FSNode dir(path);
00246     Common::FSList files;
00247     if (!dir.isDirectory() || !dir.getChildren(files, Common::FSNode::kListAll)) {
00248         warning("Game data path does not exist or is not a directory (%s)", path.c_str());
00249         return Common::kNoGameDataFoundError;
00250     }
00251 
00252     if (files.empty())
00253         return Common::kNoGameDataFoundError;
00254 
00255     // Compose a hashmap of all files in fslist.
00256     FileMap allFiles;
00257     composeFileHashMap(allFiles, files, (_maxScanDepth == 0 ? 1 : _maxScanDepth));
00258 
00259     // Run the detector on this
00260     ADDetectedGames matches = detectGame(files.begin()->getParent(), allFiles, language, platform, extra);
00261 
00262     if (cleanupPirated(matches))
00263         return Common::kNoGameDataFoundError;
00264 
00265     ADDetectedGame agdDesc;
00266     for (uint i = 0; i < matches.size(); i++) {
00267         if ((_singleId || matches[i].desc->gameId == gameid) && !matches[i].hasUnknownFiles) {
00268             agdDesc = matches[i];
00269             break;
00270         }
00271     }
00272 
00273     if (!agdDesc.desc) {
00274         // Use fallback detector if there were no matches by other means
00275         ADDetectedGame fallbackDetectedGame = fallbackDetect(allFiles, files);
00276         agdDesc = fallbackDetectedGame;
00277         if (agdDesc.desc) {
00278             // Seems we found a fallback match. But first perform a basic
00279             // sanity check: the gameid must match.
00280             if (!_singleId && agdDesc.desc->gameId != gameid)
00281                 agdDesc = ADDetectedGame();
00282         }
00283     }
00284 
00285     if (!agdDesc.desc)
00286         return Common::kNoGameDataFoundError;
00287 
00288     // If the GUI options were updated, we catch this here and update them in the users config
00289     // file transparently.
00290     Common::String lang = getGameGUIOptionsDescriptionLanguage(agdDesc.desc->language);
00291     if (agdDesc.desc->flags & ADGF_ADDENGLISH)
00292         lang += " " + getGameGUIOptionsDescriptionLanguage(Common::EN_ANY);
00293 
00294     Common::updateGameGUIOptions(agdDesc.desc->guiOptions + _guiOptions, lang);
00295 
00296     DetectedGame gameDescriptor = toDetectedGame(agdDesc);
00297 
00298     bool showTestingWarning = false;
00299 
00300 #ifdef RELEASE_BUILD
00301     showTestingWarning = true;
00302 #endif
00303 
00304     if (((gameDescriptor.gameSupportLevel == kUnstableGame
00305             || (gameDescriptor.gameSupportLevel == kTestingGame
00306                     && showTestingWarning)))
00307             && !Engine::warnUserAboutUnsupportedGame())
00308         return Common::kUserCanceled;
00309 
00310     debug(2, "Running %s", gameDescriptor.description.c_str());
00311     initSubSystems(agdDesc.desc);
00312     if (!createInstance(syst, engine, agdDesc.desc))
00313         return Common::kNoGameDataFoundError;
00314     else
00315         return Common::kNoError;
00316 }
00317 
00318 void AdvancedMetaEngine::composeFileHashMap(FileMap &allFiles, const Common::FSList &fslist, int depth, const Common::String &parentName) const {
00319     if (depth <= 0)
00320         return;
00321 
00322     if (fslist.empty())
00323         return;
00324 
00325     for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
00326         Common::String tstr = (_matchFullPaths && !parentName.empty() ? parentName + "/" : "") + file->getName();
00327 
00328         if (file->isDirectory()) {
00329             Common::FSList files;
00330 
00331             if (!_directoryGlobs)
00332                 continue;
00333 
00334             bool matched = false;
00335             for (const char * const *glob = _directoryGlobs; *glob; glob++)
00336                 if (file->getName().matchString(*glob, true)) {
00337                     matched = true;
00338                     break;
00339                 }
00340 
00341             if (!matched)
00342                 continue;
00343 
00344             if (!file->getChildren(files, Common::FSNode::kListAll))
00345                 continue;
00346 
00347             composeFileHashMap(allFiles, files, depth - 1, tstr);
00348         }
00349 
00350         // Strip any trailing dot
00351         if (tstr.lastChar() == '.')
00352             tstr.deleteLastChar();
00353 
00354         allFiles[tstr] = *file; // Record the presence of this file
00355     }
00356 }
00357 
00358 bool AdvancedMetaEngine::getFileProperties(const Common::FSNode &parent, const FileMap &allFiles, const ADGameDescription &game, const Common::String fname, FileProperties &fileProps) const {
00359     // FIXME/TODO: We don't handle the case that a file is listed as a regular
00360     // file and as one with resource fork.
00361 
00362     if (game.flags & ADGF_MACRESFORK) {
00363         Common::MacResManager macResMan;
00364 
00365         if (!macResMan.open(parent, fname))
00366             return false;
00367 
00368         fileProps.md5 = macResMan.computeResForkMD5AsString(_md5Bytes);
00369         fileProps.size = macResMan.getResForkDataSize();
00370 
00371         if (fileProps.size != 0)
00372             return true;
00373     }
00374 
00375     if (!allFiles.contains(fname))
00376         return false;
00377 
00378     Common::File testFile;
00379 
00380     if (!testFile.open(allFiles[fname]))
00381         return false;
00382 
00383     fileProps.size = (int32)testFile.size();
00384     fileProps.md5 = Common::computeStreamMD5AsString(testFile, _md5Bytes);
00385     return true;
00386 }
00387 
00388 ADDetectedGames AdvancedMetaEngine::detectGame(const Common::FSNode &parent, const FileMap &allFiles, Common::Language language, Common::Platform platform, const Common::String &extra) const {
00389     FilePropertiesMap filesProps;
00390     ADDetectedGames matched;
00391 
00392     const ADGameFileDescription *fileDesc;
00393     const ADGameDescription *g;
00394     const byte *descPtr;
00395 
00396     debug(3, "Starting detection in dir '%s'", parent.getPath().c_str());
00397 
00398     // Check which files are included in some ADGameDescription *and* are present.
00399     // Compute MD5s and file sizes for these files.
00400     for (descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) {
00401         g = (const ADGameDescription *)descPtr;
00402 
00403         for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) {
00404             Common::String fname = fileDesc->fileName;
00405             FileProperties tmp;
00406 
00407             if (filesProps.contains(fname))
00408                 continue;
00409 
00410             if (getFileProperties(parent, allFiles, *g, fname, tmp)) {
00411                 debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str());
00412                 filesProps[fname] = tmp;
00413             }
00414         }
00415     }
00416 
00417     int maxFilesMatched = 0;
00418     bool gotAnyMatchesWithAllFiles = false;
00419 
00420     // MD5 based matching
00421     uint i;
00422     for (i = 0, descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize, ++i) {
00423         g = (const ADGameDescription *)descPtr;
00424 
00425         // Do not even bother to look at entries which do not have matching
00426         // language and platform (if specified).
00427         if ((language != Common::UNK_LANG && g->language != Common::UNK_LANG && g->language != language
00428              && !(language == Common::EN_ANY && (g->flags & ADGF_ADDENGLISH))) ||
00429             (platform != Common::kPlatformUnknown && g->platform != Common::kPlatformUnknown && g->platform != platform)) {
00430             continue;
00431         }
00432 
00433         if ((_flags & kADFlagUseExtraAsHint) && !extra.empty() && g->extra != extra)
00434             continue;
00435 
00436         ADDetectedGame game(g);
00437         bool allFilesPresent = true;
00438         int curFilesMatched = 0;
00439 
00440         // Try to match all files for this game
00441         for (fileDesc = game.desc->filesDescriptions; fileDesc->fileName; fileDesc++) {
00442             Common::String tstr = fileDesc->fileName;
00443 
00444             if (!filesProps.contains(tstr)) {
00445                 allFilesPresent = false;
00446                 break;
00447             }
00448 
00449             game.matchedFiles[tstr] = filesProps[tstr];
00450 
00451             if (game.hasUnknownFiles)
00452                 continue;
00453 
00454             if (fileDesc->md5 != nullptr && fileDesc->md5 != filesProps[tstr].md5) {
00455                 debug(3, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesProps[tstr].md5.c_str());
00456                 game.hasUnknownFiles = true;
00457                 continue;
00458             }
00459 
00460             if (fileDesc->fileSize != -1 && fileDesc->fileSize != filesProps[tstr].size) {
00461                 debug(3, "Size Mismatch. Skipping");
00462                 game.hasUnknownFiles = true;
00463                 continue;
00464             }
00465 
00466             debug(3, "Matched file: %s", tstr.c_str());
00467             curFilesMatched++;
00468         }
00469 
00470         // We found at least one entry with all required files present.
00471         // That means that we got new variant of the game.
00472         //
00473         // Without this check we would have erroneous checksum display
00474         // where only located files will be enlisted.
00475         //
00476         // Potentially this could rule out variants where some particular file
00477         // is really missing, but the developers should better know about such
00478         // cases.
00479         if (allFilesPresent && !gotAnyMatchesWithAllFiles) {
00480             if (matched.empty() || strcmp(matched.back().desc->gameId, g->gameId) != 0)
00481                 matched.push_back(game);
00482         }
00483 
00484         if (allFilesPresent && !game.hasUnknownFiles) {
00485             debug(2, "Found game: %s (%s %s/%s) (%d)", g->gameId, g->extra,
00486              getPlatformDescription(g->platform), getLanguageDescription(g->language), i);
00487 
00488             if (curFilesMatched > maxFilesMatched) {
00489                 debug(2, " ... new best match, removing all previous candidates");
00490                 maxFilesMatched = curFilesMatched;
00491 
00492                 matched.clear();    // Remove any prior, lower ranked matches.
00493                 matched.push_back(game);
00494             } else if (curFilesMatched == maxFilesMatched) {
00495                 matched.push_back(game);
00496             } else {
00497                 debug(2, " ... skipped");
00498             }
00499 
00500             gotAnyMatchesWithAllFiles = true;
00501         } else {
00502             debug(5, "Skipping game: %s (%s %s/%s) (%d)", g->gameId, g->extra,
00503              getPlatformDescription(g->platform), getLanguageDescription(g->language), i);
00504         }
00505     }
00506 
00507     return matched;
00508 }
00509 
00510 ADDetectedGame AdvancedMetaEngine::detectGameFilebased(const FileMap &allFiles, const Common::FSList &fslist, const ADFileBasedFallback *fileBasedFallback) const {
00511     const ADFileBasedFallback *ptr;
00512     const char* const* filenames;
00513 
00514     int maxNumMatchedFiles = 0;
00515     ADDetectedGame result;
00516 
00517     for (ptr = fileBasedFallback; ptr->desc; ++ptr) {
00518         const ADGameDescription *agdesc = ptr->desc;
00519         int numMatchedFiles = 0;
00520         bool fileMissing = false;
00521 
00522         for (filenames = ptr->filenames; *filenames; ++filenames) {
00523             debug(3, "++ %s", *filenames);
00524             if (!allFiles.contains(*filenames)) {
00525                 fileMissing = true;
00526                 break;
00527             }
00528 
00529             numMatchedFiles++;
00530         }
00531 
00532         if (!fileMissing) {
00533             debug(4, "Matched: %s", agdesc->gameId);
00534 
00535             if (numMatchedFiles > maxNumMatchedFiles) {
00536                 maxNumMatchedFiles = numMatchedFiles;
00537 
00538                 debug(4, "and overridden");
00539 
00540                 ADDetectedGame game(agdesc);
00541                 game.hasUnknownFiles = true;
00542 
00543                 for (filenames = ptr->filenames; *filenames; ++filenames) {
00544                     FileProperties tmp;
00545 
00546                     if (getFileProperties(fslist.begin()->getParent(), allFiles, *agdesc, *filenames, tmp))
00547                         game.matchedFiles[*filenames] = tmp;
00548                 }
00549 
00550                 result = game;
00551             }
00552         }
00553     }
00554 
00555     return result;
00556 }
00557 
00558 PlainGameList AdvancedMetaEngine::getSupportedGames() const {
00559     if (_singleId != NULL) {
00560         PlainGameList gl;
00561 
00562         const PlainGameDescriptor *g = _gameIds;
00563         while (g->gameId) {
00564             if (0 == scumm_stricmp(_singleId, g->gameId)) {
00565                 gl.push_back(*g);
00566 
00567                 return gl;
00568             }
00569             g++;
00570         }
00571         error("Engine %s doesn't have its singleid specified in ids list", _singleId);
00572     }
00573 
00574     return PlainGameList(_gameIds);
00575 }
00576 
00577 PlainGameDescriptor AdvancedMetaEngine::findGame(const char *gameId) const {
00578     // First search the list of supported gameids for a match.
00579     const PlainGameDescriptor *g = findPlainGameDescriptor(gameId, _gameIds);
00580     if (g)
00581         return *g;
00582 
00583     // No match found
00584     return PlainGameDescriptor::empty();
00585 }
00586 
00587 AdvancedMetaEngine::AdvancedMetaEngine(const void *descs, uint descItemSize, const PlainGameDescriptor *gameIds, const ADExtraGuiOptionsMap *extraGuiOptions)
00588     : _gameDescriptors((const byte *)descs), _descItemSize(descItemSize), _gameIds(gameIds),
00589       _extraGuiOptions(extraGuiOptions) {
00590 
00591     _md5Bytes = 5000;
00592     _singleId = NULL;
00593     _flags = 0;
00594     _guiOptions = GUIO_NONE;
00595     _maxScanDepth = 1;
00596     _directoryGlobs = NULL;
00597     _matchFullPaths = false;
00598 }
00599 
00600 void AdvancedMetaEngine::initSubSystems(const ADGameDescription *gameDesc) const {
00601 #ifdef ENABLE_EVENTRECORDER
00602     if (gameDesc) {
00603         g_eventRec.processGameDescription(gameDesc);
00604     }
00605 #endif
00606 }


Generated on Sat Mar 23 2019 05:01:14 for ResidualVM by doxygen 1.7.1
curved edge   curved edge