scummvm/engines/advancedDetector.cpp
Bastien Bouclet 0f2c6cd9ea ENGINES: Change mac resource fork file detection to use the file cache
Common::MacResMan is now able to open files from a specified
Common::Archive. This is a bit hacky as dynamic_cast is used to break
the Archive encapsulation to retreive the underlying FSNode. It should
however be more correct than the previous code that assumed files were
at the root of the currently running game's path.

AdvancedDetector constructs a Common::Archive from its FileMap based
filesystem cache and uses it to detect the mac resource fork files.

This cuts the time it takes to run the detection code with all the
engines enabled as dynamic plugins on the 3DS to 30 s from 280 s.
2020-09-20 16:33:45 +02:00

635 lines
18 KiB
C++

/* 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#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/system.h"
#include "common/textconsole.h"
#include "common/translation.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 AdvancedMetaEngine::FileMap &fileMap) : _fileMap(fileMap) {}
bool hasFile(const Common::String &name) const override {
return _fileMap.contains(name);
}
int listMembers(Common::ArchiveMemberList &list) const override {
int files = 0;
for (AdvancedMetaEngine::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::String &name) const override {
AdvancedMetaEngine::FileMap::const_iterator it = _fileMap.find(name);
if (it == _fileMap.end()) {
return Common::ArchiveMemberPtr();
}
return Common::ArchiveMemberPtr(new Common::FSNode(it->_value));
}
Common::SeekableReadStream *createReadStreamForMember(const Common::String &name) const override {
Common::FSNode fsNode = _fileMap[name];
return fsNode.createReadStream();
}
private:
const AdvancedMetaEngine::FileMap &_fileMap;
};
static Common::String sanitizeName(const char *name) {
Common::String res;
while (*name) {
if (Common::isAlnum(*name))
res += tolower(*name);
name++;
}
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) {
Common::String res;
if (desc->flags & ADGF_AUTOGENTARGET && desc->extra && *desc->extra) {
res = sanitizeName(desc->extra);
} else {
res = desc->gameId;
}
if (desc->flags & ADGF_DEMO) {
res = res + "-demo";
}
if (desc->flags & ADGF_CD) {
res = res + "-cd";
}
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 AdvancedMetaEngine::toDetectedGame(const ADDetectedGame &adGame) 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;
}
DetectedGame game(getEngineId(), desc->gameId, title, desc->language, desc->platform, extra);
game.hasUnknownFiles = adGame.hasUnknownFiles;
game.matchedFiles = adGame.matchedFiles;
game.preferredTarget = generatePreferredTarget(desc);
game.gameSupportLevel = kStableGame;
if (desc->flags & ADGF_UNSTABLE)
game.gameSupportLevel = kUnstableGame;
else if (desc->flags & ADGF_TESTING)
game.gameSupportLevel = kTestingGame;
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 cleanupPirated(ADDetectedGames &matched) {
// 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 AdvancedMetaEngine::detectGames(const Common::FSList &fslist) const {
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
ADDetectedGames matches = detectGame(fslist.begin()->getParent(), allFiles, Common::UNK_LANG, Common::kPlatformUnknown, "");
cleanupPirated(matches);
DetectedGames detectedGames;
for (uint i = 0; i < matches.size(); i++) {
DetectedGame game = toDetectedGame(matches[i]);
if (game.hasUnknownFiles) {
// Non fallback games with unknown files cannot be added/launched
game.canBeAdded = false;
}
detectedGames.push_back(game);
}
bool foundKnownGames = false;
for (uint i = 0; i < detectedGames.size(); i++) {
foundKnownGames |= detectedGames[i].canBeAdded;
}
if (!foundKnownGames) {
// Use fallback detector if there were no matches by other means
ADDetectedGame fallbackDetectionResult = fallbackDetect(allFiles, fslist);
if (fallbackDetectionResult.desc) {
DetectedGame fallbackDetectedGame = toDetectedGame(fallbackDetectionResult);
fallbackDetectedGame.preferredTarget += "-fallback";
detectedGames.push_back(fallbackDetectedGame);
}
}
return detectedGames;
}
const ExtraGuiOptions AdvancedMetaEngine::getExtraGuiOptions(const Common::String &target) const {
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 AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) const {
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::String path;
if (ConfMan.hasKey("path")) {
path = ConfMan.get("path");
} else {
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.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
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) {
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);
// 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;
debug(2, "Running %s", gameDescriptor.description.c_str());
initSubSystems(agdDesc.desc);
if (!createInstance(syst, engine, agdDesc.desc))
return Common::kNoGameDataFoundError;
else
return Common::kNoError;
}
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
}
}
bool AdvancedMetaEngine::getFileProperties(const FileMap &allFiles, const ADGameDescription &game, const Common::String fname, FileProperties &fileProps) const {
// FIXME/TODO: We don't handle the case that a file is listed as a regular
// file and as one with resource fork.
if (game.flags & ADGF_MACRESFORK) {
FileMapArchive fileMapArchive(allFiles);
Common::MacResManager macResMan;
if (!macResMan.open(fname, fileMapArchive))
return false;
fileProps.md5 = macResMan.computeResForkMD5AsString(_md5Bytes);
fileProps.size = macResMan.getResForkDataSize();
if (fileProps.size != 0)
return true;
}
if (!allFiles.contains(fname))
return false;
Common::File testFile;
if (!testFile.open(allFiles[fname]))
return false;
fileProps.size = (int32)testFile.size();
fileProps.md5 = Common::computeStreamMD5AsString(testFile, _md5Bytes);
return true;
}
ADDetectedGames AdvancedMetaEngine::detectGame(const Common::FSNode &parent, const FileMap &allFiles, Common::Language language, Common::Platform platform, const Common::String &extra) const {
FilePropertiesMap filesProps;
ADDetectedGames matched;
const ADGameFileDescription *fileDesc;
const ADGameDescription *g;
const byte *descPtr;
debug(3, "Starting detection in dir '%s'", parent.getPath().c_str());
// 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++) {
Common::String fname = fileDesc->fileName;
if (filesProps.contains(fname))
continue;
FileProperties tmp;
if (getFileProperties(allFiles, *g, fname, tmp)) {
debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str());
}
// Both positive and negative results are cached to avoid
// repeatedly checking for files.
filesProps[fname] = 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;
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;
if (!filesProps.contains(tstr) || filesProps[tstr].size == -1) {
allFilesPresent = false;
break;
}
game.matchedFiles[tstr] = filesProps[tstr];
if (game.hasUnknownFiles)
continue;
if (fileDesc->md5 != nullptr && fileDesc->md5 != filesProps[tstr].md5) {
debug(3, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesProps[tstr].md5.c_str());
game.hasUnknownFiles = true;
continue;
}
if (fileDesc->fileSize != -1 && fileDesc->fileSize != filesProps[tstr].size) {
debug(3, "Size Mismatch. Skipping");
game.hasUnknownFiles = true;
continue;
}
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) {
if (matched.empty() || strcmp(matched.back().desc->gameId, g->gameId) != 0)
matched.push_back(game);
}
if (allFilesPresent && !game.hasUnknownFiles) {
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(game);
} else if (curFilesMatched == maxFilesMatched) {
matched.push_back(game);
} else {
debug(2, " ... skipped");
}
gotAnyMatchesWithAllFiles = true;
} else {
debug(5, "Skipping game: %s (%s %s/%s) (%d)", g->gameId, g->extra,
getPlatformDescription(g->platform), getLanguageDescription(g->language), i);
}
}
return matched;
}
ADDetectedGame AdvancedMetaEngine::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) {
debug(3, "++ %s", *filenames);
if (!allFiles.contains(*filenames)) {
fileMissing = true;
break;
}
numMatchedFiles++;
}
if (!fileMissing) {
debug(4, "Matched: %s", agdesc->gameId);
if (numMatchedFiles > maxNumMatchedFiles) {
maxNumMatchedFiles = numMatchedFiles;
debug(4, "and overridden");
ADDetectedGame game(agdesc);
game.hasUnknownFiles = true;
for (filenames = ptr->filenames; *filenames; ++filenames) {
FileProperties tmp;
if (getFileProperties(allFiles, *agdesc, *filenames, tmp))
game.matchedFiles[*filenames] = tmp;
}
result = game;
}
}
}
return result;
}
PlainGameList AdvancedMetaEngine::getSupportedGames() const {
return PlainGameList(_gameIds);
}
PlainGameDescriptor AdvancedMetaEngine::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();
}
AdvancedMetaEngine::AdvancedMetaEngine(const void *descs, uint descItemSize, const PlainGameDescriptor *gameIds, const ADExtraGuiOptionsMap *extraGuiOptions)
: _gameDescriptors((const byte *)descs), _descItemSize(descItemSize), _gameIds(gameIds),
_extraGuiOptions(extraGuiOptions) {
_md5Bytes = 5000;
_flags = 0;
_guiOptions = GUIO_NONE;
_maxScanDepth = 1;
_directoryGlobs = NULL;
_matchFullPaths = false;
}
void AdvancedMetaEngine::initSubSystems(const ADGameDescription *gameDesc) const {
#ifdef ENABLE_EVENTRECORDER
if (gameDesc) {
g_eventRec.processGameDescription(gameDesc);
}
#endif
}