/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #define FORBIDDEN_SYMBOL_EXCEPTION_printf #include "common/debug.h" #include "common/util.h" #include "common/file.h" #include "common/macresman.h" #include "common/md5.h" #include "common/config-manager.h" #include "common/punycode.h" #include "common/system.h" #include "common/textconsole.h" #include "common/tokenizer.h" #include "common/translation.h" #include "common/compression/installshield_cab.h" #include "common/compression/installshieldv3_archive.h" #include "gui/EventRecorder.h" #include "gui/gui-manager.h" #include "gui/message.h" #include "engines/advancedDetector.h" #include "engines/obsolete.h" /** * Adapter to be able to use Common::Archive based code from the AD. */ class FileMapArchive : public Common::Archive { public: FileMapArchive(const AdvancedMetaEngineDetection::FileMap &fileMap) : _fileMap(fileMap) {} bool hasFile(const Common::Path &path) const override { return _fileMap.contains(path); } int listMembers(Common::ArchiveMemberList &list) const override { int files = 0; for (AdvancedMetaEngineDetection::FileMap::const_iterator it = _fileMap.begin(); it != _fileMap.end(); ++it) { list.push_back(Common::ArchiveMemberPtr(new Common::FSNode(it->_value))); ++files; } return files; } const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override { AdvancedMetaEngineDetection::FileMap::const_iterator it = _fileMap.find(path); if (it == _fileMap.end()) { return Common::ArchiveMemberPtr(); } return Common::ArchiveMemberPtr(new Common::FSNode(it->_value)); } Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override { Common::FSNode fsNode = _fileMap.getValOrDefault(path); return fsNode.createReadStream(); } private: const AdvancedMetaEngineDetection::FileMap &_fileMap; }; static Common::String sanitizeName(const char *name, int maxLen) { Common::String res; Common::String word; Common::String lastWord; const char *origname = name; do { if (Common::isAlnum(*name)) { word += tolower(*name); } else { // Skipping short words and "the" if ((word.size() > 2 && !word.equals("the")) || (!word.empty() && Common::isDigit(word[0]))) { // Adding first word, or when word fits if (res.empty() || (int)word.size() < maxLen) res += word; maxLen -= word.size(); } if ((*name && *(name + 1) == 0) || !*name) { if (res.empty()) // Make sure that we add at least something res += word.empty() ? lastWord : word; break; } if (!word.empty()) lastWord = word; word.clear(); } if (*name) name++; } while (maxLen > 0); if (res.empty()) error("AdvancedDetector: Incorrect extra in game: \"%s\"", origname); return res; } /** * Generate a preferred target value as * GAMEID-PLAFORM-LANG * or (if ADGF_DEMO has been set) * GAMEID-demo-PLAFORM-LANG */ static Common::String generatePreferredTarget(const ADGameDescription *desc, int maxLen, Common::String targetID) { Common::String res; if (!targetID.empty()) { res = targetID; } else if (desc->flags & ADGF_AUTOGENTARGET && desc->extra && *desc->extra) { res = sanitizeName(desc->extra, maxLen); } else { res = desc->gameId; } if (desc->flags & ADGF_DEMO) { res = res + "-demo"; } if (desc->flags & ADGF_CD) { res = res + "-cd"; } if (desc->flags & ADGF_DVD) { res = res + "-dvd"; } if (desc->flags & ADGF_REMASTERED) { res = res + "-remastered"; } if (desc->platform != Common::kPlatformDOS && desc->platform != Common::kPlatformUnknown && !(desc->flags & ADGF_DROPPLATFORM)) { res = res + "-" + getPlatformAbbrev(desc->platform); } if (desc->language != Common::EN_ANY && desc->language != Common::UNK_LANG && !(desc->flags & ADGF_DROPLANGUAGE)) { res = res + "-" + getLanguageCode(desc->language); } return res; } DetectedGame AdvancedMetaEngineDetection::toDetectedGame(const ADDetectedGame &adGame, ADDetectedGameExtraInfo *extraInfo) const { const ADGameDescription *desc = adGame.desc; const char *title; const char *extra; if (desc->flags & ADGF_USEEXTRAASTITLE) { title = desc->extra; extra = ""; } else { const PlainGameDescriptor *pgd = findPlainGameDescriptor(desc->gameId, _gameIds); if (pgd) { title = pgd->description; } else { title = ""; } extra = desc->extra; } if (extraInfo) { if (!extraInfo->gameName.empty()) title = extraInfo->gameName.c_str(); } DetectedGame game(getName(), desc->gameId, title, desc->language, desc->platform, extra, ((desc->flags & (ADGF_UNSUPPORTED | ADGF_WARNING)) != 0)); game.hasUnknownFiles = adGame.hasUnknownFiles; game.matchedFiles = adGame.matchedFiles; if (extraInfo && !extraInfo->targetID.empty()) { game.preferredTarget = generatePreferredTarget(desc, _maxAutogenLength, extraInfo->targetID); } else { game.preferredTarget = generatePreferredTarget(desc, _maxAutogenLength, Common::String()); } game.gameSupportLevel = kStableGame; if (desc->flags & ADGF_UNSTABLE) game.gameSupportLevel = kUnstableGame; else if (desc->flags & ADGF_TESTING) game.gameSupportLevel = kTestingGame; else if (desc->flags & ADGF_UNSUPPORTED) game.gameSupportLevel = kUnsupportedGame; else if (desc->flags & ADGF_WARNING) game.gameSupportLevel = kWarningGame; game.setGUIOptions(desc->guiOptions + _guiOptions); game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(desc->language)); if (desc->flags & ADGF_ADDENGLISH) game.appendGUIOptions(getGameGUIOptionsDescriptionLanguage(Common::EN_ANY)); if (_flags & kADFlagUseExtraAsHint) game.extra = desc->extra; return game; } bool AdvancedMetaEngineDetection::cleanupPirated(ADDetectedGames &matched) const { // OKay, now let's sense presence of pirated games if (!matched.empty()) { for (uint j = 0; j < matched.size();) { if (matched[j].desc->flags & ADGF_PIRATED) matched.remove_at(j); else ++j; } // We ruled out all variants and now have nothing if (matched.empty()) { warning("Illegitimate game copy detected. We provide no support in such cases"); if (GUI::GuiManager::hasInstance()) { GUI::MessageDialog dialog(_("Illegitimate game copy detected. We provide no support in such cases")); dialog.runModal(); }; return true; } } return false; } DetectedGames AdvancedMetaEngineDetection::detectGames(const Common::FSList &fslist, uint32 skipADFlags, bool skipIncomplete) { FileMap allFiles; if (fslist.empty()) return DetectedGames(); // Sometimes this method is called directly, so we have to build the maps, especially // the _directoryGlobsMap preprocessDescriptions(); // Compose a hashmap of all files in fslist. composeFileHashMap(allFiles, fslist, (_maxScanDepth == 0 ? 1 : _maxScanDepth)); // Run the detector on this ADDetectedGames matches = detectGame(fslist.begin()->getParent(), allFiles, Common::UNK_LANG, Common::kPlatformUnknown, "", skipADFlags, skipIncomplete); cleanupPirated(matches); DetectedGames detectedGames; for (uint i = 0; i < matches.size(); i++) { DetectedGame game = toDetectedGame(matches[i]); if (game.hasUnknownFiles && !(_flags & kADFlagCanPlayUnknownVariants)) { game.canBeAdded = false; } detectedGames.push_back(game); } bool foundKnownGames = false; for (uint i = 0; i < detectedGames.size(); i++) { foundKnownGames |= !detectedGames[i].hasUnknownFiles; } if (!foundKnownGames) { // Use fallback detector if there were no matches by other means ADDetectedGameExtraInfo *extraInfo = nullptr; ADDetectedGame fallbackDetectionResult = fallbackDetect(allFiles, fslist, &extraInfo); if (fallbackDetectionResult.desc) { DetectedGame fallbackDetectedGame = toDetectedGame(fallbackDetectionResult, extraInfo); if (extraInfo != nullptr) { // then it's our duty to free it delete extraInfo; } else { // don't add fallback when we are specifying the targetID fallbackDetectedGame.preferredTarget += "-fallback"; } if (_flags & kADFlagPreferFallbackDetection) { // fallback detection was successful and this engine prefers // it over the partial matches found in the detection table detectedGames.clear(); } detectedGames.push_back(fallbackDetectedGame); } } return detectedGames; } const ExtraGuiOptions AdvancedMetaEngine::getExtraGuiOptions(const Common::String &target) const { const ADExtraGuiOptionsMap *extraGuiOptions = getAdvancedExtraGuiOptions(); if (!extraGuiOptions) return ExtraGuiOptions(); ExtraGuiOptions options; // If there isn't any target specified, return all available GUI options. // Only used when an engine starts in order to set option defaults. if (target.empty()) { for (const ADExtraGuiOptionsMap *entry = extraGuiOptions; entry->guioFlag; ++entry) options.push_back(entry->option); return options; } // Query the GUI options const Common::String guiOptionsString = ConfMan.get("guioptions", target); const Common::String guiOptions = parseGameGUIOptions(guiOptionsString); // Add all the applying extra GUI options. for (const ADExtraGuiOptionsMap *entry = extraGuiOptions; entry->guioFlag; ++entry) { if (guiOptions.contains(entry->guioFlag)) options.push_back(entry->option); } return options; } Common::Error AdvancedMetaEngineDetection::createInstance(OSystem *syst, Engine **engine) { assert(engine); Common::Language language = Common::UNK_LANG; Common::Platform platform = Common::kPlatformUnknown; Common::String extra; if (ConfMan.hasKey("language")) language = Common::parseLanguage(ConfMan.get("language")); if (ConfMan.hasKey("platform")) platform = Common::parsePlatform(ConfMan.get("platform")); if (_flags & kADFlagUseExtraAsHint) { if (ConfMan.hasKey("extra")) extra = ConfMan.get("extra"); } Common::String gameid = ConfMan.get("gameid"); Common::Path path; if (ConfMan.hasKey("path")) { path = ConfMan.getPath("path"); } else { path = Common::Path("."); warning("No path was provided. Assuming the data files are in the current directory"); } Common::FSNode dir(path); Common::FSList files; if (!dir.isDirectory() || !dir.getChildren(files, Common::FSNode::kListAll)) { warning("Game data path does not exist or is not a directory (%s)", path.toString(Common::Path::kNativeSeparator).c_str()); return Common::kNoGameDataFoundError; } if (files.empty()) return Common::kNoGameDataFoundError; // Sometimes this method is called directly, so we have to build the maps, especially // the _directoryGlobsMap preprocessDescriptions(); // Compose a hashmap of all files in fslist. FileMap allFiles; composeFileHashMap(allFiles, files, (_maxScanDepth == 0 ? 1 : _maxScanDepth)); // Clear md5 cache before each detection starts, just in case. ADCacheMan.clear(); // Run the detector on this ADDetectedGames matches = detectGame(files.begin()->getParent(), allFiles, language, platform, extra); if (cleanupPirated(matches)) return Common::kNoGameDataFoundError; ADDetectedGame agdDesc; for (uint i = 0; i < matches.size(); i++) { if (matches[i].desc->gameId == gameid && (!matches[i].hasUnknownFiles || (_flags & kADFlagCanPlayUnknownVariants))) { agdDesc = matches[i]; break; } } if (!agdDesc.desc) { // Use fallback detector if there were no matches by other means ADDetectedGame fallbackDetectedGame = fallbackDetect(allFiles, files); agdDesc = fallbackDetectedGame; if (agdDesc.desc) { // Seems we found a fallback match. But first perform a basic // sanity check: the gameid must match. if (agdDesc.desc->gameId != gameid) agdDesc = ADDetectedGame(); } } if (!agdDesc.desc) return Common::kNoGameDataFoundError; DetectedGame gameDescriptor = toDetectedGame(agdDesc); // Detection is done, no need to keep archives in memory anymore ADCacheMan.clearArchives(); // If the GUI options were updated, we catch this here and update them in the users config // file transparently. ConfMan.setAndFlush("guioptions", gameDescriptor.getGUIOptions()); bool showTestingWarning = false; #ifdef RELEASE_BUILD showTestingWarning = true; #endif if (((gameDescriptor.gameSupportLevel == kUnstableGame || (gameDescriptor.gameSupportLevel == kTestingGame && showTestingWarning))) && !Engine::warnUserAboutUnsupportedGame()) return Common::kUserCanceled; if (gameDescriptor.gameSupportLevel == kWarningGame && !Engine::warnUserAboutUnsupportedGame(gameDescriptor.extra)) return Common::kUserCanceled; if (gameDescriptor.gameSupportLevel == kUnsupportedGame) { Engine::errorUnsupportedGame(gameDescriptor.extra); return Common::kUserCanceled; } debug("Running %s", gameDescriptor.description.c_str()); Common::Array filenames; for (FilePropertiesMap::const_iterator i = gameDescriptor.matchedFiles.begin(); i != gameDescriptor.matchedFiles.end(); ++i) { filenames.push_back(i->_key); } Common::sort(filenames.begin(), filenames.end()); for (uint i = 0; i < filenames.size(); ++i) { const FileProperties &file = gameDescriptor.matchedFiles[filenames[i]]; debug("%s: %s, %llu bytes.", filenames[i].toString().c_str(), file.md5.c_str(), (unsigned long long)file.size); } initSubSystems(agdDesc.desc); PluginList pl = EngineMan.getPlugins(PLUGIN_TYPE_ENGINE); Plugin *plugin = nullptr; // By this point of time, we should have only one plugin in memory. if (pl.size() == 1) { plugin = pl[0]; } if (plugin) { if (_flags & kADFlagMatchFullPaths) { Common::StringArray dirs = getPathsFromEntry(agdDesc.desc); Common::FSNode gameDataDir = Common::FSNode(ConfMan.getPath("path")); for (auto d = dirs.begin(); d != dirs.end(); ++d) SearchMan.addSubDirectoryMatching(gameDataDir, *d, 0, _fullPathGlobsDepth); } // Call child class's createInstanceMethod. return plugin->get().createInstance(syst, engine, agdDesc.desc); } return Common::Error(Common::kEnginePluginNotFound); } void AdvancedMetaEngineDetection::composeFileHashMap(FileMap &allFiles, const Common::FSList &fslist, int depth, const Common::Path &parentName) const { if (depth <= 0) return; if (fslist.empty()) return; for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { Common::String efname = Common::punycode_encodefilename(file->getName()); Common::Path tstr = ((_flags & kADFlagMatchFullPaths) ? parentName : Common::Path()).appendComponent(efname); if (file->isDirectory()) { if (!_globsMap.contains(efname)) continue; Common::FSList files; if (!file->getChildren(files, Common::FSNode::kListAll)) continue; composeFileHashMap(allFiles, files, depth - 1, tstr); continue; } // Strip any trailing dot if (efname.lastChar() == '.') { efname.deleteLastChar(); tstr = ((_flags & kADFlagMatchFullPaths) ? parentName : Common::Path()).appendComponent(efname); } debugC(9, kDebugGlobalDetection, "$$ ['%s'] ['%s'] in '%s", tstr.toString().c_str(), efname.c_str(), firstPathComponents(fslist.front().getPath().toString(), '/').c_str()); allFiles[tstr] = *file; // Record the presence of this file allFiles[Common::Path(efname, Common::Path::kNoSeparator)] = *file; // ...and its file name } } /* Singleton Cache Storage for MD5 */ namespace Common { DECLARE_SINGLETON(AdvancedDetectorCacheManager); } static MD5Properties gameFileToMD5Props(const ADGameFileDescription *fileEntry, uint32 gameFlags) { MD5Properties ret = kMD5Head; if (fileEntry && fileEntry->md5 && strchr(fileEntry->md5, ':')) { const char *ptr; for (ptr = fileEntry->md5; *ptr != ':'; ptr++) switch (*ptr) { case 'r': ret = (MD5Properties)(ret | kMD5MacResFork); break; case 'd': ret = (MD5Properties)(ret | kMD5MacDataFork); break; case 't': ret = (MD5Properties)(ret | kMD5Tail); break; case 'A': ret = (MD5Properties)(ret | kMD5Archive); } return ret; } if (gameFlags & ADGF_MACRESFORK) { ret = (MD5Properties)(ret | kMD5MacResOrDataFork); } if (gameFlags & ADGF_TAILMD5) { ret = (MD5Properties)(ret | kMD5Tail); } return ret; } Common::String md5PropToGameFile(MD5Properties flags) { Common::String res; switch (flags & kMD5MacMask) { case kMD5MacDataFork: res = "d"; break; case kMD5MacResOrDataFork: res = "m"; break; case kMD5MacResFork: res = "r"; break; default: break; } if (flags & kMD5Tail) res += "t"; if (flags & kMD5Archive) res += "A"; return res; } static bool getFilePropertiesIntern(uint md5Bytes, const AdvancedMetaEngine::FileMap &allFiles, MD5Properties md5prop, const Common::Path &fname, FileProperties &fileProps); bool AdvancedMetaEngineDetection::getFileProperties(const FileMap &allFiles, MD5Properties md5prop, const Common::Path &fname, FileProperties &fileProps) const { Common::String hashname = md5PropToCachePrefix(md5prop); hashname += ':'; hashname += fname.toString('/'); hashname += ':'; hashname += Common::String::format("%d", _md5Bytes); if (ADCacheMan.containsMD5(hashname)) { fileProps.md5 = ADCacheMan.getMD5(hashname); fileProps.size = ADCacheMan.getSize(hashname); return true; } bool res = getFilePropertiesIntern(_md5Bytes, allFiles, md5prop, fname, fileProps); if (res) { ADCacheMan.setMD5(hashname, fileProps.md5); ADCacheMan.setSize(hashname, fileProps.size); } return res; } bool AdvancedMetaEngine::getFilePropertiesExtern(uint md5Bytes, const FileMap &allFiles, MD5Properties md5prop, const Common::Path &fname, FileProperties &fileProps) const { return getFilePropertiesIntern(md5Bytes, allFiles, md5prop, fname, fileProps); } static bool getFilePropertiesIntern(uint md5Bytes, const AdvancedMetaEngine::FileMap &allFiles, MD5Properties md5prop, const Common::Path &fname, FileProperties &fileProps) { if (md5prop & (kMD5MacResFork | kMD5MacDataFork)) { FileMapArchive fileMapArchive(allFiles); bool is_legacy = ((md5prop & kMD5MacMask) == kMD5MacResOrDataFork); if (md5prop & kMD5MacResFork) { Common::MacResManager macResMan; if (!macResMan.open(fname, fileMapArchive)) return false; fileProps.md5 = macResMan.computeResForkMD5AsString(md5Bytes, ((md5prop & kMD5Tail) != 0)); fileProps.size = macResMan.getResForkDataSize(); if (fileProps.size != 0) { fileProps.md5prop = (MD5Properties)((md5prop & kMD5Tail) | kMD5MacResFork); return true; } } if (md5prop & kMD5MacDataFork) { Common::SeekableReadStream *dataFork = Common::MacResManager::openFileOrDataFork(fname, fileMapArchive); // Logically 0-sized data fork is valid but legacy code continues fallback if (dataFork && (dataFork->size() || !is_legacy)) { fileProps.size = dataFork->size(); fileProps.md5 = Common::computeStreamMD5AsString(*dataFork, md5Bytes); fileProps.md5prop = (MD5Properties)((md5prop & kMD5Tail) | kMD5MacDataFork); delete dataFork; return true; } delete dataFork; } // In modern case stop here if (!is_legacy) return false; } Common::ScopedPtr testFile; if (md5prop & kMD5Archive) { // The desired file is inside an archive // First, split the file string Common::StringTokenizer tok(fname.toString(), ":"); Common::String archiveType = tok.nextToken(); Common::Path archiveName(tok.nextToken()); Common::Path fileName(tok.nextToken()); if (!allFiles.contains(archiveName)) return false; // Check if archive has already been opened and is stored in cache Common::Archive *archive = ADCacheMan.getArchive(allFiles[archiveName]); if (!archive) { // Archive not in cache. Find the appropriate type based on the type string, // open the archive, and add it to the cache if (archiveType.equals("is")) { // InstallShield (v4 and up) archive = Common::makeInstallShieldArchive(allFiles[archiveName]); ADCacheMan.addArchive(allFiles[archiveName], archive); if (!archive) return false; } else if (archiveType.equals("is3")) { // InstallShield v3 archive = new Common::InstallShieldV3(); if (((Common::InstallShieldV3 *)archive)->open(allFiles[archiveName])) { ADCacheMan.addArchive(allFiles[archiveName], archive); } else { delete archive; return false; } } else { debugC(3, kDebugGlobalDetection, "WARNING: Archive type string '%s' not recognized", archiveType.c_str()); return false; } } // Look for file with matching name inside the archive testFile.reset(archive->createReadStreamForMember(fileName)); if (!testFile) { return false; } } else { if (!allFiles.contains(fname)) return false; testFile.reset(new Common::File()); if (!((Common::File *)testFile.get())->open(allFiles[fname])) return false; } if (md5prop & kMD5Tail) { if (testFile->size() > md5Bytes) testFile->seek(-(int64)md5Bytes, SEEK_END); } fileProps.size = testFile->size(); fileProps.md5 = Common::computeStreamMD5AsString(*testFile.get(), md5Bytes); fileProps.md5prop = (MD5Properties) (md5prop & kMD5Tail); return true; } // Add backslash before double quotes (") and backslashes themselves (\) Common::String escapeString(const char *string) { if (string == nullptr) return ""; Common::String res = ""; for (int i = 0; string[i] != '\0'; i++) { if (string[i] == '"' || string[i] == '\\') res += "\\"; res += string[i]; } return res; } void AdvancedMetaEngineDetection::dumpDetectionEntries() const { const byte *descPtr; for (descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) { auto g = ((const ADGameDescription *)descPtr); const PlainGameDescriptor *gameDesc = findPlainGameDescriptor(g->gameId, _gameIds); const char *title = ""; if (gameDesc != 0) title = gameDesc->description; printf("game (\n"); printf("\tname \"%s\"\n", escapeString(g->gameId).c_str()); printf("\ttitle \"%s\"\n", escapeString(title).c_str()); printf("\textra \"%s\"\n", escapeString(g->extra).c_str()); printf("\tlanguage \"%s\"\n", escapeString(getLanguageLocale(g->language)).c_str()); printf("\tplatform \"%s\"\n", escapeString(getPlatformCode(g->platform)).c_str()); printf("\tsourcefile \"%s\"\n", escapeString(getName()).c_str()); printf("\tengine \"%s\"\n", escapeString(getEngineName()).c_str()); for (auto fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { const char *fname = fileDesc->fileName; int64 fsize = fileDesc->fileSize; Common::String md5 = fileDesc->md5; MD5Properties md5prop = gameFileToMD5Props(fileDesc, g->flags); Common::String md5Prefix = md5PropToGameFile(md5prop); Common::String key = md5; if (md5Prefix != "" && md5.find(':') == Common::String::npos) key = md5Prefix + ':' + md5; printf("\trom ( name \"%s\" size %lld md5-%d %s )\n", escapeString(fname).c_str(), static_cast(fsize), _md5Bytes, key.c_str()); } printf(")\n\n"); // Closing for 'game (' } } ADDetectedGames AdvancedMetaEngineDetection::detectGame(const Common::FSNode &parent, const FileMap &allFiles, Common::Language language, Common::Platform platform, const Common::String &extra, uint32 skipADFlags, bool skipIncomplete) { CachedPropertiesMap filesProps; ADDetectedGames matched; const ADGameFileDescription *fileDesc; const ADGameDescription *g; const byte *descPtr; debugC(3, kDebugGlobalDetection, "Starting detection for engine '%s' in dir '%s'", getName(), parent.getPath().toString(Common::Path::kNativeSeparator).c_str()); preprocessDescriptions(); // Check which files are included in some ADGameDescription *and* whether // they are present. Compute MD5s and file sizes for the available files. for (descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) { g = (const ADGameDescription *)descPtr; for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { MD5Properties md5prop = gameFileToMD5Props(fileDesc, g->flags); Common::String fname = fileDesc->fileName; Common::String key = md5PropToCachePrefix(md5prop); key += ':'; key += fname; if (filesProps.contains(key)) continue; FileProperties tmp; if (getFileProperties(allFiles, md5prop, Common::Path(fname), tmp)) { debugC(3, kDebugGlobalDetection, "> '%s': '%s' %ld", key.c_str(), tmp.md5.c_str(), long(tmp.size)); } // Both positive and negative results are cached to avoid // repeatedly checking for files. filesProps[key] = tmp; } } int maxFilesMatched = 0; bool gotAnyMatchesWithAllFiles = false; // MD5 based matching uint i; for (i = 0, descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize, ++i) { g = (const ADGameDescription *)descPtr; // Do not even bother to look at entries which do not have matching // language and platform (if specified). if ((language != Common::UNK_LANG && g->language != Common::UNK_LANG && g->language != language && !(language == Common::EN_ANY && (g->flags & ADGF_ADDENGLISH))) || (platform != Common::kPlatformUnknown && g->platform != Common::kPlatformUnknown && g->platform != platform)) { continue; } if ((_flags & kADFlagUseExtraAsHint) && !extra.empty() && g->extra != extra) continue; if (g->flags & skipADFlags) { debugC(3, kDebugGlobalDetection, "Skipping unsupported target for engine '%s' for the following reasons:\n\t'%s'", g->gameId, g->extra); continue; } ADDetectedGame game(g); bool allFilesPresent = true; int curFilesMatched = 0; // Try to match all files for this game for (fileDesc = game.desc->filesDescriptions; fileDesc->fileName; fileDesc++) { Common::String tstr = fileDesc->fileName; MD5Properties md5prop = gameFileToMD5Props(fileDesc, g->flags); Common::String key = md5PropToCachePrefix(md5prop); key += ':'; key += tstr; if (!filesProps.contains(key) || filesProps[key].size == -1) { allFilesPresent = false; break; } game.matchedFiles[Common::Path(tstr)] = filesProps[key]; if (game.hasUnknownFiles) continue; const char *md5_wo_prefix = fileDesc->md5; if (md5_wo_prefix && strchr(md5_wo_prefix, ':')) md5_wo_prefix = strchr(md5_wo_prefix, ':') + 1; if (md5_wo_prefix != nullptr && md5_wo_prefix != filesProps[key].md5) { debugC(3, kDebugGlobalDetection, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesProps[key].md5.c_str()); game.hasUnknownFiles = true; continue; } if (fileDesc->fileSize != -1 && fileDesc->fileSize != filesProps[key].size) { debugC(3, kDebugGlobalDetection, "Size Mismatch. Skipping (%ld) (%ld)", long(fileDesc->fileSize), long(filesProps[key].size)); game.hasUnknownFiles = true; continue; } debugC(3, kDebugGlobalDetection, "Matched file: %s", tstr.c_str()); curFilesMatched++; } // We found at least one entry with all required files present. // That means that we got new variant of the game. // // Without this check we would have erroneous checksum display // where only located files will be enlisted. // // Potentially this could rule out variants where some particular file // is really missing, but the developers should better know about such // cases. if (allFilesPresent && !gotAnyMatchesWithAllFiles) { // Do sanity check if (game.hasUnknownFiles && (skipIncomplete || isEntryGrayListed(g))) { debugC(3, kDebugGlobalDetection, "Skipping game: %s (%s %s/%s) (%d), %s %s", g->gameId, g->extra, getPlatformDescription(g->platform), getLanguageDescription(g->language), i, skipIncomplete ? "(ignore incomplete matches)" : "", isEntryGrayListed(g) ? "(didn't pass sanity test)" : ""); continue; } if (matched.empty() || strcmp(matched.back().desc->gameId, g->gameId) != 0) matched.push_back(game); } if (allFilesPresent && !game.hasUnknownFiles) { debugC(2, kDebugGlobalDetection, "Found game: %s (%s %s/%s) (%d)", g->gameId, g->extra, getPlatformDescription(g->platform), getLanguageDescription(g->language), i); if (curFilesMatched > maxFilesMatched) { debugC(2, kDebugGlobalDetection, " ... new best match, removing all previous candidates"); maxFilesMatched = curFilesMatched; matched.clear(); // Remove any prior, lower ranked matches. matched.push_back(game); } else if (curFilesMatched == maxFilesMatched) { matched.push_back(game); } else { debugC(2, kDebugGlobalDetection, " ... skipped"); } gotAnyMatchesWithAllFiles = true; } else { debugC(7, kDebugGlobalDetection, "Skipping game: %s (%s %s/%s) (%d)", g->gameId, g->extra, getPlatformDescription(g->platform), getLanguageDescription(g->language), i); } } debugC(2, "Totally found %d matches", matched.size()); return matched; } ADDetectedGame AdvancedMetaEngineDetection::detectGameFilebased(const FileMap &allFiles, const ADFileBasedFallback *fileBasedFallback) const { const ADFileBasedFallback *ptr; const char* const* filenames; int maxNumMatchedFiles = 0; ADDetectedGame result; for (ptr = fileBasedFallback; ptr->desc; ++ptr) { const ADGameDescription *agdesc = ptr->desc; int numMatchedFiles = 0; bool fileMissing = false; for (filenames = ptr->filenames; *filenames; ++filenames) { debugC(3, kDebugGlobalDetection, "++ %s", *filenames); if (!allFiles.contains(*filenames)) { fileMissing = true; break; } numMatchedFiles++; } if (!fileMissing) { debugC(4, kDebugGlobalDetection, "Matched: %s", agdesc->gameId); if (numMatchedFiles > maxNumMatchedFiles) { MD5Properties md5prop = gameFileToMD5Props(nullptr, agdesc->flags); maxNumMatchedFiles = numMatchedFiles; debugC(4, kDebugGlobalDetection, "and overridden"); ADDetectedGame game(agdesc); game.hasUnknownFiles = true; for (filenames = ptr->filenames; *filenames; ++filenames) { FileProperties tmp; if (getFileProperties(allFiles, md5prop, *filenames, tmp)) game.matchedFiles[*filenames] = tmp; } result = game; } } } return result; } PlainGameList AdvancedMetaEngineDetection::getSupportedGames() const { return PlainGameList(_gameIds); } PlainGameDescriptor AdvancedMetaEngineDetection::findGame(const char *gameId) const { // First search the list of supported gameids for a match. const PlainGameDescriptor *g = findPlainGameDescriptor(gameId, _gameIds); if (g) return *g; // No match found return PlainGameDescriptor::empty(); } static const char *const grayList[] = { "game.exe", "demo.exe", "game", "gamedata", "demo", "demo.ini", "data", "data.z", "data.cab", "data1.hdr", "data1.cab", "data2.cab", "data3.cab", "data4.cab", "data5.cab", "Double-click me", "engine.exe", "install.exe", "play.exe", "start.exe", "item.dat", "abc.exe", "ppc", 0 }; AdvancedMetaEngineDetection::AdvancedMetaEngineDetection(const void *descs, uint descItemSize, const PlainGameDescriptor *gameIds) : _gameDescriptors((const byte *)descs), _descItemSize(descItemSize), _gameIds(gameIds) { _md5Bytes = 5000; _flags = 0; _guiOptions = GUIO_NONE; _maxScanDepth = 1; _directoryGlobs = NULL; _maxAutogenLength = 15; _fullPathGlobsDepth = 5; _hashMapsInited = false; for (auto f = grayList; *f; f++) _grayListMap.setVal(*f, true); } void AdvancedMetaEngineDetection::initSubSystems(const ADGameDescription *gameDesc) const { #ifdef ENABLE_EVENTRECORDER if (gameDesc) { g_eventRec.processGameDescription(gameDesc); } #endif } void AdvancedMetaEngineDetection::preprocessDescriptions() { if (_hashMapsInited) return; _hashMapsInited = true; // Put all directory globs into a hashmap for faster usage if (_directoryGlobs) { for (auto glob = _directoryGlobs; *glob; glob++) _globsMap.setVal(*glob, true); } // Now scan all detection entries for (const byte *descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) { const ADGameDescription *g = (const ADGameDescription *)descPtr; // Scan for potential directory globs for (const ADGameFileDescription *fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { if (strchr(fileDesc->fileName, '/')) { if (!(_flags & kADFlagMatchFullPaths)) warning("Path component detected in entry for '%s:%s' but no kADFlagMatchFullPaths is set", getName(), g->gameId); Common::StringTokenizer tok(fileDesc->fileName, "/"); uint32 depth = 0; while (!tok.empty()) { Common::String component = tok.nextToken(); if (!tok.empty() && !_globsMap.contains(component.c_str())) { // If it is not the last component _globsMap.setVal(component, true); debugC(4, kDebugGlobalDetection, " Added '%s' to globs", component.c_str()); } depth++; } if (depth > _maxScanDepth) { _maxScanDepth = depth; debugC(4, kDebugGlobalDetection, " Increased scan depth to %d", _maxScanDepth); } } } // Check if the detection entry have only files from the blacklist if (isEntryGrayListed(g)) { debug(0, "WARNING: Detection entry for '%s:%s' contains only blacklisted names. Add more files to the entry (%s)", getName(), g->gameId, g->filesDescriptions[0].md5); } } #ifndef RELEASE_BUILD // Check the provided tables for sanity detectClashes(); #endif } Common::StringArray AdvancedMetaEngineDetection::getPathsFromEntry(const ADGameDescription *g) { Common::StringArray result; Common::HashMap unique; for (const ADGameFileDescription *fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { if (!strchr(fileDesc->fileName, '/')) continue; Common::StringTokenizer tok(fileDesc->fileName, "/"); while (!tok.empty()) { Common::String component = tok.nextToken(); if (!tok.empty()) { // If it is not the last component if (!unique.contains(component)) result.push_back(component); unique[component] = true; } } } return result; } bool AdvancedMetaEngineDetection::isEntryGrayListed(const ADGameDescription *g) const { bool grayIsPresent = false, nonGrayIsPresent = false; for (const ADGameFileDescription *fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { if (_grayListMap.contains(fileDesc->fileName)) { grayIsPresent = true; } else { nonGrayIsPresent = true; } } return (grayIsPresent && !nonGrayIsPresent); } Common::Error AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) { PluginList pl = PluginMan.getPlugins(PLUGIN_TYPE_ENGINE); if (pl.size() == 1) { const Plugin *metaEnginePlugin = PluginMan.getMetaEngineFromEngine(pl[0]); if (metaEnginePlugin) { return metaEnginePlugin->get().createInstance(syst, engine); } } return Common::Error(); } void AdvancedMetaEngineDetection::detectClashes() const { // First, check that we do not have duplicated entries in _gameIds Common::HashMap idsMap; for (const PlainGameDescriptor *g = _gameIds; g->gameId; g++) { if (idsMap.contains(g->gameId)) debug(0, "WARNING: Detection gameId for '%s:%s' has duplicates", getName(), g->gameId); idsMap[g->gameId] = 0; } for (const byte *descPtr = _gameDescriptors; ((const ADGameDescription *)descPtr)->gameId != nullptr; descPtr += _descItemSize) { const ADGameDescription *g = (const ADGameDescription *)descPtr; if (!idsMap.contains(g->gameId)) { debug(0, "WARNING: Detection gameId for '%s:%s' is not present in gameids", getName(), g->gameId); } else { idsMap[g->gameId]++; } // Perform sanity checks for entries with files inside archives for (const ADGameFileDescription &fileDesc : ((const ADGameDescription *)descPtr)->filesDescriptions) { if (fileDesc.fileName == nullptr && fileDesc.md5 == nullptr) { break; } if (gameFileToMD5Props(&fileDesc, ((const ADGameDescription *)descPtr)->flags) & kMD5Archive) { Common::StringTokenizer tok(fileDesc.fileName, ":"); uint numTokens = 0; while (!tok.empty()) { if (tok.nextToken().empty()) { break; } ++numTokens; } // We need exactly three tokens: : : if (numTokens != 3) { debug(0, "WARNING: Detection entry '%s' for gameId '%s:%s' is invalid", fileDesc.fileName, getName(), g->gameId); } } } } for (auto &k : idsMap) { if (k._value == 0 && k._key != getName()) debug(0, "WARNING: Detection gameId for '%s:%s' has no games in the detection table", getName(), k._key.c_str()); } } bool AdvancedMetaEngine::checkExtendedSaves(MetaEngineFeature f) const { return (f == kSavesUseExtendedFormat) || (f == kSimpleSavesNames) || (f == kSupportsListSaves) || (f == kSupportsDeleteSave) || (f == kSavesSupportMetaInfo) || (f == kSavesSupportThumbnail) || (f == kSavesSupportCreationDate) || (f == kSavesSupportPlayTime); }