GameList AdvancedMetaEngine::detectGames(const Common::FSList &fslist) const { ADGameDescList matches; GameList detectedGames; FileMap allFiles; if (fslist.empty()) return detectedGames; // Compose a hashmap of all files in fslist. composeFileHashMap(allFiles, fslist, (_maxScanDepth == 0 ? 1 : _maxScanDepth)); // Run the detector on this matches = detectGame(fslist.begin()->getParent(), allFiles, Common::UNK_LANG, Common::kPlatformUnknown, ""); if (matches.empty()) { // Use fallback detector if there were no matches by other means const ADGameDescription *fallbackDesc = fallbackDetect(allFiles, fslist); if (fallbackDesc != 0) { GameDescriptor desc(toGameDescriptor(*fallbackDesc, _gameIds)); updateGameDescriptor(desc, fallbackDesc); detectedGames.push_back(desc); } } else { // Otherwise use the found matches cleanupPirated(matches); for (uint i = 0; i < matches.size(); i++) { GameDescriptor desc(toGameDescriptor(*matches[i], _gameIds)); updateGameDescriptor(desc, matches[i]); detectedGames.push_back(desc); } } return detectedGames; }
static void composeFileHashMap(const Common::FSList &fslist, DescMap &fileMD5Map, int depth, const char **globs) { if (depth <= 0) return; if (fslist.empty()) return; for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { if (!file->isDirectory()) { DetectorDesc d; d.node = *file; d.md5Entry = 0; fileMD5Map[file->getName()] = d; } else { if (!globs) continue; bool matched = false; for (const char **glob = globs; *glob; glob++) if (file->getName().matchString(*glob, true)) { matched = true; break; } if (!matched) continue; Common::FSList files; if (file->getChildren(files, Common::FSNode::kListAll)) { composeFileHashMap(files, fileMD5Map, depth - 1, globs); } } } }
static void composeFileHashMap(const Common::FSList &fslist, FileMap &allFiles, int depth, const char * const *directoryGlobs) { if (depth <= 0) return; if (fslist.empty()) return; // First we compose a hashmap of all files in fslist. // Includes nifty stuff like removing trailing dots and ignoring case. for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { if (file->isDirectory()) { Common::FSList files; if (!directoryGlobs) continue; bool matched = false; for (const char * const *glob = directoryGlobs; *glob; glob++) if (file->getName().matchString(*glob, true)) { matched = true; break; } if (!matched) continue; if (!file->getChildren(files, Common::FSNode::kListAll)) continue; composeFileHashMap(files, allFiles, depth - 1, directoryGlobs); } Common::String tstr = file->getName(); // Strip any trailing dot if (tstr.lastChar() == '.') tstr.deleteLastChar(); allFiles[tstr] = *file; // Record the presence of this file } }
void AdvancedMetaEngine::composeFileHashMap(FileMap &allFiles, const Common::FSList &fslist, int depth, const Common::String &parentName) const { if (depth <= 0) return; if (fslist.empty()) return; for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { Common::String tstr = (_matchFullPaths && !parentName.empty() ? parentName + "/" : "") + file->getName(); if (file->isDirectory()) { Common::FSList files; if (!_directoryGlobs) continue; bool matched = false; for (const char * const *glob = _directoryGlobs; *glob; glob++) if (file->getName().matchString(*glob, true)) { matched = true; break; } if (!matched) continue; if (!file->getChildren(files, Common::FSNode::kListAll)) continue; composeFileHashMap(allFiles, files, depth - 1, tstr); } // Strip any trailing dot if (tstr.lastChar() == '.') tstr.deleteLastChar(); allFiles[tstr] = *file; // Record the presence of this file } }
static ADGameDescList detectGame(const Common::FSList &fslist, const ADParams ¶ms, Common::Language language, Common::Platform platform, const Common::String &extra) { FileMap allFiles; SizeMD5Map filesSizeMD5; const ADGameFileDescription *fileDesc; const ADGameDescription *g; const byte *descPtr; if (fslist.empty()) return ADGameDescList(); Common::FSNode parent = fslist.begin()->getParent(); debug(3, "Starting detection in dir '%s'", parent.getPath().c_str()); // First we compose a hashmap of all files in fslist. // Includes nifty stuff like removing trailing dots and ignoring case. composeFileHashMap(fslist, allFiles, (params.depth == 0 ? 1 : params.depth), params.directoryGlobs); // Check which files are included in some ADGameDescription *and* present // in fslist. Compute MD5s and file sizes for these files. for (descPtr = params.descs; ((const ADGameDescription *)descPtr)->gameid != 0; descPtr += params.descItemSize) { g = (const ADGameDescription *)descPtr; for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { Common::String fname = fileDesc->fileName; SizeMD5 tmp; if (filesSizeMD5.contains(fname)) continue; // FIXME/TODO: We don't handle the case that a file is listed as a regular // file and as one with resource fork. if (g->flags & ADGF_MACRESFORK) { Common::MacResManager *macResMan = new Common::MacResManager(); if (macResMan->open(parent, fname)) { tmp.md5 = macResMan->computeResForkMD5AsString(params.md5Bytes); tmp.size = macResMan->getResForkDataSize(); debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str()); filesSizeMD5[fname] = tmp; } delete macResMan; } else { if (allFiles.contains(fname)) { debug(3, "+ %s", fname.c_str()); Common::File testFile; if (testFile.open(allFiles[fname])) { tmp.size = (int32)testFile.size(); tmp.md5 = Common::computeStreamMD5AsString(testFile, params.md5Bytes); } else { tmp.size = -1; } debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str()); filesSizeMD5[fname] = tmp; } } } } ADGameDescList matched; int maxFilesMatched = 0; bool gotAnyMatchesWithAllFiles = false; // MD5 based matching uint i; for (i = 0, descPtr = params.descs; ((const ADGameDescription *)descPtr)->gameid != 0; descPtr += params.descItemSize, ++i) { g = (const ADGameDescription *)descPtr; bool fileMissing = false; // 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 ((params.flags & kADFlagUseExtraAsHint) && !extra.empty() && g->extra != extra) continue; bool allFilesPresent = true; int curFilesMatched = 0; // Try to match all files for this game for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) { Common::String tstr = fileDesc->fileName; if (!filesSizeMD5.contains(tstr)) { fileMissing = true; allFilesPresent = false; break; } if (fileDesc->md5 != NULL && fileDesc->md5 != filesSizeMD5[tstr].md5) { debug(3, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesSizeMD5[tstr].md5.c_str()); fileMissing = true; break; } if (fileDesc->fileSize != -1 && fileDesc->fileSize != filesSizeMD5[tstr].size) { debug(3, "Size Mismatch. Skipping"); fileMissing = true; break; } debug(3, "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 = true; if (!fileMissing) { debug(2, "Found game: %s (%s %s/%s) (%d)", g->gameid, g->extra, getPlatformDescription(g->platform), getLanguageDescription(g->language), i); if (curFilesMatched > maxFilesMatched) { debug(2, " ... new best match, removing all previous candidates"); maxFilesMatched = curFilesMatched; matched.clear(); // Remove any prior, lower ranked matches. matched.push_back(g); } else if (curFilesMatched == maxFilesMatched) { matched.push_back(g); } else { debug(2, " ... skipped"); } } else { debug(5, "Skipping game: %s (%s %s/%s) (%d)", g->gameid, g->extra, getPlatformDescription(g->platform), getLanguageDescription(g->language), i); } } // We didn't find a match if (matched.empty()) { if (!filesSizeMD5.empty() && gotAnyMatchesWithAllFiles) { reportUnknown(parent, filesSizeMD5); } // Filename based fallback if (params.fileBasedFallback != 0) matched = detectGameFilebased(allFiles, params); } return matched; }
Common::Error AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) const { assert(engine); const ADGameDescription *agdDesc = 0; 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::String path; if (ConfMan.hasKey("path")) { path = ConfMan.get("path"); } else { path = "."; // This situation may happen only when game was // launched from a command line with wrong target and // no path was provided. // // A dummy entry will get created and will keep game path // We mark this entry, so it will not be added to the // config file. // // Fixes bug #1544799 ConfMan.setBool("autoadded", true); 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, true)) { warning("Game data path does not exist or is not a directory (%s)", path.c_str()); return Common::kNoGameDataFoundError; } if (files.empty()) return Common::kNoGameDataFoundError; // Compose a hashmap of all files in fslist. FileMap allFiles; composeFileHashMap(allFiles, files, (_maxScanDepth == 0 ? 1 : _maxScanDepth)); // Run the detector on this ADGameDescList matches = detectGame(files.begin()->getParent(), allFiles, language, platform, extra); if (cleanupPirated(matches)) return Common::kNoGameDataFoundError; if (_singleId == NULL) { // Find the first match with correct gameid. for (uint i = 0; i < matches.size(); i++) { if (matches[i]->gameId == gameid) { agdDesc = matches[i]; break; } } } else if (matches.size() > 0) { agdDesc = matches[0]; } if (agdDesc == 0) { // Use fallback detector if there were no matches by other means agdDesc = fallbackDetect(allFiles, files); if (agdDesc != 0) { // Seems we found a fallback match. But first perform a basic // sanity check: the gameid must match. if (_singleId == NULL && agdDesc->gameId != gameid) agdDesc = 0; } } if (agdDesc == 0) return Common::kNoGameDataFoundError; // If the GUI options were updated, we catch this here and update them in the users config // file transparently. Common::String lang = getGameGUIOptionsDescriptionLanguage(agdDesc->language); if (agdDesc->flags & ADGF_ADDENGLISH) lang += " " + getGameGUIOptionsDescriptionLanguage(Common::EN_ANY); Common::updateGameGUIOptions(agdDesc->guiOptions + _guiOptions, lang); GameDescriptor gameDescriptor = toGameDescriptor(*agdDesc, _gameIds); bool showTestingWarning = false; #ifdef RELEASE_BUILD showTestingWarning = true; #endif if (((gameDescriptor.getSupportLevel() == kUnstableGame || (gameDescriptor.getSupportLevel() == kTestingGame && showTestingWarning))) && !Engine::warnUserAboutUnsupportedGame()) return Common::kUserCanceled; debug(2, "Running %s", gameDescriptor.description().c_str()); initSubSystems(agdDesc); if (!createInstance(syst, engine, agdDesc)) return Common::kNoGameDataFoundError; else return Common::kNoError; }
static void detectGames(const Common::FSList &fslist, Common::List<DetectorResult> &results, const char *gameid) { DescMap fileMD5Map; DetectorResult dr; // Dive one level down since mac indy3/loom has its files split into directories. See Bug #1438631 composeFileHashMap(fslist, fileMD5Map, 2, directoryGlobs); // Iterate over all filename patterns. for (const GameFilenamePattern *gfp = gameFilenamesTable; gfp->gameid; ++gfp) { // If a gameid was specified, we only try to detect that specific game, // so we can just skip over everything with a differing gameid. if (gameid && scumm_stricmp(gameid, gfp->gameid)) continue; // Generate the detectname corresponding to the gfp. If the file doesn't // exist in the directory we are looking at, we can skip to the next // one immediately. Common::String file(generateFilenameForDetection(gfp->pattern, gfp->genMethod)); if (!fileMD5Map.contains(file)) continue; // Reset the DetectorResult variable dr.fp.pattern = gfp->pattern; dr.fp.genMethod = gfp->genMethod; dr.game.gameid = 0; dr.language = gfp->language; dr.md5.clear(); dr.extra = 0; // ____ _ _ // | _ \ __ _ _ __| |_ / | // | |_) / _` | '__| __| | | // | __/ (_| | | | |_ | | // |_| \__,_|_| \__| |_| // // PART 1: Trying to find an exact match using MD5. // // // Background: We found a valid detection file. Check if its MD5 // checksum occurs in our MD5 table. If it does, try to use that // to find an exact match. // // We only do that if the MD5 hadn't already been computed (since // we may look at some detection files multiple times). // DetectorDesc &d = fileMD5Map[file]; if (d.md5.empty()) { Common::SeekableReadStream *tmp = 0; bool isDiskImg = (file.hasSuffix(".d64") || file.hasSuffix(".dsk") || file.hasSuffix(".prg")); if (isDiskImg) { tmp = openDiskImage(d.node, gfp); debug(2, "Falling back to disk-based detection"); } else { tmp = d.node.createReadStream(); } Common::String md5str; if (tmp) md5str = computeStreamMD5AsString(*tmp, kMD5FileSizeLimit); if (!md5str.empty()) { d.md5 = md5str; d.md5Entry = findInMD5Table(md5str.c_str()); dr.md5 = d.md5; if (d.md5Entry) { // Exact match found. Compute the precise game settings. computeGameSettingsFromMD5(fslist, gfp, d.md5Entry, dr); // Print some debug info int filesize = tmp->size(); if (d.md5Entry->filesize != filesize) debug(1, "SCUMM detector found matching file '%s' with MD5 %s, size %d\n", file.c_str(), md5str.c_str(), filesize); // Sanity check: We *should* have found a matching gameid / variant at this point. // If not, then there's a bug in our data tables... assert(dr.game.gameid != 0); // Add it to the list of detected games results.push_back(dr); } } if (isDiskImg) closeDiskImage((ScummDiskImage*)tmp); delete tmp; } // If an exact match for this file has already been found, don't bother // looking at it anymore. if (d.md5Entry) continue; // ____ _ ____ // | _ \ __ _ _ __| |_ |___ \ * // | |_) / _` | '__| __| __) | // | __/ (_| | | | |_ / __/ // |_| \__,_|_| \__| |_____| // // PART 2: Fuzzy matching for files with unknown MD5. // // We loop over the game variants matching the gameid associated to // the gfp record. We then try to decide for each whether it could be // appropriate or not. dr.md5 = d.md5; for (const GameSettings *g = gameVariantsTable; g->gameid; ++g) { // Skip over entries with a different gameid. if (g->gameid[0] == 0 || scumm_stricmp(gfp->gameid, g->gameid)) continue; dr.game = *g; dr.extra = g->variant; // FIXME: We (ab)use 'variant' for the 'extra' description for now. if (gfp->platform != Common::kPlatformUnknown) dr.game.platform = gfp->platform; // If a variant has been specified, use that! if (gfp->variant) { if (!scumm_stricmp(gfp->variant, g->variant)) { // perfect match found results.push_back(dr); break; } continue; } // HACK: Perhaps it is some modified translation? dr.language = detectLanguage(fslist, g->id); // Add the game/variant to the candidates list if it is consistent // with the file(s) we are seeing. if (testGame(g, fileMD5Map, file)) results.push_back(dr); } } }