mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-12 03:56:20 +00:00
bb813719b5
The engine ID identifies which engine should be used to launch the target. Also remove the 'single ID' system. Different games from engines that used that system now have different game IDs. Also-By: Matthew Hoops <clone2727@gmail.com>
589 lines
17 KiB
C++
589 lines
17 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 "engines/advancedDetector.h"
|
|
#include "engines/obsolete.h"
|
|
|
|
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");
|
|
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;
|
|
|
|
// If the GUI options were updated, we catch this here and update them in the users config
|
|
// file transparently.
|
|
Common::String lang = getGameGUIOptionsDescriptionLanguage(agdDesc.desc->language);
|
|
if (agdDesc.desc->flags & ADGF_ADDENGLISH)
|
|
lang += " " + getGameGUIOptionsDescriptionLanguage(Common::EN_ANY);
|
|
|
|
Common::updateGameGUIOptions(agdDesc.desc->guiOptions + _guiOptions, lang);
|
|
|
|
DetectedGame gameDescriptor = toDetectedGame(agdDesc);
|
|
|
|
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 Common::FSNode &parent, 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) {
|
|
Common::MacResManager macResMan;
|
|
|
|
if (!macResMan.open(parent, fname))
|
|
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* are present.
|
|
// Compute MD5s and file sizes for these 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;
|
|
FileProperties tmp;
|
|
|
|
if (filesProps.contains(fname))
|
|
continue;
|
|
|
|
if (getFileProperties(parent, allFiles, *g, fname, tmp)) {
|
|
debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5.c_str());
|
|
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)) {
|
|
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 Common::FSList &fslist, 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(fslist.begin()->getParent(), 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
|
|
}
|